diff options
Diffstat (limited to 'app/assets/javascripts/import_entities')
17 files changed, 495 insertions, 158 deletions
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index f337520b0db..3daa5eebcb6 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -1,5 +1,6 @@ <script> import { + GlButton, GlEmptyState, GlDropdown, GlDropdownItem, @@ -8,10 +9,13 @@ import { GlLoadingIcon, GlSearchBoxByClick, GlSprintf, + GlSafeHtmlDirective as SafeHtml, + GlTooltip, } from '@gitlab/ui'; -import { s__, __ } from '~/locale'; +import { s__, __, n__ } from '~/locale'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; -import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql'; +import { STATUSES } from '../../constants'; +import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql'; import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql'; import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql'; import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; @@ -23,6 +27,7 @@ const DEFAULT_PAGE_SIZE = PAGE_SIZES[0]; export default { components: { + GlButton, GlEmptyState, GlDropdown, GlDropdownItem, @@ -31,9 +36,13 @@ export default { GlLoadingIcon, GlSearchBoxByClick, GlSprintf, + GlTooltip, ImportTableRow, PaginationLinks, }, + directives: { + SafeHtml, + }, props: { sourceUrl: { @@ -65,12 +74,28 @@ export default { }, computed: { + groups() { + return this.bulkImportSourceGroups?.nodes ?? []; + }, + + hasGroupsWithValidationError() { + return this.groups.some((g) => g.validation_errors.length); + }, + + availableGroupsForImport() { + return this.groups.filter((g) => g.progress.status === STATUSES.NONE); + }, + + isImportAllButtonDisabled() { + return this.hasGroupsWithValidationError || this.availableGroupsForImport.length === 0; + }, + humanizedTotal() { return this.paginationInfo.total >= 1000 ? __('1000+') : this.paginationInfo.total; }, hasGroups() { - return this.bulkImportSourceGroups?.nodes?.length > 0; + return this.groups.length > 0; }, hasEmptyFilter() { @@ -105,6 +130,10 @@ export default { }, methods: { + groupsCount(count) { + return n__('%d group', '%d groups', count); + }, + setPage(page) { this.page = page; }, @@ -123,24 +152,57 @@ export default { }); }, - importGroup(sourceGroupId) { + importGroups(sourceGroupIds) { this.$apollo.mutate({ - mutation: importGroupMutation, - variables: { sourceGroupId }, + mutation: importGroupsMutation, + variables: { sourceGroupIds }, }); }, + importAllGroups() { + this.importGroups(this.availableGroupsForImport.map((g) => g.id)); + }, + setPageSize(size) { this.perPage = size; }, }, + gitlabLogo: window.gon.gitlab_logo, PAGE_SIZES, }; </script> <template> <div> + <h1 + class="gl-my-0 gl-py-4 gl-font-size-h1 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex" + > + <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" /> + {{ s__('BulkImport|Import groups from GitLab') }} + <div ref="importAllButtonWrapper" class="gl-ml-auto"> + <gl-button + v-if="!$apollo.loading && hasGroups" + :disabled="isImportAllButtonDisabled" + variant="confirm" + @click="importAllGroups" + > + <gl-sprintf :message="s__('BulkImport|Import %{groups}')"> + <template #groups> + {{ groupsCount(availableGroupsForImport.length) }} + </template> + </gl-sprintf> + </gl-button> + </div> + <gl-tooltip v-if="isImportAllButtonDisabled" :target="() => $refs.importAllButtonWrapper"> + <template v-if="hasGroupsWithValidationError"> + {{ s__('BulkImport|One or more groups has validation errors') }} + </template> + <template v-else> + {{ s__('BulkImport|No groups on this page are available for import') }} + </template> + </gl-tooltip> + </h1> <div class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex" > @@ -153,7 +215,7 @@ export default { <strong>{{ paginationInfo.end }}</strong> </template> <template #total> - <strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong> + <strong>{{ groupsCount(paginationInfo.total) }}</strong> </template> <template #filter> <strong>{{ filter }}</strong> @@ -180,7 +242,7 @@ export default { :description="s__('Check your source instance permissions.')" /> <template v-else> - <table class="gl-w-full"> + <table class="gl-w-full" data-qa-selector="import_table"> <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"> <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th> <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th> @@ -196,7 +258,7 @@ export default { :group-path-regex="groupPathRegex" @update-target-namespace="updateTargetNamespace(group.id, $event)" @update-new-name="updateNewName(group.id, $event)" - @import-group="importGroup(group.id)" + @import-group="importGroups([group.id])" /> </template> </tbody> diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index aed879e75fb..60cd5bb0a96 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -10,8 +10,11 @@ import { GlFormInput, } from '@gitlab/ui'; import { joinPaths } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; import ImportStatus from '../../components/import_status.vue'; import { STATUSES } from '../../constants'; +import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql'; +import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql'; import groupQuery from '../graphql/queries/group.query.graphql'; const DEBOUNCE_INTERVAL = 300; @@ -52,6 +55,27 @@ export default { fullPath: this.fullPath, }; }, + update({ existingGroup }) { + const variables = { + field: 'new_name', + sourceGroupId: this.group.id, + }; + + if (!existingGroup) { + this.$apollo.mutate({ + mutation: removeValidationErrorMutation, + variables, + }); + } else { + this.$apollo.mutate({ + mutation: addValidationErrorMutation, + variables: { + ...variables, + message: s__('BulkImport|Name already exists.'), + }, + }); + } + }, skip() { return !this.isNameValid || this.isAlreadyImported; }, @@ -63,8 +87,12 @@ export default { return this.group.import_target; }, + invalidNameValidationMessage() { + return this.group.validation_errors.find(({ field }) => field === 'new_name')?.message; + }, + isInvalid() { - return Boolean(!this.isNameValid || this.existingGroup); + return Boolean(!this.isNameValid || this.invalidNameValidationMessage); }, isNameValid() { @@ -72,11 +100,11 @@ export default { }, isAlreadyImported() { - return this.group.status !== STATUSES.NONE; + return this.group.progress.status !== STATUSES.NONE; }, isFinished() { - return this.group.status === STATUSES.FINISHED; + return this.group.progress.status === STATUSES.FINISHED; }, fullPath() { @@ -91,7 +119,11 @@ export default { </script> <template> - <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"> + <tr + class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid" + data-qa-selector="import_item" + :data-qa-source-group="group.full_path" + > <td class="gl-p-4"> <gl-link :href="group.web_url" @@ -122,6 +154,7 @@ export default { :disabled="isAlreadyImported" toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!" class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1" + data-qa-selector="target_namespace_selector_dropdown" > <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{ s__('BulkImport|No parent') @@ -134,6 +167,8 @@ export default { <gl-dropdown-item v-for="ns in availableNamespaces" :key="ns.full_path" + data-qa-selector="target_group_dropdown_item" + :data-qa-group-name="ns.full_path" @click="$emit('update-target-namespace', ns.full_path)" > {{ ns.full_path }} @@ -157,22 +192,23 @@ export default { <template v-if="!isNameValid"> {{ __('Please choose a group URL with no special characters.') }} </template> - <template v-else-if="existingGroup"> - {{ s__('BulkImport|Name already exists.') }} + <template v-else-if="invalidNameValidationMessage"> + {{ invalidNameValidationMessage }} </template> </p> </div> </div> </td> - <td class="gl-p-4 gl-white-space-nowrap"> - <import-status :status="group.status" /> + <td class="gl-p-4 gl-white-space-nowrap" data-qa-selector="import_status_indicator"> + <import-status :status="group.progress.status" class="gl-mt-2" /> </td> <td class="gl-p-4"> <gl-button v-if="!isAlreadyImported" :disabled="isInvalid" - variant="success" + variant="confirm" category="secondary" + data-qa-selector="import_group_button" @click="$emit('import-group')" >{{ __('Import') }}</gl-button > diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index d444cc77aa7..2cde3781a6a 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -4,40 +4,83 @@ import axios from '~/lib/utils/axios_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { STATUSES } from '../../constants'; +import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql'; +import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql'; +import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; +import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql'; import { SourceGroupsManager } from './services/source_groups_manager'; import { StatusPoller } from './services/status_poller'; +import typeDefs from './typedefs.graphql'; export const clientTypenames = { BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroup: 'ClientBulkImportSourceGroup', AvailableNamespace: 'ClientAvailableNamespace', BulkImportPageInfo: 'ClientBulkImportPageInfo', + BulkImportTarget: 'ClientBulkImportTarget', + BulkImportProgress: 'ClientBulkImportProgress', + BulkImportValidationError: 'ClientBulkImportValidationError', }; -export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { - let statusPoller; +function makeGroup(data) { + const result = { + __typename: clientTypenames.BulkImportSourceGroup, + ...data, + }; + const NESTED_OBJECT_FIELDS = { + import_target: clientTypenames.BulkImportTarget, + progress: clientTypenames.BulkImportProgress, + }; - let sourceGroupManager; - const getGroupsManager = (client) => { - if (!sourceGroupManager) { - sourceGroupManager = new GroupsManager({ client, sourceUrl }); + Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => { + if (!data[field]) { + return; } - return sourceGroupManager; - }; + result[field] = { + __typename: type, + ...data[field], + }; + }); + + return result; +} + +const localProgressId = (id) => `not-started-${id}`; + +export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { + const groupsManager = new GroupsManager({ + sourceUrl, + }); + + let statusPoller; return { Query: { + async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) { + return client.readFragment({ + fragment: bulkImportSourceGroupItemFragment, + fragmentName: 'BulkImportSourceGroupItem', + id: getCacheKey({ + __typename: clientTypenames.BulkImportSourceGroup, + id, + }), + }); + }, + async bulkImportSourceGroups(_, vars, { client }) { if (!statusPoller) { statusPoller = new StatusPoller({ - groupManager: getGroupsManager(client), + updateImportStatus: ({ id, status_name: status }) => + client.mutate({ + mutation: updateImportStatusMutation, + variables: { id, status }, + }), pollPath: endpoints.jobs, }); statusPoller.startPolling(); } - const groupsManager = getGroupsManager(client); return Promise.all([ axios.get(endpoints.status, { params: { @@ -59,19 +102,21 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr return { __typename: clientTypenames.BulkImportSourceGroupConnection, nodes: data.importable_data.map((group) => { - const cachedImportState = groupsManager.getImportStateFromStorageByGroupId( - group.id, - ); + const { jobId, importState: cachedImportState } = + groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {}; - return { - __typename: clientTypenames.BulkImportSourceGroup, + return makeGroup({ ...group, - status: cachedImportState?.status ?? STATUSES.NONE, + validation_errors: [], + progress: { + id: jobId ?? localProgressId(group.id), + status: cachedImportState?.status ?? STATUSES.NONE, + }, import_target: cachedImportState?.importTarget ?? { new_name: group.full_path, target_namespace: availableNamespaces[0]?.full_path ?? '', }, - }; + }); }), pageInfo: { __typename: clientTypenames.BulkImportPageInfo, @@ -91,46 +136,149 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ), }, Mutation: { - setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) { - getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.import_target.target_namespace = targetNamespace; + setTargetNamespace: (_, { targetNamespace, sourceGroupId }) => + makeGroup({ + id: sourceGroupId, + import_target: { + target_namespace: targetNamespace, + }, + }), + + setNewName: (_, { newName, sourceGroupId }) => + makeGroup({ + id: sourceGroupId, + import_target: { + new_name: newName, + }, + }), + + async setImportProgress(_, { sourceGroupId, status, jobId }) { + if (jobId) { + groupsManager.updateImportProgress(jobId, status); + } + + return makeGroup({ + id: sourceGroupId, + progress: { + id: jobId ?? localProgressId(sourceGroupId), + status, + }, }); }, - setNewName(_, { newName, sourceGroupId }, { client }) { - getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.import_target.new_name = newName; + async updateImportStatus(_, { id, status }) { + groupsManager.updateImportProgress(id, status); + + return { + __typename: clientTypenames.BulkImportProgress, + id, + status, + }; + }, + + async addValidationError(_, { sourceGroupId, field, message }, { client }) { + const { + data: { + bulkImportSourceGroup: { validation_errors: validationErrors, ...group }, + }, + } = await client.query({ + query: bulkImportSourceGroupQuery, + variables: { id: sourceGroupId }, }); + + return { + ...group, + validation_errors: [ + ...validationErrors.filter(({ field: f }) => f !== field), + { + __typename: clientTypenames.BulkImportValidationError, + field, + message, + }, + ], + }; }, - async importGroup(_, { sourceGroupId }, { client }) { - const groupManager = getGroupsManager(client); - const group = groupManager.findById(sourceGroupId); - groupManager.setImportStatus(group, STATUSES.SCHEDULING); - try { - const response = await axios.post(endpoints.createBulkImport, { - bulk_import: [ - { - source_type: 'group_entity', - source_full_path: group.full_path, - destination_namespace: group.import_target.target_namespace, - destination_name: group.import_target.new_name, - }, - ], - }); - groupManager.startImport({ group, importId: response.data.id }); - } catch (e) { - const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed'); - createFlash({ message }); - groupManager.setImportStatus(group, STATUSES.NONE); - throw e; - } + async removeValidationError(_, { sourceGroupId, field }, { client }) { + const { + data: { + bulkImportSourceGroup: { validation_errors: validationErrors, ...group }, + }, + } = await client.query({ + query: bulkImportSourceGroupQuery, + variables: { id: sourceGroupId }, + }); + + return { + ...group, + validation_errors: validationErrors.filter(({ field: f }) => f !== field), + }; + }, + + async importGroups(_, { sourceGroupIds }, { client }) { + const groups = await Promise.all( + sourceGroupIds.map((id) => + client + .query({ + query: bulkImportSourceGroupQuery, + variables: { id }, + }) + .then(({ data }) => data.bulkImportSourceGroup), + ), + ); + + const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) => + makeGroup({ + id: sourceGroupId, + progress: { + id: localProgressId(sourceGroupId), + status: STATUSES.SCHEDULING, + }, + }), + ); + + const defaultErrorMessage = s__('BulkImport|Importing the group failed'); + axios + .post(endpoints.createBulkImport, { + bulk_import: groups.map((group) => ({ + source_type: 'group_entity', + source_full_path: group.full_path, + destination_namespace: group.import_target.target_namespace, + destination_name: group.import_target.new_name, + })), + }) + .then(({ data: { id: jobId } }) => { + groupsManager.createImportState(jobId, { + status: STATUSES.CREATED, + groups, + }); + + return { status: STATUSES.CREATED, jobId }; + }) + .catch((e) => { + const message = e?.response?.data?.error ?? defaultErrorMessage; + createFlash({ message }); + return { status: STATUSES.NONE }; + }) + .then((newStatus) => + sourceGroupIds.forEach((sourceGroupId) => + client.mutate({ + mutation: setImportProgressMutation, + variables: { sourceGroupId, ...newStatus }, + }), + ), + ) + .catch(() => createFlash({ message: defaultErrorMessage })); + + return GROUPS_BEING_SCHEDULED; }, }, }; } export const createApolloClient = ({ sourceUrl, endpoints }) => - createDefaultClient(createResolvers({ sourceUrl, endpoints }), { assumeImmutableResults: true }); + createDefaultClient( + createResolvers({ sourceUrl, endpoints }), + { assumeImmutableResults: true }, + typeDefs, + ); diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql index 50774e36599..47675cd1bd0 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql @@ -1,8 +1,19 @@ +#import "./bulk_import_source_group_progress.fragment.graphql" + fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup { id web_url full_path full_name - status - import_target + progress { + ...BulkImportSourceGroupProgress + } + import_target { + target_namespace + new_name + } + validation_errors { + field + message + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql new file mode 100644 index 00000000000..2d60bf82d65 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql @@ -0,0 +1,4 @@ +fragment BulkImportSourceGroupProgress on ClientBulkImportProgress { + id + status +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql new file mode 100644 index 00000000000..d95c460c046 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql @@ -0,0 +1,9 @@ +mutation addValidationError($sourceGroupId: String!, $field: String!, $message: String!) { + addValidationError(sourceGroupId: $sourceGroupId, field: $field, message: $message) @client { + id + validation_errors { + field + message + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql deleted file mode 100644 index 412608d3faf..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql +++ /dev/null @@ -1,3 +0,0 @@ -mutation importGroup($sourceGroupId: String!) { - importGroup(sourceGroupId: $sourceGroupId) @client -} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql new file mode 100644 index 00000000000..d8e46329e38 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql @@ -0,0 +1,9 @@ +mutation importGroups($sourceGroupIds: [String!]!) { + importGroups(sourceGroupIds: $sourceGroupIds) @client { + id + progress { + id + status + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql new file mode 100644 index 00000000000..940bf4dfaac --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql @@ -0,0 +1,9 @@ +mutation removeValidationError($sourceGroupId: String!, $field: String!) { + removeValidationError(sourceGroupId: $sourceGroupId, field: $field) @client { + id + validation_errors { + field + message + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql new file mode 100644 index 00000000000..2ec1269932a --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql @@ -0,0 +1,9 @@ +mutation setImportProgress($status: String!, $sourceGroupId: String!, $jobId: String) { + setImportProgress(status: $status, sourceGroupId: $sourceGroupId, jobId: $jobId) @client { + id + progress { + id + status + } + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql index 2bc19891401..354bf2a5815 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql @@ -1,3 +1,8 @@ mutation setNewName($newName: String!, $sourceGroupId: String!) { - setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client + setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client { + id + import_target { + new_name + } + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql index fc98a1652c1..a0ef407f135 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql @@ -1,3 +1,8 @@ mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) { - setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client + setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client { + id + import_target { + target_namespace + } + } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql new file mode 100644 index 00000000000..8c0233b2939 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql @@ -0,0 +1,6 @@ +mutation updateImportStatus($status: String!, $id: String!) { + updateImportStatus(status: $status, id: $id) @client { + id + status + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql new file mode 100644 index 00000000000..0aff23af96d --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql @@ -0,0 +1,7 @@ +#import "../fragments/bulk_import_source_group_item.fragment.graphql" + +query bulkImportSourceGroup($id: ID!) { + bulkImportSourceGroup(id: $id) @client { + ...BulkImportSourceGroupItem + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js index 2c88d25358f..97dbdbf518a 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js @@ -1,26 +1,10 @@ -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import produce from 'immer'; import { debounce, merge } from 'lodash'; -import { STATUSES } from '../../../constants'; -import ImportSourceGroupFragment from '../fragments/bulk_import_source_group_item.fragment.graphql'; - -function extractTypeConditionFromFragment(fragment) { - return fragment.definitions[0]?.typeCondition.name.value; -} - -function generateGroupId(id) { - return defaultDataIdFromObject({ - __typename: extractTypeConditionFromFragment(ImportSourceGroupFragment), - id, - }); -} export const KEY = 'gl-bulk-imports-import-state'; export const DEBOUNCE_INTERVAL = 200; export class SourceGroupsManager { - constructor({ client, sourceUrl, storage = window.localStorage }) { - this.client = client; + constructor({ sourceUrl, storage = window.localStorage }) { this.sourceUrl = sourceUrl; this.storage = storage; @@ -29,51 +13,58 @@ export class SourceGroupsManager { loadImportStatesFromStorage() { try { - return JSON.parse(this.storage.getItem(KEY)) ?? {}; + return Object.fromEntries( + Object.entries(JSON.parse(this.storage.getItem(KEY)) ?? {}).map(([jobId, config]) => { + // new format of storage + if (config.groups) { + return [jobId, config]; + } + + return [ + jobId, + { + status: config.status, + groups: [{ id: config.id, importTarget: config.importTarget }], + }, + ]; + }), + ); } catch { return {}; } } - findById(id) { - const cacheId = generateGroupId(id); - return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId }); - } - - update(group, fn) { - this.client.writeFragment({ - fragment: ImportSourceGroupFragment, - id: generateGroupId(group.id), - data: produce(group, fn), - }); - } - - updateById(id, fn) { - const group = this.findById(id); - this.update(group, fn); - } - - saveImportState(importId, group) { + createImportState(importId, jobConfig) { this.importStates[this.getStorageKey(importId)] = { - id: group.id, - importTarget: group.import_target, - status: group.status, + status: jobConfig.status, + groups: jobConfig.groups.map((g) => ({ importTarget: g.import_target, id: g.id })), }; this.saveImportStatesToStorage(); } - getImportStateFromStorage(importId) { - return this.importStates[this.getStorageKey(importId)]; + updateImportProgress(importId, status) { + const currentState = this.importStates[this.getStorageKey(importId)]; + if (!currentState) { + return; + } + + currentState.status = status; + this.saveImportStatesToStorage(); } getImportStateFromStorageByGroupId(groupId) { const PREFIX = this.getStorageKey(''); - const [, importState] = + const [jobId, importState] = Object.entries(this.importStates).find( - ([key, group]) => key.startsWith(PREFIX) && group.id === groupId, + ([key, state]) => key.startsWith(PREFIX) && state.groups.some((g) => g.id === groupId), ) ?? []; - return importState; + if (!jobId) { + return null; + } + + const group = importState.groups.find((g) => g.id === groupId); + return { jobId, importState: { ...group, status: importState.status } }; } getStorageKey(importId) { @@ -91,34 +82,4 @@ export class SourceGroupsManager { // empty catch intentional: storage might be unavailable or full } }, DEBOUNCE_INTERVAL); - - startImport({ group, importId }) { - this.setImportStatus(group, STATUSES.CREATED); - this.saveImportState(importId, group); - } - - setImportStatus(group, status) { - this.update(group, (sourceGroup) => { - // eslint-disable-next-line no-param-reassign - sourceGroup.status = status; - }); - } - - setImportStatusByImportId(importId, status) { - const importState = this.getImportStateFromStorage(importId); - if (!importState) { - return; - } - - if (importState.status !== status) { - importState.status = status; - } - - const group = this.findById(importState.id); - if (group?.id) { - this.setImportStatus(group, status); - } - - this.saveImportStatesToStorage(); - } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js index b80a575afce..0297b3d3428 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -5,13 +5,15 @@ import Poll from '~/lib/utils/poll'; import { s__ } from '~/locale'; export class StatusPoller { - constructor({ groupManager, pollPath }) { + constructor({ updateImportStatus, pollPath }) { this.eTagPoll = new Poll({ resource: { fetchJobs: () => axios.get(pollPath), }, method: 'fetchJobs', - successCallback: ({ data }) => this.updateImportsStatuses(data), + successCallback: ({ data: statuses }) => { + statuses.forEach((status) => updateImportStatus(status)); + }, errorCallback: () => createFlash({ message: s__('BulkImport|Update of import statuses with realtime changes failed'), @@ -25,17 +27,9 @@ export class StatusPoller { this.eTagPoll.stop(); } }); - - this.groupManager = groupManager; } startPolling() { this.eTagPoll.makeRequest(); } - - async updateImportsStatuses(importStatuses) { - importStatuses.forEach(({ id, status_name: statusName }) => { - this.groupManager.setImportStatusByImportId(id, statusName); - }); - } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql new file mode 100644 index 00000000000..c830aaa75e6 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql @@ -0,0 +1,65 @@ +type ClientBulkImportAvailableNamespace { + id: ID! + full_path: String! +} + +type ClientBulkImportTarget { + target_namespace: String! + new_name: String! +} + +type ClientBulkImportSourceGroupConnection { + nodes: [ClientBulkImportSourceGroup!]! + pageInfo: ClientBulkImportPageInfo! +} + +type ClientBulkImportProgress { + id: ID + status: String! +} + +type ClientBulkImportValidationError { + field: String! + message: String! +} + +type ClientBulkImportSourceGroup { + id: ID! + web_url: String! + full_path: String! + full_name: String! + progress: ClientBulkImportProgress! + import_target: ClientBulkImportTarget! + validation_errors: [ClientBulkImportValidationError!]! +} + +type ClientBulkImportPageInfo { + page: Int! + perPage: Int! + total: Int! + totalPages: Int! +} + +extend type Query { + bulkImportSourceGroup(id: ID!): ClientBulkImportSourceGroup + bulkImportSourceGroups( + page: Int! + perPage: Int! + filter: String! + ): ClientBulkImportSourceGroupConnection! + availableNamespaces: [ClientBulkImportAvailableNamespace!]! +} + +extend type Mutation { + setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup! + setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup! + importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]! + setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup! + updateImportProgress(id: ID, status: String!): ClientBulkImportProgress + addValidationError( + sourceGroupId: ID! + field: String! + message: String! + ): ClientBulkImportSourceGroup! + removeValidationError(sourceGroupId: ID!, field: String!): ClientBulkImportSourceGroup! +} |