summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorLuke Bennett <lbennett@gitlab.com>2018-11-07 16:44:21 +0000
committerLuke Bennett <lbennett@gitlab.com>2019-02-13 00:17:52 +0000
commitaf989df0ec0c15f269071080ab08417e688dabf7 (patch)
tree53096af07d17412dc38b70327e69433b965504dd /app/assets
parent534a61179e2d0d7f9f376af1d01ed536e27f5b6d (diff)
downloadgitlab-ce-af989df0ec0c15f269071080ab08417e688dabf7.tar.gz
Improve the GitHub and Gitea import feature table interfaceimport-go-to-project-cta-nibble-frontend
These are frontend changes. Use Vue for the import feature UI for "githubish" providers (GitHub and Gitea). Add "Go to project" button after a successful import. Use CI-style status icons and improve spacing of the table and its component. Adds ETag polling to the github and gitea import jobs endpoint.
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue101
-rw-r--r--app/assets/javascripts/import_projects/components/import_status.vue47
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue55
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue110
-rw-r--r--app/assets/javascripts/import_projects/constants.js48
-rw-r--r--app/assets/javascripts/import_projects/event_hub.js3
-rw-r--r--app/assets/javascripts/import_projects/index.js47
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js106
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js20
-rw-r--r--app/assets/javascripts/import_projects/store/index.js15
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js55
-rw-r--r--app/assets/javascripts/import_projects/store/state.js15
-rw-r--r--app/assets/javascripts/pages/import/gitea/status/index.js7
-rw-r--r--app/assets/javascripts/pages/import/github/status/index.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue34
-rw-r--r--app/assets/stylesheets/pages/import.scss51
18 files changed, 728 insertions, 11 deletions
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
new file mode 100644
index 00000000000..777f8fa6691
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -0,0 +1,101 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { __, sprintf } from '~/locale';
+import ImportedProjectTableRow from './imported_project_table_row.vue';
+import ProviderRepoTableRow from './provider_repo_table_row.vue';
+import eventHub from '../event_hub';
+
+export default {
+ name: 'ImportProjectsTable',
+ components: {
+ ImportedProjectTableRow,
+ ProviderRepoTableRow,
+ LoadingButton,
+ GlLoadingIcon,
+ },
+ props: {
+ providerTitle: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']),
+ ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
+
+ emptyStateText() {
+ return sprintf(__('No %{providerTitle} repositories available to import'), {
+ providerTitle: this.providerTitle,
+ });
+ },
+
+ fromHeaderText() {
+ return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle });
+ },
+ },
+
+ mounted() {
+ return this.fetchRepos();
+ },
+
+ beforeDestroy() {
+ this.stopJobsPolling();
+ this.clearJobsEtagPoll();
+ },
+
+ methods: {
+ ...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']),
+
+ importAll() {
+ eventHub.$emit('importAll');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <p class="light text-nowrap mt-2 my-sm-0">
+ {{ s__('ImportProjects|Select the projects you want to import') }}
+ </p>
+ <loading-button
+ container-class="btn btn-success js-import-all"
+ :loading="isImportingAnyRepo"
+ :label="__('Import all repositories')"
+ :disabled="!hasProviderRepos"
+ type="button"
+ @click="importAll"
+ />
+ </div>
+ <gl-loading-icon
+ v-if="isLoadingRepos"
+ class="js-loading-button-icon import-projects-loading-icon"
+ :size="4"
+ />
+ <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
+ <table class="table import-table">
+ <thead>
+ <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
+ <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
+ <th class="import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <imported-project-table-row
+ v-for="project in importedProjects"
+ :key="project.id"
+ :project="project"
+ />
+ <provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" />
+ </tbody>
+ </table>
+ </div>
+ <div v-else class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/import_status.vue b/app/assets/javascripts/import_projects/components/import_status.vue
new file mode 100644
index 00000000000..9e3347a657f
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/import_status.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import STATUS_MAP from '../constants';
+
+export default {
+ name: 'ImportStatus',
+ components: {
+ CiIcon,
+ GlLoadingIcon,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ mappedStatus() {
+ return STATUS_MAP[this.status];
+ },
+
+ ciIconStatus() {
+ const { icon } = this.mappedStatus;
+
+ return {
+ icon: `status_${icon}`,
+ group: icon,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon
+ v-if="mappedStatus.loadingIcon"
+ :inline="true"
+ :class="mappedStatus.textClass"
+ class="align-middle mr-2"
+ />
+ <ci-icon v-else css-classes="align-middle mr-2" :status="ciIconStatus" />
+ <span :class="mappedStatus.textClass">{{ mappedStatus.text }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
new file mode 100644
index 00000000000..ab2bd87ee9f
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
@@ -0,0 +1,55 @@
+<script>
+import ImportStatus from './import_status.vue';
+import { STATUSES } from '../constants';
+
+export default {
+ name: 'ImportedProjectTableRow',
+ components: {
+ ImportStatus,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ displayFullPath() {
+ return this.project.fullPath.replace(/^\//, '');
+ },
+
+ isFinished() {
+ return this.project.importStatus === STATUSES.FINISHED;
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="js-imported-project import-row">
+ <td>
+ <a
+ :href="project.providerLink"
+ rel="noreferrer noopener"
+ target="_blank"
+ class="js-provider-link"
+ >
+ {{ project.importSource }}
+ </a>
+ </td>
+ <td class="js-full-path">{{ displayFullPath }}</td>
+ <td><import-status :status="project.importStatus" /></td>
+ <td>
+ <a
+ v-if="isFinished"
+ class="btn btn-default js-go-to-project"
+ :href="project.fullPath"
+ rel="noreferrer noopener"
+ target="_blank"
+ >
+ {{ __('Go to project') }}
+ </a>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
new file mode 100644
index 00000000000..7cc29fa1b91
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -0,0 +1,110 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import { __ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import eventHub from '../event_hub';
+import { STATUSES } from '../constants';
+import ImportStatus from './import_status.vue';
+
+export default {
+ name: 'ProviderRepoTableRow',
+ components: {
+ Select2Select,
+ LoadingButton,
+ ImportStatus,
+ },
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ targetNamespace: this.$store.state.defaultTargetNamespace,
+ newName: this.repo.sanitizedName,
+ };
+ },
+
+ computed: {
+ ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']),
+
+ ...mapGetters(['namespaceSelectOptions']),
+
+ importButtonText() {
+ return this.ciCdOnly ? __('Connect') : __('Import');
+ },
+
+ select2Options() {
+ return {
+ data: this.namespaceSelectOptions,
+ containerCssClass:
+ 'import-namespace-select js-namespace-select qa-project-namespace-select',
+ };
+ },
+
+ isLoadingImport() {
+ return this.reposBeingImported.includes(this.repo.id);
+ },
+
+ status() {
+ return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE;
+ },
+ },
+
+ created() {
+ eventHub.$on('importAll', () => this.importRepo());
+ },
+
+ methods: {
+ ...mapActions(['fetchImport']),
+
+ importRepo() {
+ return this.fetchImport({
+ newName: this.newName,
+ targetNamespace: this.targetNamespace,
+ repo: this.repo,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="qa-project-import-row js-provider-repo import-row">
+ <td>
+ <a
+ :href="repo.providerLink"
+ rel="noreferrer noopener"
+ target="_blank"
+ class="js-provider-link"
+ >
+ {{ repo.fullName }}
+ </a>
+ </td>
+ <td class="d-flex flex-wrap flex-lg-nowrap">
+ <select2-select v-model="targetNamespace" :options="select2Options" />
+ <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
+ >/</span
+ >
+ <input
+ v-model="newName"
+ type="text"
+ class="form-control import-project-name-input js-new-name qa-project-path-field"
+ />
+ </td>
+ <td><import-status :status="status" /></td>
+ <td>
+ <button
+ v-if="!isLoadingImport"
+ type="button"
+ class="qa-import-button js-import-button btn btn-default"
+ @click="importRepo"
+ >
+ {{ importButtonText }}
+ </button>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_projects/constants.js b/app/assets/javascripts/import_projects/constants.js
new file mode 100644
index 00000000000..ad33ca158d2
--- /dev/null
+++ b/app/assets/javascripts/import_projects/constants.js
@@ -0,0 +1,48 @@
+import { __ } from '../locale';
+
+// The `scheduling` status is only present on the client-side,
+// it is used as the status when we are requesting to start an import.
+
+export const STATUSES = {
+ FINISHED: 'finished',
+ FAILED: 'failed',
+ SCHEDULED: 'scheduled',
+ STARTED: 'started',
+ NONE: 'none',
+ SCHEDULING: 'scheduling',
+};
+
+const STATUS_MAP = {
+ [STATUSES.FINISHED]: {
+ icon: 'success',
+ text: __('Done'),
+ textClass: 'text-success',
+ },
+ [STATUSES.FAILED]: {
+ icon: 'failed',
+ text: __('Failed'),
+ textClass: 'text-danger',
+ },
+ [STATUSES.SCHEDULED]: {
+ icon: 'pending',
+ text: __('Scheduled'),
+ textClass: 'text-warning',
+ },
+ [STATUSES.STARTED]: {
+ icon: 'running',
+ text: __('Running…'),
+ textClass: 'text-info',
+ },
+ [STATUSES.NONE]: {
+ icon: 'created',
+ text: __('Not started'),
+ textClass: 'text-muted',
+ },
+ [STATUSES.SCHEDULING]: {
+ loadingIcon: true,
+ text: __('Scheduling'),
+ textClass: 'text-warning',
+ },
+};
+
+export default STATUS_MAP;
diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/import_projects/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
new file mode 100644
index 00000000000..5c77484aee1
--- /dev/null
+++ b/app/assets/javascripts/import_projects/index.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import { mapActions } from 'vuex';
+import Translate from '../vue_shared/translate';
+import ImportProjectsTable from './components/import_projects_table.vue';
+import { parseBoolean } from '../lib/utils/common_utils';
+import store from './store';
+
+Vue.use(Translate);
+
+export default function mountImportProjectsTable(mountElement) {
+ if (!mountElement) return undefined;
+
+ const {
+ reposPath,
+ provider,
+ providerTitle,
+ canSelectNamespace,
+ jobsPath,
+ importPath,
+ ciCdOnly,
+ } = mountElement.dataset;
+
+ return new Vue({
+ el: mountElement,
+ store,
+
+ created() {
+ this.setInitialData({
+ reposPath,
+ provider,
+ jobsPath,
+ importPath,
+ defaultTargetNamespace: gon.current_username,
+ ciCdOnly: parseBoolean(ciCdOnly),
+ canSelectNamespace: parseBoolean(canSelectNamespace),
+ });
+ },
+
+ methods: {
+ ...mapActions(['setInitialData']),
+ },
+
+ render(createElement) {
+ return createElement(ImportProjectsTable, { props: { providerTitle } });
+ },
+ });
+}
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
new file mode 100644
index 00000000000..c44500937cc
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -0,0 +1,106 @@
+import Visibility from 'visibilityjs';
+import * as types from './mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import Poll from '~/lib/utils/poll';
+import createFlash from '~/flash';
+import { s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+let eTagPoll;
+
+export const clearJobsEtagPoll = () => {
+ eTagPoll = null;
+};
+export const stopJobsPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+export const restartJobsPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+
+export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos);
+export const receiveReposSuccess = ({ commit }, repos) =>
+ commit(types.RECEIVE_REPOS_SUCCESS, repos);
+export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR);
+export const fetchRepos = ({ state, dispatch }) => {
+ dispatch('requestRepos');
+
+ return axios
+ .get(state.reposPath)
+ .then(({ data }) =>
+ dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ )
+ .then(() => dispatch('fetchJobs'))
+ .catch(() => {
+ createFlash(
+ sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
+ provider: state.provider,
+ }),
+ );
+
+ dispatch('receiveReposError');
+ });
+};
+
+export const requestImport = ({ commit, state }, repoId) => {
+ if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId);
+};
+export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) =>
+ commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId });
+export const receiveImportError = ({ commit }, repoId) =>
+ commit(types.RECEIVE_IMPORT_ERROR, repoId);
+export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => {
+ dispatch('requestImport', repo.id);
+
+ return axios
+ .post(state.importPath, {
+ ci_cd_only: state.ciCdOnly,
+ new_name: newName,
+ repo_id: repo.id,
+ target_namespace: targetNamespace,
+ })
+ .then(({ data }) =>
+ dispatch('receiveImportSuccess', {
+ importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
+ repoId: repo.id,
+ }),
+ )
+ .catch(() => {
+ createFlash(s__('ImportProjects|Importing the project failed'));
+
+ dispatch('receiveImportError', { repoId: repo.id });
+ });
+};
+
+export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
+ commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
+export const fetchJobs = ({ state, dispatch }) => {
+ if (eTagPoll) return;
+
+ eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(state.jobsPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data }) =>
+ dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ dispatch('restartJobsPolling');
+ } else {
+ dispatch('stopJobsPolling');
+ }
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
new file mode 100644
index 00000000000..f03474a8404
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -0,0 +1,20 @@
+export const namespaceSelectOptions = state => {
+ const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
+ id: fullPath,
+ text: fullPath,
+ }));
+
+ return [
+ { text: 'Groups', children: serializedNamespaces },
+ {
+ text: 'Users',
+ children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
+ },
+ ];
+};
+
+export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
+
+export const hasProviderRepos = state => state.providerRepos.length > 0;
+
+export const hasImportedProjects = state => state.importedProjects.length > 0;
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
new file mode 100644
index 00000000000..6ac9bfd8189
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+});
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
new file mode 100644
index 00000000000..6ba3fd6f29e
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -0,0 +1,11 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+
+export const REQUEST_REPOS = 'REQUEST_REPOS';
+export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
+export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
+
+export const REQUEST_IMPORT = 'REQUEST_IMPORT';
+export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
+export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
+
+export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
new file mode 100644
index 00000000000..b88de0268e7
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+
+ [types.REQUEST_REPOS](state) {
+ state.isLoadingRepos = true;
+ },
+
+ [types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) {
+ state.isLoadingRepos = false;
+
+ state.importedProjects = importedProjects;
+ state.providerRepos = providerRepos;
+ state.namespaces = namespaces;
+ },
+
+ [types.RECEIVE_REPOS_ERROR](state) {
+ state.isLoadingRepos = false;
+ },
+
+ [types.REQUEST_IMPORT](state, repoId) {
+ state.reposBeingImported.push(repoId);
+ },
+
+ [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
+ const existingRepoIndex = state.reposBeingImported.indexOf(repoId);
+ if (state.reposBeingImported.includes(repoId))
+ state.reposBeingImported.splice(existingRepoIndex, 1);
+
+ const providerRepoIndex = state.providerRepos.findIndex(
+ providerRepo => providerRepo.id === repoId,
+ );
+ state.providerRepos.splice(providerRepoIndex, 1);
+ state.importedProjects.unshift(importedProject);
+ },
+
+ [types.RECEIVE_IMPORT_ERROR](state, repoId) {
+ const repoIndex = state.reposBeingImported.indexOf(repoId);
+ if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1);
+ },
+
+ [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
+ updatedProjects.forEach(updatedProject => {
+ const existingProject = state.importedProjects.find(
+ importedProject => importedProject.id === updatedProject.id,
+ );
+
+ Vue.set(existingProject, 'importStatus', updatedProject.importStatus);
+ });
+ },
+};
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
new file mode 100644
index 00000000000..637fef6e53c
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ reposPath: '',
+ importPath: '',
+ jobsPath: '',
+ currentProjectId: '',
+ provider: '',
+ currentUsername: '',
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ reposBeingImported: [],
+ isLoadingRepos: false,
+ canSelectNamespace: false,
+ ciCdOnly: false,
+});
diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js
new file mode 100644
index 00000000000..dcd84f0faf9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/gitea/status/index.js
@@ -0,0 +1,7 @@
+import mountImportProjectsTable from '~/import_projects';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('import-projects-mount-element');
+
+ mountImportProjectsTable(mountElement);
+});
diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js
new file mode 100644
index 00000000000..dcd84f0faf9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/github/status/index.js
@@ -0,0 +1,7 @@
+import mountImportProjectsTable from '~/import_projects';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('import-projects-mount-element');
+
+ mountImportProjectsTable(mountElement);
+});
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index b8eb555106f..2f498c4fa2a 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: false,
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
cssClass() {
@@ -59,5 +64,5 @@ export default {
};
</script>
<template>
- <span :class="cssClass"> <icon :name="icon" :size="size" /> </span>
+ <span :class="cssClass"> <icon :name="icon" :size="size" :css-classes="cssClasses" /> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
new file mode 100644
index 00000000000..19c5da0461a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -0,0 +1,34 @@
+<script>
+import $ from 'jquery';
+
+export default {
+ name: 'Select2Select',
+ props: {
+ options: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mounted() {
+ $(this.$refs.dropdownInput)
+ .val(this.value)
+ .select2(this.options)
+ .on('change', event => this.$emit('input', event.target.value));
+ },
+
+ beforeDestroy() {
+ $(this.$refs.dropdownInput).select2('destroy');
+ },
+};
+</script>
+
+<template>
+ <input ref="dropdownInput" type="hidden" />
+</template>
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index a4f76a9495a..7f800367cad 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,20 +1,51 @@
-.import-jobs-from-col,
.import-jobs-to-col {
- width: 40%;
+ width: 39%;
}
.import-jobs-status-col {
- width: 20%;
+ width: 15%;
}
-.btn-import {
- .loading-icon {
- display: none;
+.import-jobs-cta-col {
+ width: 1%;
+}
+
+.import-project-name-input {
+ border-radius: 0 $border-radius-default $border-radius-default 0;
+ position: relative;
+ left: -1px;
+ max-width: 300px;
+}
+
+.import-namespace-select {
+ width: auto !important;
+
+ > .select2-choice {
+ border-radius: $border-radius-default 0 0 $border-radius-default;
+ position: relative;
+ left: 1px;
}
+}
- &.is-loading {
- .loading-icon {
- display: inline-block;
- }
+.import-slash-divider {
+ background-color: $gray-lightest;
+ border: 1px solid $border-color;
+}
+
+.import-row {
+ height: 55px;
+}
+
+.import-table {
+ .import-jobs-from-col,
+ .import-jobs-to-col,
+ .import-jobs-status-col,
+ .import-jobs-cta-col {
+ border-bottom-width: 1px;
+ padding-left: $gl-padding;
}
}
+
+.import-projects-loading-icon {
+ margin-top: $gl-padding-32;
+}