diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-07 15:10:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-07 15:10:27 +0300 |
commit | 53f456b167f19877d663ee6ed510673cebee0f91 (patch) | |
tree | fcc0bb52b79c195bf0eda100cc5d7e7a16dc0c0b /app/assets/javascripts/import_entities | |
parent | e8a31d8dc2afd673ca50d74d26edab0a0fec83ca (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/import_entities')
10 files changed, 254 insertions, 62 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..79b3e689442 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> @@ -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 1d36b370457..51333f810a6 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() { @@ -157,21 +185,21 @@ 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.progress.status" /> + <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" @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 facefe316eb..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 @@ -20,6 +20,7 @@ export const clientTypenames = { BulkImportPageInfo: 'ClientBulkImportPageInfo', BulkImportTarget: 'ClientBulkImportTarget', BulkImportProgress: 'ClientBulkImportProgress', + BulkImportValidationError: 'ClientBulkImportValidationError', }; function makeGroup(data) { @@ -106,6 +107,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr return makeGroup({ ...group, + validation_errors: [], progress: { id: jobId ?? localProgressId(group.id), status: cachedImportState?.status ?? STATUSES.NONE, @@ -152,7 +154,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr async setImportProgress(_, { sourceGroupId, status, jobId }) { if (jobId) { - groupsManager.saveImportState(jobId, { status }); + groupsManager.updateImportProgress(jobId, status); } return makeGroup({ @@ -165,7 +167,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr }, async updateImportStatus(_, { id, status }) { - groupsManager.saveImportState(id, { status }); + groupsManager.updateImportProgress(id, status); return { __typename: clientTypenames.BulkImportProgress, @@ -174,39 +176,81 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr }; }, - async importGroup(_, { sourceGroupId }, { client }) { + async addValidationError(_, { sourceGroupId, field, message }, { client }) { const { - data: { bulkImportSourceGroup: group }, + data: { + bulkImportSourceGroup: { validation_errors: validationErrors, ...group }, + }, } = await client.query({ query: bulkImportSourceGroupQuery, variables: { id: sourceGroupId }, }); - const GROUP_BEING_SCHEDULED = makeGroup({ - id: sourceGroupId, - progress: { - id: localProgressId(sourceGroupId), - status: STATUSES.SCHEDULING, + return { + ...group, + validation_errors: [ + ...validationErrors.filter(({ field: f }) => f !== field), + { + __typename: clientTypenames.BulkImportValidationError, + field, + message, + }, + ], + }; + }, + + 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: [ - { - source_type: 'group_entity', - source_full_path: group.full_path, - destination_namespace: group.import_target.target_namespace, - destination_name: group.import_target.new_name, - }, - ], + 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.saveImportState(jobId, { - id: group.id, - importTarget: group.import_target, + groupsManager.createImportState(jobId, { status: STATUSES.CREATED, + groups, }); return { status: STATUSES.CREATED, jobId }; @@ -217,14 +261,16 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr return { status: STATUSES.NONE }; }) .then((newStatus) => - client.mutate({ - mutation: setImportProgressMutation, - variables: { sourceGroupId, ...newStatus }, - }), + sourceGroupIds.forEach((sourceGroupId) => + client.mutate({ + mutation: setImportProgressMutation, + variables: { sourceGroupId, ...newStatus }, + }), + ), ) .catch(() => createFlash({ message: defaultErrorMessage })); - return GROUP_BEING_SCHEDULED; + return GROUPS_BEING_SCHEDULED; }, }, }; 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 ee3add7966c..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 @@ -12,4 +12,8 @@ fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup { target_namespace new_name } + validation_errors { + field + message + } } 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 41d32a1d639..00000000000 --- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql +++ /dev/null @@ -1,9 +0,0 @@ -mutation importGroup($sourceGroupId: String!) { - importGroup(sourceGroupId: $sourceGroupId) @client { - id - progress { - id - status - } - } -} 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/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js index 536f96529d7..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 @@ -13,25 +13,42 @@ 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 {}; } } - saveImportState(importId, group) { - const key = this.getStorageKey(importId); - const oldState = this.importStates[key] ?? {}; + createImportState(importId, jobConfig) { + this.importStates[this.getStorageKey(importId)] = { + status: jobConfig.status, + groups: jobConfig.groups.map((g) => ({ importTarget: g.import_target, id: g.id })), + }; + this.saveImportStatesToStorage(); + } - if (!oldState.id && !group.id) { + updateImportProgress(importId, status) { + const currentState = this.importStates[this.getStorageKey(importId)]; + if (!currentState) { return; } - this.importStates[key] = { - ...oldState, - ...group, - status: group.status, - }; + currentState.status = status; this.saveImportStatesToStorage(); } @@ -39,10 +56,15 @@ export class SourceGroupsManager { const PREFIX = this.getStorageKey(''); 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 { jobId, importState }; + if (!jobId) { + return null; + } + + const group = importState.groups.find((g) => g.id === groupId); + return { jobId, importState: { ...group, status: importState.status } }; } getStorageKey(importId) { diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql index ba042d33893..c830aaa75e6 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql @@ -18,6 +18,11 @@ type ClientBulkImportProgress { status: String! } +type ClientBulkImportValidationError { + field: String! + message: String! +} + type ClientBulkImportSourceGroup { id: ID! web_url: String! @@ -25,6 +30,7 @@ type ClientBulkImportSourceGroup { full_name: String! progress: ClientBulkImportProgress! import_target: ClientBulkImportTarget! + validation_errors: [ClientBulkImportValidationError!]! } type ClientBulkImportPageInfo { @@ -45,9 +51,15 @@ extend type Query { } extend type Mutation { - setNewName(newName: String, sourceGroupId: ID!): ClientTargetNamespace! - setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientTargetNamespace! - importGroup(id: ID!): ClientBulkImportSourceGroup! + 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! } |