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 23f4190c2d0..4fcaa1b55fc 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 @@ -1,8 +1,11 @@ import axios from '~/lib/utils/axios_utils'; import createDefaultClient from '~/lib/graphql'; +import { s__ } from '~/locale'; +import createFlash from '~/flash'; import { STATUSES } from '../../constants'; import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; import { SourceGroupsManager } from './services/source_groups_manager'; +import { StatusPoller } from './services/status_poller'; export const clientTypenames = { BulkImportSourceGroup: 'ClientBulkImportSourceGroup', @@ -10,6 +13,8 @@ export const clientTypenames = { }; export function createResolvers({ endpoints }) { + let statusPoller; + return { Query: { async bulkImportSourceGroups(_, __, { client }) { @@ -57,6 +62,30 @@ export function createResolvers({ endpoints }) { const groupManager = new SourceGroupsManager({ client }); const group = groupManager.findById(sourceGroupId); groupManager.setImportStatus(group, STATUSES.SCHEDULING); + try { + 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.setImportStatus(group, STATUSES.STARTED); + if (!statusPoller) { + statusPoller = new StatusPoller({ client, interval: 3000 }); + statusPoller.startPolling(); + } + } catch (e) { + createFlash({ + message: s__('BulkImport|Importing the group failed'), + }); + + groupManager.setImportStatus(group, STATUSES.NONE); + throw e; + } }, }, }; 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 new file mode 100644 index 00000000000..5d2922b0ba8 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -0,0 +1,68 @@ +import gql from 'graphql-tag'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.query.graphql'; +import { STATUSES } from '../../../constants'; +import { SourceGroupsManager } from './source_groups_manager'; + +const groupId = i => `group${i}`; + +function generateGroupsQuery(groups) { + return gql`{ + ${groups + .map( + (g, idx) => + `${groupId(idx)}: group(fullPath: "${g.import_target.target_namespace}/${ + g.import_target.new_name + }") { id }`, + ) + .join('\n')} + }`; +} + +export class StatusPoller { + constructor({ client, interval }) { + this.client = client; + this.interval = interval; + this.timeoutId = null; + this.groupManager = new SourceGroupsManager({ client }); + } + + startPolling() { + if (this.timeoutId) { + return; + } + + this.checkPendingImports(); + } + + stopPolling() { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + + async checkPendingImports() { + try { + const { bulkImportSourceGroups } = this.client.readQuery({ + query: bulkImportSourceGroupsQuery, + }); + const groupsInProgress = bulkImportSourceGroups.filter(g => g.status === STATUSES.STARTED); + if (groupsInProgress.length) { + const { data: results } = await this.client.query({ + query: generateGroupsQuery(groupsInProgress), + fetchPolicy: 'no-cache', + }); + const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)])); + completedGroups.forEach(group => { + this.groupManager.setImportStatus(group, STATUSES.FINISHED); + }); + } + } catch (e) { + createFlash({ + message: s__('BulkImport|Update of import statuses with realtime changes failed'), + }); + } finally { + this.timeoutId = setTimeout(() => this.checkPendingImports(), this.interval); + } + } +} diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js index 618266f7a09..6f5cd7460f8 100644 --- a/app/assets/javascripts/lib/utils/keycodes.js +++ b/app/assets/javascripts/lib/utils/keycodes.js @@ -2,6 +2,7 @@ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/216102 export const BACKSPACE_KEY_CODE = 8; +export const TAB_KEY_CODE = 9; export const ENTER_KEY_CODE = 13; export const ESC_KEY_CODE = 27; export const UP_KEY_CODE = 38; diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql index 149cb256ced..d65d9892260 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql @@ -1,11 +1,11 @@ -#import "~/pipelines/graphql/queries/pipeline_stages.fragment.graphql" +#import "~/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql" query getCiConfigData($content: String!) { ciConfig(content: $content) { errors status stages { - ...PipelineStagesData + ...PipelineStagesConnection } } } diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index b1c52ffa920..8a57c9b1970 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -10,6 +10,7 @@ import TextEditor from './components/text_editor.vue'; import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql'; import getBlobContent from './graphql/queries/blob_content.graphql'; import getCiConfigData from './graphql/queries/ci_config.graphql'; +import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils'; const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; const MR_TARGET_BRANCH = 'merge_request[target_branch]'; @@ -99,7 +100,11 @@ export default { }; }, update(data) { - return data?.ciConfig ?? {}; + const { ciConfigData } = data || {}; + const stageNodes = ciConfigData?.stages?.nodes || []; + const stages = unwrapStagesWithNeeds(stageNodes); + + return { ...ciConfigData, stages }; }, error() { this.reportFailure(LOAD_FAILURE_UNKNOWN); diff --git a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql b/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql deleted file mode 100644 index 0aef2fdfd7f..00000000000 --- a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages.fragment.graphql +++ /dev/null @@ -1,12 +0,0 @@ -fragment PipelineStagesData on CiConfigStage { - name - groups { - name - jobs { - name - needs { - name - } - } - } -} diff --git a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql new file mode 100644 index 00000000000..1da4fa0a72b --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql @@ -0,0 +1,20 @@ +fragment PipelineStagesConnection on CiConfigStageConnection { + nodes { + name + groups { + nodes { + name + jobs { + nodes { + name + needs { + nodes { + name + } + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue index fb61c13983f..1ad0ca36bf8 100644 --- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue +++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue @@ -1,5 +1,5 @@