summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/import_entities/import_groups
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/import_entities/import_groups')
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue78
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue106
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js95
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql8
-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/set_new_name.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js45
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js68
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js31
12 files changed, 453 insertions, 0 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
new file mode 100644
index 00000000000..153c58b556e
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
+import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
+import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
+import ImportTableRow from './import_table_row.vue';
+
+const mapApolloMutations = mutations =>
+ Object.fromEntries(
+ Object.entries(mutations).map(([key, mutation]) => [
+ key,
+ function mutate(config) {
+ return this.$apollo.mutate({
+ mutation,
+ ...config,
+ });
+ },
+ ]),
+ );
+
+export default {
+ components: {
+ GlLoadingIcon,
+ ImportTableRow,
+ },
+
+ apollo: {
+ bulkImportSourceGroups: bulkImportSourceGroupsQuery,
+ availableNamespaces: availableNamespacesQuery,
+ },
+
+ methods: {
+ ...mapApolloMutations({
+ setTargetNamespace: setTargetNamespaceMutation,
+ setNewName: setNewNameMutation,
+ importGroup: importGroupMutation,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <div v-else-if="bulkImportSourceGroups.length">
+ <table class="gl-w-full">
+ <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>
+ <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="gl-py-4 import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="group in bulkImportSourceGroups">
+ <import-table-row
+ :key="group.id"
+ :group="group"
+ :available-namespaces="availableNamespaces"
+ @update-target-namespace="
+ setTargetNamespace({
+ variables: { sourceGroupId: group.id, targetNamespace: $event },
+ })
+ "
+ @update-new-name="
+ setNewName({
+ variables: { sourceGroupId: group.id, newName: $event },
+ })
+ "
+ @import-group="importGroup({ variables: { sourceGroupId: group.id } })"
+ />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..07603d89f0f
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlButton, GlIcon, GlLink, GlFormInput } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import ImportStatus from '../../components/import_status.vue';
+import { STATUSES } from '../../constants';
+
+export default {
+ components: {
+ Select2Select,
+ ImportStatus,
+ GlButton,
+ GlLink,
+ GlIcon,
+ GlFormInput,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ availableNamespaces: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ isDisabled() {
+ return this.group.status !== STATUSES.NONE;
+ },
+
+ isFinished() {
+ return this.group.status === STATUSES.FINISHED;
+ },
+
+ select2Options() {
+ return {
+ data: this.availableNamespaces.map(namespace => ({
+ id: namespace.full_path,
+ text: namespace.full_path,
+ })),
+ };
+ },
+ },
+ methods: {
+ getPath(group) {
+ return `${group.import_target.target_namespace}/${group.import_target.new_name}`;
+ },
+
+ getFullPath(group) {
+ return joinPaths(gon.relative_url_root || '/', this.getPath(group));
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1">
+ <td class="gl-p-4">
+ <gl-link :href="group.web_url" target="_blank">
+ {{ group.full_path }} <gl-icon name="external-link" />
+ </gl-link>
+ </td>
+ <td class="gl-p-4">
+ <gl-link v-if="isFinished" :href="getFullPath(group)">{{ getPath(group) }}</gl-link>
+
+ <div
+ v-else
+ class="import-entities-target-select gl-display-flex gl-align-items-stretch"
+ :class="{
+ disabled: isDisabled,
+ }"
+ >
+ <select2-select
+ :disabled="isDisabled"
+ :options="select2Options"
+ :value="group.import_target.target_namespace"
+ @input="$emit('update-target-namespace', $event)"
+ />
+ <div
+ class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
+ >
+ /
+ </div>
+ <gl-form-input
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ :disabled="isDisabled"
+ :value="group.import_target.new_name"
+ @input="$emit('update-new-name', $event)"
+ />
+ </div>
+ </td>
+ <td class="gl-p-4 gl-white-space-nowrap">
+ <import-status :status="group.status" />
+ </td>
+ <td class="gl-p-4">
+ <gl-button
+ v-if="!isDisabled"
+ variant="success"
+ category="secondary"
+ @click="$emit('import-group')"
+ >{{ __('Import') }}</gl-button
+ >
+ </td>
+ </tr>
+</template>
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
new file mode 100644
index 00000000000..4fcaa1b55fc
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -0,0 +1,95 @@
+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',
+ AvailableNamespace: 'ClientAvailableNamespace',
+};
+
+export function createResolvers({ endpoints }) {
+ let statusPoller;
+
+ return {
+ Query: {
+ async bulkImportSourceGroups(_, __, { client }) {
+ const {
+ data: { availableNamespaces },
+ } = await client.query({ query: availableNamespacesQuery });
+
+ return axios.get(endpoints.status).then(({ data }) => {
+ return data.importable_data.map(group => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...group,
+ status: STATUSES.NONE,
+ import_target: {
+ new_name: group.full_path,
+ target_namespace: availableNamespaces[0].full_path,
+ },
+ }));
+ });
+ },
+
+ availableNamespaces: () =>
+ axios.get(endpoints.availableNamespaces).then(({ data }) =>
+ data.map(namespace => ({
+ __typename: clientTypenames.AvailableNamespace,
+ ...namespace,
+ })),
+ ),
+ },
+ Mutation: {
+ setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) {
+ new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.import_target.target_namespace = targetNamespace;
+ });
+ },
+
+ setNewName(_, { newName, sourceGroupId }, { client }) {
+ new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.import_target.new_name = newName;
+ });
+ },
+
+ async importGroup(_, { sourceGroupId }, { client }) {
+ 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;
+ }
+ },
+ },
+ };
+}
+
+export const createApolloClient = ({ endpoints }) =>
+ createDefaultClient(createResolvers({ endpoints }), { assumeImmutableResults: true });
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
new file mode 100644
index 00000000000..50774e36599
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -0,0 +1,8 @@
+fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
+ id
+ web_url
+ full_path
+ full_name
+ status
+ import_target
+}
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
new file mode 100644
index 00000000000..412608d3faf
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
@@ -0,0 +1,3 @@
+mutation importGroup($sourceGroupId: String!) {
+ importGroup(sourceGroupId: $sourceGroupId) @client
+}
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
new file mode 100644
index 00000000000..2bc19891401
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setNewName($newName: String!, $sourceGroupId: String!) {
+ setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client
+}
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
new file mode 100644
index 00000000000..fc98a1652c1
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
+ setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
new file mode 100644
index 00000000000..5ab9796b50a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
@@ -0,0 +1,6 @@
+query availableNamespaces {
+ availableNamespaces @client {
+ id
+ full_path
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
new file mode 100644
index 00000000000..8d52d94925c
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/bulk_import_source_group_item.fragment.graphql"
+
+query bulkImportSourceGroups {
+ bulkImportSourceGroups @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
new file mode 100644
index 00000000000..f752ecc8cd6
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -0,0 +1,45 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import produce from 'immer';
+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 class SourceGroupsManager {
+ constructor({ client }) {
+ this.client = client;
+ }
+
+ 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);
+ }
+
+ setImportStatus(group, status) {
+ this.update(group, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.status = status;
+ });
+ }
+}
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/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
new file mode 100644
index 00000000000..bf427075564
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import Translate from '~/vue_shared/translate';
+import { createApolloClient } from './graphql/client_factory';
+import ImportTable from './components/import_table.vue';
+
+Vue.use(Translate);
+Vue.use(VueApollo);
+
+export function mountImportGroupsApp(mountElement) {
+ if (!mountElement) return undefined;
+
+ const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset;
+ const apolloProvider = new VueApollo({
+ defaultClient: createApolloClient({
+ endpoints: {
+ status: statusPath,
+ availableNamespaces: availableNamespacesPath,
+ createBulkImport: createBulkImportPath,
+ },
+ }),
+ });
+
+ return new Vue({
+ el: mountElement,
+ apolloProvider,
+ render(createElement) {
+ return createElement(ImportTable);
+ },
+ });
+}