Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/import_projects')
-rw-r--r--app/assets/javascripts/import_projects/components/bitbucket_status_table.vue3
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue177
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue28
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue8
-rw-r--r--app/assets/javascripts/import_projects/components/page_query_param_sync.vue39
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue105
-rw-r--r--app/assets/javascripts/import_projects/event_hub.js3
-rw-r--r--app/assets/javascripts/import_projects/index.js37
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js142
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js42
-rw-r--r--app/assets/javascripts/import_projects/store/index.js8
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js100
-rw-r--r--app/assets/javascripts/import_projects/store/state.js16
-rw-r--r--app/assets/javascripts/import_projects/utils.js7
15 files changed, 479 insertions, 246 deletions
diff --git a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
index f673a0e42dc..bc8aa522596 100644
--- a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
+++ b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
@@ -9,6 +9,7 @@ export default {
GlSprintf,
GlLink,
},
+ inheritAttrs: false,
props: {
providerTitle: {
type: String,
@@ -28,7 +29,7 @@ export default {
};
</script>
<template>
- <import-projects-table :provider-title="providerTitle">
+ <import-projects-table :provider-title="providerTitle" v-bind="$attrs">
<template #actions>
<slot name="actions"></slot>
</template>
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 6a467fb8c6a..72fdaca7e24 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -3,10 +3,12 @@ import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
-import eventHub from '../event_hub';
+import PageQueryParamSync from './page_query_param_sync.vue';
+import { isProjectImportable } from '../utils';
const reposFetchThrottleDelay = 1000;
@@ -16,8 +18,10 @@ export default {
ImportedProjectTableRow,
ProviderRepoTableRow,
IncompatibleRepoTableRow,
+ PageQueryParamSync,
GlLoadingIcon,
GlButton,
+ PaginationLinks,
},
props: {
providerTitle: {
@@ -29,23 +33,37 @@ export default {
required: false,
default: true,
},
+ paginatable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- ...mapState([
- 'importedProjects',
- 'providerRepos',
- 'incompatibleRepos',
- 'isLoadingRepos',
- 'filter',
- ]),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
...mapGetters([
+ 'isLoading',
'isImportingAnyRepo',
- 'hasProviderRepos',
- 'hasImportedProjects',
+ 'hasImportableRepos',
'hasIncompatibleRepos',
]),
+ availableNamespaces() {
+ const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
+ id: fullPath,
+ text: fullPath,
+ }));
+
+ return [
+ { text: __('Groups'), children: serializedNamespaces },
+ {
+ text: __('Users'),
+ children: [{ id: this.defaultTargetNamespace, text: this.defaultTargetNamespace }],
+ },
+ ];
+ },
+
importAllButtonText() {
return this.hasIncompatibleRepos
? __('Import all compatible repositories')
@@ -64,7 +82,8 @@ export default {
},
mounted() {
- return this.fetchRepos();
+ this.fetchNamespaces();
+ this.fetchRepos();
},
beforeDestroy() {
@@ -75,17 +94,14 @@ export default {
methods: {
...mapActions([
'fetchRepos',
- 'fetchReposFiltered',
- 'fetchJobs',
+ 'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
+ 'importAll',
+ 'setPage',
]),
- importAll() {
- eventHub.$emit('importAll');
- },
-
handleFilterInput({ target }) {
this.setFilter(target.value);
},
@@ -93,79 +109,90 @@ export default {
throttledFetchRepos: throttle(function fetch() {
this.fetchRepos();
}, reposFetchThrottleDelay),
+
+ isProjectImportable,
},
};
</script>
<template>
<div>
+ <page-query-param-sync :page="pageInfo.page" @popstate="setPage" />
+
<p class="light text-nowrap mt-2">
{{ s__('ImportProjects|Select the projects you want to import') }}
</p>
<template v-if="hasIncompatibleRepos">
- <slot name="incompatible-repos-warning"> </slot>
+ <slot name="incompatible-repos-warning"></slot>
</template>
- <div
- v-if="!isLoadingRepos"
- class="d-flex justify-content-between align-items-end flex-wrap mb-3"
- >
- <gl-button
- variant="success"
- :loading="isImportingAnyRepo"
- :disabled="!hasProviderRepos"
- type="button"
- @click="importAll"
- >
- {{ importAllButtonText }}
- </gl-button>
- <slot name="actions"></slot>
- <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <input
- :value="filter"
- data-qa-selector="githubish_import_filter_field"
- class="form-control"
- name="filter"
- :placeholder="__('Filter your projects by name')"
- autofocus
- size="40"
- @input="handleFilterInput($event)"
- @keyup.enter="throttledFetchRepos"
- />
- </form>
- </div>
<gl-loading-icon
- v-if="isLoadingRepos"
+ v-if="isLoading"
class="js-loading-button-icon import-projects-loading-icon"
size="md"
/>
- <div
- v-else-if="hasProviderRepos || hasImportedProjects || hasIncompatibleRepos"
- 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" />
- <incompatible-repo-table-row
- v-for="repo in incompatibleRepos"
- :key="repo.id"
- :repo="repo"
+ <template v-if="!isLoading">
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <gl-button
+ variant="success"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="importAll"
+ >{{ importAllButtonText }}</gl-button
+ >
+ <slot name="actions"></slot>
+ <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
+ <input
+ :value="filter"
+ data-qa-selector="githubish_import_filter_field"
+ class="form-control"
+ name="filter"
+ :placeholder="__('Filter your projects by name')"
+ autofocus
+ size="40"
+ @input="handleFilterInput($event)"
+ @keyup.enter="throttledFetchRepos"
/>
- </tbody>
- </table>
- </div>
- <div v-else class="text-center">
- <strong>{{ emptyStateText }}</strong>
- </div>
+ </form>
+ </div>
+ <div v-if="repositories.length" 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>
+ <template v-for="repo in repositories">
+ <incompatible-repo-table-row
+ v-if="repo.importSource.incompatible"
+ :key="repo.importSource.id"
+ :repo="repo"
+ />
+ <provider-repo-table-row
+ v-else-if="isProjectImportable(repo)"
+ :key="repo.importSource.id"
+ :repo="repo"
+ :available-namespaces="availableNamespaces"
+ />
+ <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <div v-else class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
+ <pagination-links
+ v-if="paginatable"
+ align="center"
+ class="gl-mt-3"
+ :page-info="pageInfo"
+ :prev-page="pageInfo.page - 1"
+ :next-page="repositories.length && pageInfo.page + 1"
+ :change="setPage"
+ />
+ </template>
</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
index ab2bd87ee9f..50e735b4478 100644
--- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import ImportStatus from './import_status.vue';
import { STATUSES } from '../constants';
@@ -6,6 +7,7 @@ export default {
name: 'ImportedProjectTableRow',
components: {
ImportStatus,
+ GlIcon,
},
props: {
project: {
@@ -16,7 +18,7 @@ export default {
computed: {
displayFullPath() {
- return this.project.fullPath.replace(/^\//, '');
+ return this.project.importedProject.fullPath.replace(/^\//, '');
},
isFinished() {
@@ -27,28 +29,30 @@ export default {
</script>
<template>
- <tr class="js-imported-project import-row">
+ <tr class="import-row">
<td>
<a
- :href="project.providerLink"
+ :href="project.importSource.providerLink"
rel="noreferrer noopener"
target="_blank"
- class="js-provider-link"
- >
- {{ project.importSource }}
+ data-testid="providerLink"
+ >{{ project.importSource.fullName }}
+ <gl-icon v-if="project.importSource.providerLink" name="external-link" />
</a>
</td>
- <td class="js-full-path">{{ displayFullPath }}</td>
- <td><import-status :status="project.importStatus" /></td>
+ <td data-testid="fullPath">{{ 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"
+ class="btn btn-default"
+ data-testid="goToProject"
+ :href="project.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
- >
- {{ __('Go to project') }}
+ >{{ __('Go to project') }}
</a>
</td>
</tr>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
index fa2fb439eac..3140585ccd7 100644
--- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlBadge,
+ GlIcon,
},
props: {
repo: {
@@ -17,8 +18,9 @@ export default {
<template>
<tr class="import-row">
<td>
- <a :href="repo.providerLink" rel="noreferrer noopener" target="_blank">
- {{ repo.fullName }}
+ <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
+ >{{ repo.importSource.fullName }}
+ <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
<td></td>
diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
new file mode 100644
index 00000000000..5ba3d70f5d0
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
@@ -0,0 +1,39 @@
+<script>
+import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+
+export default {
+ props: {
+ page: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ watch: {
+ page(newPage) {
+ updateHistory({
+ url: setUrlParams({
+ page: newPage === 1 ? null : newPage,
+ }),
+ });
+ },
+ },
+
+ created() {
+ window.addEventListener('popstate', this.updatePage);
+ },
+
+ beforeDestroy() {
+ window.removeEventListener('popstate', this.updatePage);
+ },
+
+ methods: {
+ updatePage() {
+ const page = parseInt(queryToObject(window.location.search).page, 10) || 1;
+ this.$emit('popstate', page);
+ },
+ },
+
+ render: () => null,
+};
+</script>
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
index 63524d61146..d8cffc6a7d5 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -1,9 +1,8 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
-import eventHub from '../event_hub';
-import { STATUSES } from '../constants';
import ImportStatus from './import_status.vue';
export default {
@@ -11,25 +10,26 @@ export default {
components: {
Select2Select,
ImportStatus,
+ GlIcon,
},
props: {
repo: {
type: Object,
required: true,
},
- },
-
- data() {
- return {
- targetNamespace: this.$store.state.defaultTargetNamespace,
- newName: this.repo.sanitizedName,
- };
+ availableNamespaces: {
+ type: Array,
+ required: true,
+ },
},
computed: {
- ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']),
+ ...mapState(['ciCdOnly']),
+ ...mapGetters(['getImportTarget']),
- ...mapGetters(['namespaceSelectOptions']),
+ importTarget() {
+ return this.getImportTarget(this.repo.importSource.id);
+ },
importButtonText() {
return this.ciCdOnly ? __('Connect') : __('Import');
@@ -37,37 +37,36 @@ export default {
select2Options() {
return {
- data: this.namespaceSelectOptions,
- containerCssClass:
- 'import-namespace-select js-namespace-select qa-project-namespace-select w-auto',
+ data: this.availableNamespaces,
+ containerCssClass: 'import-namespace-select qa-project-namespace-select w-auto',
};
},
- isLoadingImport() {
- return this.reposBeingImported.includes(this.repo.id);
+ targetNamespaceSelect: {
+ get() {
+ return this.importTarget.targetNamespace;
+ },
+ set(value) {
+ this.updateImportTarget({ targetNamespace: value });
+ },
},
- status() {
- return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE;
+ newNameInput: {
+ get() {
+ return this.importTarget.newName;
+ },
+ set(value) {
+ this.updateImportTarget({ newName: value });
+ },
},
},
- created() {
- eventHub.$on('importAll', this.importRepo);
- },
-
- beforeDestroy() {
- eventHub.$off('importAll', this.importRepo);
- },
-
methods: {
- ...mapActions(['fetchImport']),
-
- importRepo() {
- return this.fetchImport({
- newName: this.newName,
- targetNamespace: this.targetNamespace,
- repo: this.repo,
+ ...mapActions(['fetchImport', 'setImportTarget']),
+ updateImportTarget(changedValues) {
+ this.setImportTarget({
+ repoId: this.repo.importSource.id,
+ importTarget: { ...this.importTarget, ...changedValues },
});
},
},
@@ -75,35 +74,39 @@ export default {
</script>
<template>
- <tr class="qa-project-import-row js-provider-repo import-row">
+ <tr class="qa-project-import-row import-row">
<td>
<a
- :href="repo.providerLink"
+ :href="repo.importSource.providerLink"
rel="noreferrer noopener"
target="_blank"
- class="js-provider-link"
- >
- {{ repo.fullName }}
+ data-testid="providerLink"
+ >{{ repo.importSource.fullName }}
+ <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</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"
- />
+ <template v-if="repo.target">{{ repo.target }}</template>
+ <template v-else>
+ <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
+ <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
+ >/</span
+ >
+ <input
+ v-model="newNameInput"
+ type="text"
+ class="form-control import-project-name-input qa-project-path-field"
+ />
+ </template>
+ </td>
+ <td>
+ <import-status :status="repo.importStatus" />
</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"
+ class="qa-import-button btn btn-default"
+ @click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
</button>
diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/import_projects/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
index 68ba04aa9dd..79fbd58e355 100644
--- a/app/assets/javascripts/import_projects/index.js
+++ b/app/assets/javascripts/import_projects/index.js
@@ -2,28 +2,44 @@ import Vue from 'vue';
import Translate from '../vue_shared/translate';
import ImportProjectsTable from './components/import_projects_table.vue';
import { parseBoolean } from '../lib/utils/common_utils';
+import { queryToObject } from '../lib/utils/url_utility';
import createStore from './store';
Vue.use(Translate);
export function initStoreFromElement(element) {
const {
- reposPath,
- provider,
+ ciCdOnly,
canSelectNamespace,
+ provider,
+
+ reposPath,
jobsPath,
importPath,
- ciCdOnly,
+ namespacesPath,
+ paginatable,
} = element.dataset;
+ const params = queryToObject(document.location.search);
+ const page = parseInt(params.page ?? 1, 10);
+
return createStore({
- reposPath,
- provider,
- jobsPath,
- importPath,
- defaultTargetNamespace: gon.current_username,
- ciCdOnly: parseBoolean(ciCdOnly),
- canSelectNamespace: parseBoolean(canSelectNamespace),
+ initialState: {
+ defaultTargetNamespace: gon.current_username,
+ ciCdOnly: parseBoolean(ciCdOnly),
+ canSelectNamespace: parseBoolean(canSelectNamespace),
+ provider,
+ pageInfo: {
+ page,
+ },
+ },
+ endpoints: {
+ reposPath,
+ jobsPath,
+ importPath,
+ namespacesPath,
+ },
+ hasPagination: parseBoolean(paginatable),
});
}
@@ -31,6 +47,7 @@ export function initPropsFromElement(element) {
return {
providerTitle: element.dataset.providerTitle,
filterable: parseBoolean(element.dataset.filterable),
+ paginatable: parseBoolean(element.dataset.paginatable),
};
}
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index 8d8d33f5972..af410f411d8 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -1,41 +1,86 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { isProjectImportable } from '../utils';
+import {
+ convertObjectPropsToCamelCase,
+ normalizeHeaders,
+ parseIntPagination,
+} from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import { visitUrl } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll;
const hasRedirectInError = e => e?.response?.data?.error?.redirect;
const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect);
+const pathWithParams = ({ path, ...params }) => {
+ const filteredParams = Object.fromEntries(
+ Object.entries(params).filter(([, value]) => value !== ''),
+ );
+ const queryString = objectToQuery(filteredParams);
+ return queryString ? `${path}?${queryString}` : path;
+};
+
+const isRequired = () => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('param is required');
+};
-export const clearJobsEtagPoll = () => {
+const clearJobsEtagPoll = () => {
eTagPoll = null;
};
-export const stopJobsPolling = () => {
+
+const stopJobsPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
-export const restartJobsPolling = () => {
+
+const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
-export const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
+const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
+
+const setImportTarget = ({ commit }, { repoId, importTarget }) =>
+ commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
+
+const importAll = ({ state, dispatch }) => {
+ return Promise.all(
+ state.repositories
+ .filter(isProjectImportable)
+ .map(r => dispatch('fetchImport', r.importSource.id)),
+ );
+};
-export const fetchRepos = ({ state, dispatch, commit }) => {
+const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
+ state,
+ dispatch,
+ commit,
+}) => {
dispatch('stopJobsPolling');
commit(types.REQUEST_REPOS);
- const { provider } = state;
+ const { provider, filter } = state;
return axios
- .get(reposPathWithFilter(state))
- .then(({ data }) =>
- commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
+ .get(
+ pathWithParams({
+ path: reposPath,
+ filter,
+ page: hasPagination ? state.pageInfo.page.toString() : '',
+ }),
)
+ .then(({ data, headers }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+
+ if ('X-PAGE' in normalizedHeaders) {
+ commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders));
+ }
+
+ commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
+ })
.then(() => dispatch('fetchJobs'))
.catch(e => {
if (hasRedirectInError(e)) {
@@ -52,24 +97,26 @@ export const fetchRepos = ({ state, dispatch, commit }) => {
});
};
-export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo }) => {
- if (!state.reposBeingImported.includes(repo.id)) {
- commit(types.REQUEST_IMPORT, repo.id);
- }
+const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, getters }, repoId) => {
+ const { ciCdOnly } = state;
+ const importTarget = getters.getImportTarget(repoId);
+
+ commit(types.REQUEST_IMPORT, { repoId, importTarget });
+ const { newName, targetNamespace } = importTarget;
return axios
- .post(state.importPath, {
- ci_cd_only: state.ciCdOnly,
+ .post(importPath, {
+ repo_id: repoId,
+ ci_cd_only: ciCdOnly,
new_name: newName,
- repo_id: repo.id,
target_namespace: targetNamespace,
})
- .then(({ data }) =>
+ .then(({ data }) => {
commit(types.RECEIVE_IMPORT_SUCCESS, {
importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
- repoId: repo.id,
- }),
- )
+ repoId,
+ });
+ })
.catch(e => {
const serverErrorMessage = e?.response?.data?.errors;
const flashMessage = serverErrorMessage
@@ -84,14 +131,11 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo
createFlash(flashMessage);
- commit(types.RECEIVE_IMPORT_ERROR, repo.id);
+ commit(types.RECEIVE_IMPORT_ERROR, repoId);
});
};
-export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
- commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
-
-export const fetchJobs = ({ state, commit, dispatch }) => {
+export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
const { filter } = state;
if (eTagPoll) {
@@ -101,7 +145,7 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(jobsPathWithFilter(state)),
+ fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
@@ -129,5 +173,39 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
});
};
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => {
+ commit(types.REQUEST_NAMESPACES);
+ axios
+ .get(namespacesPath)
+ .then(({ data }) =>
+ commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
+ )
+ .catch(() => {
+ createFlash(s__('ImportProjects|Requesting namespaces failed'));
+
+ commit(types.RECEIVE_NAMESPACES_ERROR);
+ });
+};
+
+const setPage = ({ state, commit, dispatch }, page) => {
+ if (page === state.pageInfo.page) {
+ return null;
+ }
+
+ commit(types.SET_PAGE, page);
+ return dispatch('fetchRepos');
+};
+
+export default ({ endpoints = isRequired(), hasPagination }) => ({
+ clearJobsEtagPoll,
+ stopJobsPolling,
+ restartJobsPolling,
+ setFilter,
+ setImportTarget,
+ importAll,
+ setPage,
+ fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }),
+ fetchImport: fetchImportFactory(endpoints.importPath),
+ fetchJobs: fetchJobsFactory(endpoints.jobsPath),
+ fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
+});
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index e6eb8f523de..7d529c94d7d 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,29 +1,27 @@
-import { __ } from '~/locale';
+import { STATUSES } from '../constants';
-export const namespaceSelectOptions = state => {
- const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
- id: fullPath,
- text: fullPath,
- }));
+export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
- return [
- { text: __('Groups'), children: serializedNamespaces },
- {
- text: __('Users'),
- children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
- },
- ];
-};
+export const isImportingAnyRepo = state =>
+ state.repositories.some(repo =>
+ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
+ );
-export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
+export const hasIncompatibleRepos = state =>
+ state.repositories.some(repo => repo.importSource.incompatible);
-export const hasProviderRepos = state => state.providerRepos.length > 0;
+export const hasImportableRepos = state =>
+ state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
-export const hasImportedProjects = state => state.importedProjects.length > 0;
+export const getImportTarget = state => repoId => {
+ if (state.customImportTargets[repoId]) {
+ return state.customImportTargets[repoId];
+ }
-export const hasIncompatibleRepos = state => state.incompatibleRepos.length > 0;
+ const repo = state.repositories.find(r => r.importSource.id === repoId);
-export const reposPathWithFilter = ({ reposPath, filter = '' }) =>
- filter ? `${reposPath}?filter=${filter}` : reposPath;
-export const jobsPathWithFilter = ({ jobsPath, filter = '' }) =>
- filter ? `${jobsPath}?filter=${filter}` : jobsPath;
+ return {
+ newName: repo.importSource.sanitizedName,
+ targetNamespace: state.defaultTargetNamespace,
+ };
+};
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index 29deb7868ba..7ba12f81eb9 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -1,18 +1,16 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
-import * as actions from './actions';
+import actionsFactory from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
-export { state, actions, getters, mutations };
-
-export default initialState =>
+export default ({ initialState, endpoints, hasPagination }) =>
new Vuex.Store({
state: { ...state(), ...initialState },
- actions,
+ actions: actionsFactory({ endpoints, hasPagination }),
mutations,
getters,
});
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
index a23b7eef986..6adf5e59cff 100644
--- a/app/assets/javascripts/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -2,6 +2,10 @@ 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_NAMESPACES = 'REQUEST_NAMESPACES';
+export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS';
+export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR';
+
export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
@@ -9,3 +13,9 @@ export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER';
+
+export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
+
+export const SET_PAGE = 'SET_PAGE';
+
+export const SET_PAGE_INFO = 'SET_PAGE_INFO';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index ec62d0640ef..b3dbef896a6 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import * as types from './mutation_types';
+import { STATUSES } from '../constants';
export default {
[types.SET_FILTER](state, filter) {
@@ -12,48 +13,103 @@ export default {
[types.RECEIVE_REPOS_SUCCESS](
state,
- { importedProjects, providerRepos, incompatibleRepos, namespaces },
+ { importedProjects, providerRepos, incompatibleRepos = [] },
) {
+ // Normalizing structure to support legacy backend format
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
+
state.isLoadingRepos = false;
- state.importedProjects = importedProjects;
- state.providerRepos = providerRepos;
- state.incompatibleRepos = incompatibleRepos ?? [];
- state.namespaces = namespaces;
+ state.repositories = [
+ ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
+ importSource: {
+ id: `finished-${project.id}`,
+ fullName: importSource,
+ sanitizedName: project.name,
+ providerLink,
+ },
+ importStatus,
+ importedProject: project,
+ })),
+ ...providerRepos.map(project => ({
+ importSource: project,
+ importStatus: STATUSES.NONE,
+ importedProject: null,
+ })),
+ ...incompatibleRepos.map(project => ({
+ importSource: { ...project, incompatible: true },
+ importStatus: STATUSES.NONE,
+ importedProject: null,
+ })),
+ ];
},
[types.RECEIVE_REPOS_ERROR](state) {
state.isLoadingRepos = false;
},
- [types.REQUEST_IMPORT](state, repoId) {
- state.reposBeingImported.push(repoId);
+ [types.REQUEST_IMPORT](state, { repoId, importTarget }) {
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = STATUSES.SCHEDULING;
+ existingRepo.importedProject = {
+ fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
+ };
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
- const existingRepoIndex = state.reposBeingImported.indexOf(repoId);
- if (state.reposBeingImported.includes(repoId))
- state.reposBeingImported.splice(existingRepoIndex, 1);
+ const { importStatus, ...project } = importedProject;
- const providerRepoIndex = state.providerRepos.findIndex(
- providerRepo => providerRepo.id === repoId,
- );
- state.providerRepos.splice(providerRepoIndex, 1);
- state.importedProjects.unshift(importedProject);
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = importStatus;
+ existingRepo.importedProject = project;
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
- const repoIndex = state.reposBeingImported.indexOf(repoId);
- if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1);
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = STATUSES.NONE;
+ existingRepo.importedProject = null;
},
[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);
+ const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
+ if (repo) {
+ repo.importStatus = updatedProject.importStatus;
+ }
});
},
+
+ [types.REQUEST_NAMESPACES](state) {
+ state.isLoadingNamespaces = true;
+ },
+
+ [types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) {
+ state.isLoadingNamespaces = false;
+ state.namespaces = namespaces;
+ },
+
+ [types.RECEIVE_NAMESPACES_ERROR](state) {
+ state.isLoadingNamespaces = false;
+ },
+
+ [types.SET_IMPORT_TARGET](state, { repoId, importTarget }) {
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+
+ if (
+ importTarget.targetNamespace === state.defaultTargetNamespace &&
+ importTarget.newName === existingRepo.importSource.sanitizedName
+ ) {
+ Vue.delete(state.customImportTargets, repoId);
+ } else {
+ Vue.set(state.customImportTargets, repoId, importTarget);
+ }
+ },
+
+ [types.SET_PAGE_INFO](state, pageInfo) {
+ state.pageInfo = pageInfo;
+ },
+
+ [types.SET_PAGE](state, page) {
+ state.pageInfo.page = page;
+ },
};
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 0418d735b1d..3318181e4af 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -1,17 +1,13 @@
export default () => ({
- reposPath: '',
- importPath: '',
- jobsPath: '',
- currentProjectId: '',
provider: '',
- currentUsername: '',
- importedProjects: [],
- providerRepos: [],
- incompatibleRepos: [],
+ repositories: [],
namespaces: [],
- reposBeingImported: [],
+ customImportTargets: {},
isLoadingRepos: false,
- canSelectNamespace: false,
+ isLoadingNamespaces: false,
ciCdOnly: false,
filter: '',
+ pageInfo: {
+ page: 1,
+ },
});
diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js
new file mode 100644
index 00000000000..c2a2d5a607d
--- /dev/null
+++ b/app/assets/javascripts/import_projects/utils.js
@@ -0,0 +1,7 @@
+import { STATUSES } from './constants';
+
+// Will be expanded in future
+// eslint-disable-next-line import/prefer-default-export
+export function isProjectImportable(project) {
+ return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
+}