diff options
Diffstat (limited to 'app/assets/javascripts/import_projects/store')
6 files changed, 227 insertions, 91 deletions
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, + }, }); |