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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/import_entities
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/import_entities')
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue80
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js244
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js111
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql65
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!
+}