Commit b18aa67e authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'xanf-simplify-import-entities-graphql' into 'master'

Simplify management of local state in import_groups app

See merge request gitlab-org/gitlab!60615
parents fef65601 9dc3c797
...@@ -72,11 +72,11 @@ export default { ...@@ -72,11 +72,11 @@ export default {
}, },
isAlreadyImported() { isAlreadyImported() {
return this.group.status !== STATUSES.NONE; return this.group.progress.status !== STATUSES.NONE;
}, },
isFinished() { isFinished() {
return this.group.status === STATUSES.FINISHED; return this.group.progress.status === STATUSES.FINISHED;
}, },
fullPath() { fullPath() {
...@@ -165,7 +165,7 @@ export default { ...@@ -165,7 +165,7 @@ export default {
</div> </div>
</td> </td>
<td class="gl-p-4 gl-white-space-nowrap"> <td class="gl-p-4 gl-white-space-nowrap">
<import-status :status="group.status" /> <import-status :status="group.progress.status" />
</td> </td>
<td class="gl-p-4"> <td class="gl-p-4">
<gl-button <gl-button
......
...@@ -4,40 +4,82 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -4,40 +4,82 @@ import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { STATUSES } from '../../constants'; 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 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 { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller'; import { StatusPoller } from './services/status_poller';
import typeDefs from './typedefs.graphql';
export const clientTypenames = { export const clientTypenames = {
BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup', BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
AvailableNamespace: 'ClientAvailableNamespace', AvailableNamespace: 'ClientAvailableNamespace',
BulkImportPageInfo: 'ClientBulkImportPageInfo', BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
}; };
export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) { function makeGroup(data) {
let statusPoller; const result = {
__typename: clientTypenames.BulkImportSourceGroup,
...data,
};
const NESTED_OBJECT_FIELDS = {
import_target: clientTypenames.BulkImportTarget,
progress: clientTypenames.BulkImportProgress,
};
let sourceGroupManager; Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => {
const getGroupsManager = (client) => { if (!data[field]) {
if (!sourceGroupManager) { return;
sourceGroupManager = new GroupsManager({ client, sourceUrl });
} }
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 { return {
Query: { Query: {
async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) {
return client.readFragment({
fragment: bulkImportSourceGroupItemFragment,
fragmentName: 'BulkImportSourceGroupItem',
id: getCacheKey({
__typename: clientTypenames.BulkImportSourceGroup,
id,
}),
});
},
async bulkImportSourceGroups(_, vars, { client }) { async bulkImportSourceGroups(_, vars, { client }) {
if (!statusPoller) { if (!statusPoller) {
statusPoller = new StatusPoller({ statusPoller = new StatusPoller({
groupManager: getGroupsManager(client), updateImportStatus: ({ id, status_name: status }) =>
client.mutate({
mutation: updateImportStatusMutation,
variables: { id, status },
}),
pollPath: endpoints.jobs, pollPath: endpoints.jobs,
}); });
statusPoller.startPolling(); statusPoller.startPolling();
} }
const groupsManager = getGroupsManager(client);
return Promise.all([ return Promise.all([
axios.get(endpoints.status, { axios.get(endpoints.status, {
params: { params: {
...@@ -59,19 +101,20 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ...@@ -59,19 +101,20 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return { return {
__typename: clientTypenames.BulkImportSourceGroupConnection, __typename: clientTypenames.BulkImportSourceGroupConnection,
nodes: data.importable_data.map((group) => { nodes: data.importable_data.map((group) => {
const cachedImportState = groupsManager.getImportStateFromStorageByGroupId( const { jobId, importState: cachedImportState } =
group.id, groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {};
);
return { return makeGroup({
__typename: clientTypenames.BulkImportSourceGroup,
...group, ...group,
progress: {
id: jobId ?? localProgressId(group.id),
status: cachedImportState?.status ?? STATUSES.NONE, status: cachedImportState?.status ?? STATUSES.NONE,
},
import_target: cachedImportState?.importTarget ?? { import_target: cachedImportState?.importTarget ?? {
new_name: group.full_path, new_name: group.full_path,
target_namespace: availableNamespaces[0]?.full_path ?? '', target_namespace: availableNamespaces[0]?.full_path ?? '',
}, },
}; });
}), }),
pageInfo: { pageInfo: {
__typename: clientTypenames.BulkImportPageInfo, __typename: clientTypenames.BulkImportPageInfo,
...@@ -91,26 +134,65 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ...@@ -91,26 +134,65 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
), ),
}, },
Mutation: { Mutation: {
setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) { setTargetNamespace: (_, { targetNamespace, sourceGroupId }) =>
getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { makeGroup({
// eslint-disable-next-line no-param-reassign id: sourceGroupId,
sourceGroup.import_target.target_namespace = targetNamespace; import_target: {
}); target_namespace: targetNamespace,
},
}),
setNewName: (_, { newName, sourceGroupId }) =>
makeGroup({
id: sourceGroupId,
import_target: {
new_name: newName,
}, },
}),
setNewName(_, { newName, sourceGroupId }, { client }) { async setImportProgress(_, { sourceGroupId, status, jobId }) {
getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => { if (jobId) {
// eslint-disable-next-line no-param-reassign groupsManager.saveImportState(jobId, { status });
sourceGroup.import_target.new_name = newName; }
return makeGroup({
id: sourceGroupId,
progress: {
id: jobId ?? localProgressId(sourceGroupId),
status,
},
}); });
}, },
async updateImportStatus(_, { id, status }) {
groupsManager.saveImportState(id, { status });
return {
__typename: clientTypenames.BulkImportProgress,
id,
status,
};
},
async importGroup(_, { sourceGroupId }, { client }) { async importGroup(_, { sourceGroupId }, { client }) {
const groupManager = getGroupsManager(client); const {
const group = groupManager.findById(sourceGroupId); data: { bulkImportSourceGroup: group },
groupManager.setImportStatus(group, STATUSES.SCHEDULING); } = await client.query({
try { query: bulkImportSourceGroupQuery,
const response = await axios.post(endpoints.createBulkImport, { variables: { id: sourceGroupId },
});
const GROUP_BEING_SCHEDULED = makeGroup({
id: sourceGroupId,
progress: {
id: localProgressId(sourceGroupId),
status: STATUSES.SCHEDULING,
},
});
const defaultErrorMessage = s__('BulkImport|Importing the group failed');
axios
.post(endpoints.createBulkImport, {
bulk_import: [ bulk_import: [
{ {
source_type: 'group_entity', source_type: 'group_entity',
...@@ -119,18 +201,38 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr ...@@ -119,18 +201,38 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
destination_name: group.import_target.new_name, destination_name: group.import_target.new_name,
}, },
], ],
})
.then(({ data: { id: jobId } }) => {
groupsManager.saveImportState(jobId, {
id: group.id,
importTarget: group.import_target,
status: STATUSES.CREATED,
}); });
groupManager.startImport({ group, importId: response.data.id });
} catch (e) { return { status: STATUSES.CREATED, jobId };
const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed'); })
.catch((e) => {
const message = e?.response?.data?.error ?? defaultErrorMessage;
createFlash({ message }); createFlash({ message });
groupManager.setImportStatus(group, STATUSES.NONE); return { status: STATUSES.NONE };
throw e; })
} .then((newStatus) =>
client.mutate({
mutation: setImportProgressMutation,
variables: { sourceGroupId, ...newStatus },
}),
)
.catch(() => createFlash({ message: defaultErrorMessage }));
return GROUP_BEING_SCHEDULED;
}, },
}, },
}; };
} }
export const createApolloClient = ({ sourceUrl, endpoints }) => export const createApolloClient = ({ sourceUrl, endpoints }) =>
createDefaultClient(createResolvers({ sourceUrl, endpoints }), { assumeImmutableResults: true }); createDefaultClient(
createResolvers({ sourceUrl, endpoints }),
{ assumeImmutableResults: true },
typeDefs,
);
#import "./bulk_import_source_group_progress.fragment.graphql"
fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup { fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
id id
web_url web_url
full_path full_path
full_name full_name
status progress {
import_target ...BulkImportSourceGroupProgress
}
import_target {
target_namespace
new_name
}
} }
mutation importGroup($sourceGroupId: String!) { mutation importGroup($sourceGroupId: String!) {
importGroup(sourceGroupId: $sourceGroupId) @client importGroup(sourceGroupId: $sourceGroupId) @client {
id
progress {
id
status
}
}
} }
mutation setImportProgress($status: String!, $sourceGroupId: String!, $jobId: String) {
setImportProgress(status: $status, sourceGroupId: $sourceGroupId, jobId: $jobId) @client {
id
progress {
id
status
}
}
}
mutation setNewName($newName: String!, $sourceGroupId: String!) { mutation setNewName($newName: String!, $sourceGroupId: String!) {
setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client {
id
import_target {
new_name
}
}
} }
mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) { mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client {
id
import_target {
target_namespace
}
}
} }
mutation updateImportStatus($status: String!, $id: String!) {
updateImportStatus(status: $status, id: $id) @client {
id
status
}
}
#import "../fragments/bulk_import_source_group_item.fragment.graphql"
query bulkImportSourceGroup($id: ID!) {
bulkImportSourceGroup(id: $id) @client {
...BulkImportSourceGroupItem
}
}
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import produce from 'immer';
import { debounce, merge } from 'lodash'; 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 KEY = 'gl-bulk-imports-import-state';
export const DEBOUNCE_INTERVAL = 200; export const DEBOUNCE_INTERVAL = 200;
export class SourceGroupsManager { export class SourceGroupsManager {
constructor({ client, sourceUrl, storage = window.localStorage }) { constructor({ sourceUrl, storage = window.localStorage }) {
this.client = client;
this.sourceUrl = sourceUrl; this.sourceUrl = sourceUrl;
this.storage = storage; this.storage = storage;
...@@ -35,45 +19,30 @@ export class SourceGroupsManager { ...@@ -35,45 +19,30 @@ export class SourceGroupsManager {
} }
} }
findById(id) { saveImportState(importId, group) {
const cacheId = generateGroupId(id); const key = this.getStorageKey(importId);
return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId }); const oldState = this.importStates[key] ?? {};
}
update(group, fn) {
this.client.writeFragment({
fragment: ImportSourceGroupFragment,
id: generateGroupId(group.id),
data: produce(group, fn),
});
}
updateById(id, fn) { if (!oldState.id && !group.id) {
const group = this.findById(id); return;
this.update(group, fn);
} }
saveImportState(importId, group) { this.importStates[key] = {
this.importStates[this.getStorageKey(importId)] = { ...oldState,
id: group.id, ...group,
importTarget: group.import_target,
status: group.status, status: group.status,
}; };
this.saveImportStatesToStorage(); this.saveImportStatesToStorage();
} }
getImportStateFromStorage(importId) {
return this.importStates[this.getStorageKey(importId)];
}
getImportStateFromStorageByGroupId(groupId) { getImportStateFromStorageByGroupId(groupId) {
const PREFIX = this.getStorageKey(''); const PREFIX = this.getStorageKey('');
const [, importState] = const [jobId, importState] =
Object.entries(this.importStates).find( Object.entries(this.importStates).find(
([key, group]) => key.startsWith(PREFIX) && group.id === groupId, ([key, group]) => key.startsWith(PREFIX) && group.id === groupId,
) ?? []; ) ?? [];
return importState; return { jobId, importState };
} }
getStorageKey(importId) { getStorageKey(importId) {
...@@ -91,34 +60,4 @@ export class SourceGroupsManager { ...@@ -91,34 +60,4 @@ export class SourceGroupsManager {
// empty catch intentional: storage might be unavailable or full // empty catch intentional: storage might be unavailable or full
} }
}, DEBOUNCE_INTERVAL); }, 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();
}
} }
...@@ -5,13 +5,15 @@ import Poll from '~/lib/utils/poll'; ...@@ -5,13 +5,15 @@ import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export class StatusPoller { export class StatusPoller {
constructor({ groupManager, pollPath }) { constructor({ updateImportStatus, pollPath }) {
this.eTagPoll = new Poll({ this.eTagPoll = new Poll({
resource: { resource: {
fetchJobs: () => axios.get(pollPath), fetchJobs: () => axios.get(pollPath),
}, },
method: 'fetchJobs', method: 'fetchJobs',
successCallback: ({ data }) => this.updateImportsStatuses(data), successCallback: ({ data: statuses }) => {
statuses.forEach((status) => updateImportStatus(status));
},
errorCallback: () => errorCallback: () =>
createFlash({ createFlash({
message: s__('BulkImport|Update of import statuses with realtime changes failed'), message: s__('BulkImport|Update of import statuses with realtime changes failed'),
...@@ -25,17 +27,9 @@ export class StatusPoller { ...@@ -25,17 +27,9 @@ export class StatusPoller {
this.eTagPoll.stop(); this.eTagPoll.stop();
} }
}); });
this.groupManager = groupManager;
} }
startPolling() { startPolling() {
this.eTagPoll.makeRequest(); this.eTagPoll.makeRequest();
} }
async updateImportsStatuses(importStatuses) {
importStatuses.forEach(({ id, status_name: statusName }) => {
this.groupManager.setImportStatusByImportId(id, statusName);
});
}
} }
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 ClientBulkImportSourceGroup {
id: ID!
web_url: String!
full_path: String!
full_name: String!
progress: ClientBulkImportProgress!
import_target: ClientBulkImportTarget!
}
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!): ClientTargetNamespace!
setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientTargetNamespace!
importGroup(id: ID!): ClientBulkImportSourceGroup!
setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup!
updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
}
...@@ -19,7 +19,7 @@ const getFakeGroup = (status) => ({ ...@@ -19,7 +19,7 @@ const getFakeGroup = (status) => ({
new_name: 'group1', new_name: 'group1',
}, },
id: 1, id: 1,
status, progress: { status },
}); });
const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group'; const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group';
......
...@@ -9,9 +9,12 @@ import { ...@@ -9,9 +9,12 @@ import {
createResolvers, createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory'; } from '~/import_entities/import_groups/graphql/client_factory';
import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
...@@ -78,6 +81,31 @@ describe('Bulk import resolvers', () => { ...@@ -78,6 +81,31 @@ describe('Bulk import resolvers', () => {
}); });
}); });
describe('bulkImportSourceGroup', () => {
beforeEach(async () => {
axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
axiosMockAdapter
.onGet(FAKE_ENDPOINTS.availableNamespaces)
.reply(httpStatus.OK, availableNamespacesFixture);
return client.query({
query: bulkImportSourceGroupsQuery,
});
});
it('returns group', async () => {
const { id } = statusEndpointFixture.importable_data[0];
const {
data: { bulkImportSourceGroup: group },
} = await client.query({
query: bulkImportSourceGroupQuery,
variables: { id: id.toString() },
});
expect(group).toMatchObject(statusEndpointFixture.importable_data[0]);
});
});
describe('bulkImportSourceGroups', () => { describe('bulkImportSourceGroups', () => {
let results; let results;
...@@ -89,8 +117,12 @@ describe('Bulk import resolvers', () => { ...@@ -89,8 +117,12 @@ describe('Bulk import resolvers', () => {
}); });
it('respects cached import state when provided by group manager', async () => { it('respects cached import state when provided by group manager', async () => {
const FAKE_JOB_ID = '1';
const FAKE_STATUS = 'DEMO_STATUS'; const FAKE_STATUS = 'DEMO_STATUS';
const FAKE_IMPORT_TARGET = {}; const FAKE_IMPORT_TARGET = {
new_name: 'test-name',
target_namespace: 'test-namespace',
};
const TARGET_INDEX = 0; const TARGET_INDEX = 0;
const clientWithMockedManager = createClient({ const clientWithMockedManager = createClient({
...@@ -98,8 +130,11 @@ describe('Bulk import resolvers', () => { ...@@ -98,8 +130,11 @@ describe('Bulk import resolvers', () => {
getImportStateFromStorageByGroupId(groupId) { getImportStateFromStorageByGroupId(groupId) {
if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) { if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
return { return {
jobId: FAKE_JOB_ID,
importState: {
status: FAKE_STATUS, status: FAKE_STATUS,
importTarget: FAKE_IMPORT_TARGET, importTarget: FAKE_IMPORT_TARGET,
},
}; };
} }
...@@ -113,8 +148,8 @@ describe('Bulk import resolvers', () => { ...@@ -113,8 +148,8 @@ describe('Bulk import resolvers', () => {
}); });
const clientResults = clientResponse.data.bulkImportSourceGroups.nodes; const clientResults = clientResponse.data.bulkImportSourceGroups.nodes;
expect(clientResults[TARGET_INDEX].import_target).toBe(FAKE_IMPORT_TARGET); expect(clientResults[TARGET_INDEX].import_target).toStrictEqual(FAKE_IMPORT_TARGET);
expect(clientResults[TARGET_INDEX].status).toBe(FAKE_STATUS); expect(clientResults[TARGET_INDEX].progress.status).toBe(FAKE_STATUS);
}); });
it('populates each result instance with empty import_target when there are no available namespaces', async () => { it('populates each result instance with empty import_target when there are no available namespaces', async () => {
...@@ -143,8 +178,8 @@ describe('Bulk import resolvers', () => { ...@@ -143,8 +178,8 @@ describe('Bulk import resolvers', () => {
).toBe(true); ).toBe(true);
}); });
it('populates each result instance with status field default to none', () => { it('populates each result instance with status default to none', () => {
expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); expect(results.every((r) => r.progress.status === STATUSES.NONE)).toBe(true);
}); });
it('populates each result instance with import_target defaulted to first available namespace', () => { it('populates each result instance with import_target defaulted to first available namespace', () => {
...@@ -183,7 +218,6 @@ describe('Bulk import resolvers', () => { ...@@ -183,7 +218,6 @@ describe('Bulk import resolvers', () => {
}); });
describe('mutations', () => { describe('mutations', () => {
let results;
const GROUP_ID = 1; const GROUP_ID = 1;
beforeEach(() => { beforeEach(() => {
...@@ -195,7 +229,10 @@ describe('Bulk import resolvers', () => { ...@@ -195,7 +229,10 @@ describe('Bulk import resolvers', () => {
{ {
__typename: clientTypenames.BulkImportSourceGroup, __typename: clientTypenames.BulkImportSourceGroup,
id: GROUP_ID, id: GROUP_ID,
progress: {
id: `test-${GROUP_ID}`,
status: STATUSES.NONE, status: STATUSES.NONE,
},
web_url: 'https://fake.host/1', web_url: 'https://fake.host/1',
full_path: 'fake_group_1', full_path: 'fake_group_1',
full_name: 'fake_name_1', full_name: 'fake_name_1',
...@@ -214,35 +251,42 @@ describe('Bulk import resolvers', () => { ...@@ -214,35 +251,42 @@ describe('Bulk import resolvers', () => {
}, },
}, },
}); });
client
.watchQuery({
query: bulkImportSourceGroupsQuery,
fetchPolicy: 'cache-only',
})
.subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
}); });
it('setTargetNamespaces updates group target namespace', async () => { it('setTargetNamespaces updates group target namespace', async () => {
const NEW_TARGET_NAMESPACE = 'target'; const NEW_TARGET_NAMESPACE = 'target';
await client.mutate({ const {
data: {
setTargetNamespace: {
id: idInResponse,
import_target: { target_namespace: namespaceInResponse },
},
},
} = await client.mutate({
mutation: setTargetNamespaceMutation, mutation: setTargetNamespaceMutation,
variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE }, variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE },
}); });
expect(results[0].import_target.target_namespace).toBe(NEW_TARGET_NAMESPACE); expect(idInResponse).toBe(GROUP_ID);
expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
}); });
it('setNewName updates group target name', async () => { it('setNewName updates group target name', async () => {
const NEW_NAME = 'new'; const NEW_NAME = 'new';
await client.mutate({ const {
data: {
setNewName: {
id: idInResponse,
import_target: { new_name: nameInResponse },
},
},
} = await client.mutate({
mutation: setNewNameMutation, mutation: setNewNameMutation,
variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME }, variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME },
}); });
expect(results[0].import_target.new_name).toBe(NEW_NAME); expect(idInResponse).toBe(GROUP_ID);
expect(nameInResponse).toBe(NEW_NAME);
}); });
describe('importGroup', () => { describe('importGroup', () => {
...@@ -261,7 +305,21 @@ describe('Bulk import resolvers', () => { ...@@ -261,7 +305,21 @@ describe('Bulk import resolvers', () => {
query: bulkImportSourceGroupsQuery, query: bulkImportSourceGroupsQuery,
}); });
expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING); expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING);
});
describe('when request completes', () => {
let results;
beforeEach(() => {
client
.watchQuery({
query: bulkImportSourceGroupsQuery,
fetchPolicy: 'cache-only',
})
.subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
}); });
it('sets import status to CREATED when request completes', async () => { it('sets import status to CREATED when request completes', async () => {
...@@ -270,8 +328,9 @@ describe('Bulk import resolvers', () => { ...@@ -270,8 +328,9 @@ describe('Bulk import resolvers', () => {
mutation: importGroupMutation, mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID }, variables: { sourceGroupId: GROUP_ID },
}); });
await waitForPromises();
expect(results[0].status).toBe(STATUSES.CREATED); expect(results[0].progress.status).toBe(STATUSES.CREATED);
}); });
it('resets status to NONE if request fails', async () => { it('resets status to NONE if request fails', async () => {
...@@ -287,7 +346,8 @@ describe('Bulk import resolvers', () => { ...@@ -287,7 +346,8 @@ describe('Bulk import resolvers', () => {
.catch(() => {}); .catch(() => {});
await waitForPromises(); await waitForPromises();
expect(results[0].status).toBe(STATUSES.NONE); expect(results[0].progress.status).toBe(STATUSES.NONE);
});
}); });
it('shows default error message when server error is not provided', async () => { it('shows default error message when server error is not provided', async () => {
...@@ -324,5 +384,39 @@ describe('Bulk import resolvers', () => { ...@@ -324,5 +384,39 @@ describe('Bulk import resolvers', () => {
expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE }); expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
}); });
}); });
it('setImportProgress updates group progress', async () => {
const NEW_STATUS = 'dummy';
const FAKE_JOB_ID = 5;
const {
data: {
setImportProgress: { progress },
},
} = await client.mutate({
mutation: setImportProgressMutation,
variables: { sourceGroupId: GROUP_ID, status: NEW_STATUS, jobId: FAKE_JOB_ID },
});
expect(progress).toMatchObject({
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
});
it('updateImportStatus returns new status', async () => {
const NEW_STATUS = 'dummy';
const FAKE_JOB_ID = 5;
const {
data: { updateImportStatus: statusInResponse },
} = await client.mutate({
mutation: updateImportStatusMutation,
variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
});
expect(statusInResponse).toMatchObject({
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
});
}); });
}); });
...@@ -10,7 +10,10 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({ ...@@ -10,7 +10,10 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({
new_name: `group${id}`, new_name: `group${id}`,
}, },
id, id,
progress: {
id: `test-${id}`,
status, status,
},
...rest, ...rest,
}); });
......
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
import { import {
KEY, KEY,
SourceGroupsManager, SourceGroupsManager,
...@@ -10,42 +7,29 @@ const FAKE_SOURCE_URL = 'http://demo.host'; ...@@ -10,42 +7,29 @@ const FAKE_SOURCE_URL = 'http://demo.host';
describe('SourceGroupsManager', () => { describe('SourceGroupsManager', () => {
let manager; let manager;
let client;
let storage; let storage;
const getFakeGroup = () => ({
__typename: clientTypenames.BulkImportSourceGroup,
id: 5,
});
beforeEach(() => { beforeEach(() => {
client = {
readFragment: jest.fn(),
writeFragment: jest.fn(),
};
storage = { storage = {
getItem: jest.fn(), getItem: jest.fn(),
setItem: jest.fn(), setItem: jest.fn(),
}; };
manager = new SourceGroupsManager({ client, storage, sourceUrl: FAKE_SOURCE_URL }); manager = new SourceGroupsManager({ storage, sourceUrl: FAKE_SOURCE_URL });
}); });
describe('storage management', () => { describe('storage management', () => {
const IMPORT_ID = 1; const IMPORT_ID = 1;
const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' }; const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' };
const STATUS = 'FAKE_STATUS'; const STATUS = 'FAKE_STATUS';
const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS }; const FAKE_GROUP = { id: 1, importTarget: IMPORT_TARGET, status: STATUS };
it('loads state from storage on creation', () => { it('loads state from storage on creation', () => {
expect(storage.getItem).toHaveBeenCalledWith(KEY); expect(storage.getItem).toHaveBeenCalledWith(KEY);
}); });
it('saves to storage when import is starting', () => { it('saves to storage when saveImportState is called', () => {
manager.startImport({ manager.saveImportState(IMPORT_ID, FAKE_GROUP);
importId: IMPORT_ID,
group: FAKE_GROUP,
});
const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]); const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]);
expect(Object.values(storedObject)[0]).toStrictEqual({ expect(Object.values(storedObject)[0]).toStrictEqual({
id: FAKE_GROUP.id, id: FAKE_GROUP.id,
...@@ -54,15 +38,12 @@ describe('SourceGroupsManager', () => { ...@@ -54,15 +38,12 @@ describe('SourceGroupsManager', () => {
}); });
}); });
it('saves to storage when import status is updated', () => { it('updates storage when previous state is available', () => {
const CHANGED_STATUS = 'changed'; const CHANGED_STATUS = 'changed';
manager.startImport({ manager.saveImportState(IMPORT_ID, FAKE_GROUP);
importId: IMPORT_ID,
group: FAKE_GROUP,
});
manager.setImportStatusByImportId(IMPORT_ID, CHANGED_STATUS); manager.saveImportState(IMPORT_ID, { status: CHANGED_STATUS });
const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]); const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]);
expect(Object.values(storedObject)[0]).toStrictEqual({ expect(Object.values(storedObject)[0]).toStrictEqual({
id: FAKE_GROUP.id, id: FAKE_GROUP.id,
...@@ -71,63 +52,4 @@ describe('SourceGroupsManager', () => { ...@@ -71,63 +52,4 @@ describe('SourceGroupsManager', () => {
}); });
}); });
}); });
it('finds item by group id', () => {
const ID = 5;
const FAKE_GROUP = getFakeGroup();
client.readFragment.mockReturnValue(FAKE_GROUP);
const group = manager.findById(ID);
expect(group).toBe(FAKE_GROUP);
expect(client.readFragment).toHaveBeenCalledWith({
fragment: ImportSourceGroupFragment,
id: defaultDataIdFromObject(getFakeGroup()),
});
});
it('updates group with provided function', () => {
const UPDATED_GROUP = {};
const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
manager.update(getFakeGroup(), fn);
expect(client.writeFragment).toHaveBeenCalledWith({
fragment: ImportSourceGroupFragment,
id: defaultDataIdFromObject(getFakeGroup()),
data: UPDATED_GROUP,
});
});
it('updates group by id with provided function', () => {
const UPDATED_GROUP = {};
const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
client.readFragment.mockReturnValue(getFakeGroup());
manager.updateById(getFakeGroup().id, fn);
expect(client.readFragment).toHaveBeenCalledWith({
fragment: ImportSourceGroupFragment,
id: defaultDataIdFromObject(getFakeGroup()),
});
expect(client.writeFragment).toHaveBeenCalledWith({
fragment: ImportSourceGroupFragment,
id: defaultDataIdFromObject(getFakeGroup()),
data: UPDATED_GROUP,
});
});
it('sets import status when group is provided', () => {
client.readFragment.mockReturnValue(getFakeGroup());
const NEW_STATUS = 'NEW_STATUS';
manager.setImportStatus(getFakeGroup(), NEW_STATUS);
expect(client.writeFragment).toHaveBeenCalledWith({
fragment: ImportSourceGroupFragment,
id: defaultDataIdFromObject(getFakeGroup()),
data: {
...getFakeGroup(),
status: NEW_STATUS,
},
});
});
}); });
...@@ -21,17 +21,15 @@ const FAKE_POLL_PATH = '/fake/poll/path'; ...@@ -21,17 +21,15 @@ const FAKE_POLL_PATH = '/fake/poll/path';
describe('Bulk import status poller', () => { describe('Bulk import status poller', () => {
let poller; let poller;
let mockAdapter; let mockAdapter;
let groupManager; let updateImportStatus;
const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH); const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH);
beforeEach(() => { beforeEach(() => {
mockAdapter = new MockAdapter(axios); mockAdapter = new MockAdapter(axios);
mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {}); mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
groupManager = { updateImportStatus = jest.fn();
setImportStatusByImportId: jest.fn(), poller = new StatusPoller({ updateImportStatus, pollPath: FAKE_POLL_PATH });
};
poller = new StatusPoller({ groupManager, pollPath: FAKE_POLL_PATH });
}); });
it('creates poller with proper config', () => { it('creates poller with proper config', () => {
...@@ -96,9 +94,9 @@ describe('Bulk import status poller', () => { ...@@ -96,9 +94,9 @@ describe('Bulk import status poller', () => {
it('when success response arrives updates relevant group status', () => { it('when success response arrives updates relevant group status', () => {
const FAKE_ID = 5; const FAKE_ID = 5;
const [[pollConfig]] = Poll.mock.calls; const [[pollConfig]] = Poll.mock.calls;
const FAKE_RESPONSE = { id: FAKE_ID, status_name: STATUSES.FINISHED };
pollConfig.successCallback({ data: [FAKE_RESPONSE] });
pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] }); expect(updateImportStatus).toHaveBeenCalledWith(FAKE_RESPONSE);
expect(groupManager.setImportStatusByImportId).toHaveBeenCalledWith(FAKE_ID, STATUSES.FINISHED);
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment