Commit 96db744c authored by Illya Klymov's avatar Illya Klymov

Implement separate status for bulk imports

- return separate status for each top level group

Changelog: added
parent e4d749f0
...@@ -314,9 +314,8 @@ export default { ...@@ -314,9 +314,8 @@ export default {
variables: { importRequests }, variables: { importRequests },
}); });
} catch (error) { } catch (error) {
const message = error?.networkError?.response?.data?.error ?? i18n.ERROR_IMPORT;
createFlash({ createFlash({
message, message: i18n.ERROR_IMPORT,
captureError: true, captureError: true,
error, error,
}); });
......
...@@ -32,8 +32,10 @@ export default { ...@@ -32,8 +32,10 @@ export default {
fullPath() { fullPath() {
return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent'); return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
}, },
invalidNameValidationMessage() { validationMessage() {
return getInvalidNameValidationMessage(this.group.importTarget); return (
this.group.progress?.message || getInvalidNameValidationMessage(this.group.importTarget)
);
}, },
}, },
}; };
...@@ -93,10 +95,11 @@ export default { ...@@ -93,10 +95,11 @@ export default {
@input="$emit('update-new-name', $event)" @input="$emit('update-new-name', $event)"
/> />
<p <p
v-if="group.flags.isAvailableForImport && group.flags.isInvalid" v-if="group.flags.isAvailableForImport && (group.flags.isInvalid || validationMessage)"
class="gl-text-red-500 gl-m-0 gl-mt-2" class="gl-text-red-500 gl-m-0 gl-mt-2"
role="alert"
> >
{{ invalidNameValidationMessage }} {{ validationMessage }}
</p> </p>
</div> </div>
</div> </div>
......
...@@ -142,9 +142,7 @@ export function createResolvers({ endpoints }) { ...@@ -142,9 +142,7 @@ export function createResolvers({ endpoints }) {
}; };
}); });
const { const { data: responses } = await axios.post(endpoints.createBulkImport, {
data: { id: jobId },
} = await axios.post(endpoints.createBulkImport, {
bulk_import: importOperations.map((op) => ({ bulk_import: importOperations.map((op) => ({
source_type: 'group_entity', source_type: 'group_entity',
source_full_path: op.group.fullPath, source_full_path: op.group.fullPath,
...@@ -153,15 +151,17 @@ export function createResolvers({ endpoints }) { ...@@ -153,15 +151,17 @@ export function createResolvers({ endpoints }) {
})), })),
}); });
return importOperations.map((op) => { return importOperations.map((op, idx) => {
const response = responses[idx];
const lastImportTarget = { const lastImportTarget = {
targetNamespace: op.targetNamespace, targetNamespace: op.targetNamespace,
newName: op.newName, newName: op.newName,
}; };
const progress = { const progress = {
id: jobId, id: response.id || `local-${Date.now()}-${idx}`,
status: STATUSES.CREATED, status: response.success ? STATUSES.CREATED : STATUSES.FAILED,
message: response.message || null,
}; };
localStorageCache.set(op.group.webUrl, { progress, lastImportTarget }); localStorageCache.set(op.group.webUrl, { progress, lastImportTarget });
......
fragment BulkImportSourceGroupProgress on ClientBulkImportProgress { fragment BulkImportSourceGroupProgress on ClientBulkImportProgress {
id id
status status
message
} }
...@@ -9,6 +9,7 @@ mutation importGroups($importRequests: [ImportGroupInput!]!) { ...@@ -9,6 +9,7 @@ mutation importGroups($importRequests: [ImportGroupInput!]!) {
progress { progress {
id id
status status
message
} }
} }
} }
...@@ -22,7 +22,14 @@ export class LocalStorageCache { ...@@ -22,7 +22,14 @@ export class LocalStorageCache {
loadCacheFromStorage() { loadCacheFromStorage() {
try { try {
return JSON.parse(this.storage.getItem(KEY)) ?? {}; const storage = JSON.parse(this.storage.getItem(KEY)) ?? {};
Object.values(storage).forEach((entry) => {
if (entry.progress && !('message' in entry.progress)) {
// eslint-disable-next-line no-param-reassign
entry.progress.message = '';
}
});
return storage;
} catch { } catch {
return {}; return {};
} }
......
...@@ -16,6 +16,7 @@ type ClientBulkImportSourceGroupConnection { ...@@ -16,6 +16,7 @@ type ClientBulkImportSourceGroupConnection {
type ClientBulkImportProgress { type ClientBulkImportProgress {
id: ID! id: ID!
status: String! status: String!
message: String
} }
type ClientBulkImportValidationError { type ClientBulkImportValidationError {
......
...@@ -40,13 +40,9 @@ class Import::BulkImportsController < ApplicationController ...@@ -40,13 +40,9 @@ class Import::BulkImportsController < ApplicationController
end end
def create def create
response = ::BulkImports::CreateService.new(current_user, create_params, credentials).execute responses = create_params.map { |entry| ::BulkImports::CreateService.new(current_user, [entry], credentials).execute }
if response.success? render json: responses.map { |response| { success: response.success?, id: response.payload[:id], message: response.message } }
render json: response.payload.to_json(only: [:id])
else
render json: { error: response.message }, status: response.http_status
end
end end
def realtime_changes def realtime_changes
......
...@@ -215,9 +215,13 @@ RSpec.describe Import::BulkImportsController do ...@@ -215,9 +215,13 @@ RSpec.describe Import::BulkImportsController do
let(:pat) { "fake-pat" } let(:pat) { "fake-pat" }
let(:bulk_import_params) do let(:bulk_import_params) do
[{ "source_type" => "group_entity", [{ "source_type" => "group_entity",
"source_full_path" => "full_path", "source_full_path" => "full_path",
"destination_name" => "destination_name", "destination_name" => "destination_name",
"destination_namespace" => "root" }] "destination_namespace" => "root" },
{ "source_type" => "group_entity2",
"source_full_path" => "full_path2",
"destination_name" => "destination_name2",
"destination_namespace" => "root" }]
end end
before do before do
...@@ -225,29 +229,23 @@ RSpec.describe Import::BulkImportsController do ...@@ -225,29 +229,23 @@ RSpec.describe Import::BulkImportsController do
session[:bulk_import_gitlab_url] = instance_url session[:bulk_import_gitlab_url] = instance_url
end end
it 'executes BulkImpors::CreatetService' do it 'executes BulkImpors::CreateService' do
error_response = ServiceResponse.error(message: 'Record invalid', http_status: :unprocessable_entity)
expect_next_instance_of( expect_next_instance_of(
::BulkImports::CreateService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service| ::BulkImports::CreateService, user, [bulk_import_params[0]], { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: bulk_import)) allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: bulk_import))
end end
post :create, params: { bulk_import: bulk_import_params }
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq({ id: bulk_import.id }.to_json)
end
it 'returns error when validation fails' do
error_response = ServiceResponse.error(message: 'Record invalid', http_status: :unprocessable_entity)
expect_next_instance_of( expect_next_instance_of(
::BulkImports::CreateService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service| ::BulkImports::CreateService, user, [bulk_import_params[1]], { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(error_response) allow(service).to receive(:execute).and_return(error_response)
end end
post :create, params: { bulk_import: bulk_import_params } post :create, params: { bulk_import: bulk_import_params }
expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq({ error: 'Record invalid' }.to_json) expect(json_response).to eq([{ "success" => true, "id" => bulk_import.id, "message" => nil },
{ "success" => false, "id" => nil, "message" => "Record invalid" }])
end end
end end
end end
......
...@@ -123,13 +123,22 @@ describe('import target cell', () => { ...@@ -123,13 +123,22 @@ describe('import target cell', () => {
}); });
describe('when entity is available for import', () => { describe('when entity is available for import', () => {
const FAKE_PROGRESS_MESSAGE = 'progress message';
beforeEach(() => { beforeEach(() => {
group = generateFakeTableEntry({ id: 1, flags: { isAvailableForImport: true } }); group = generateFakeTableEntry({
id: 1,
flags: { isAvailableForImport: true },
progress: { message: FAKE_PROGRESS_MESSAGE },
});
createComponent({ group }); createComponent({ group });
}); });
it('renders namespace dropdown as enabled', () => { it('renders namespace dropdown as enabled', () => {
expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined); expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
}); });
it('renders progress message as error if it exists', () => {
expect(wrapper.find('[role=alert]').text()).toBe(FAKE_PROGRESS_MESSAGE);
});
}); });
}); });
...@@ -163,12 +163,14 @@ describe('Bulk import resolvers', () => { ...@@ -163,12 +163,14 @@ describe('Bulk import resolvers', () => {
}); });
describe('mutations', () => { describe('mutations', () => {
beforeEach(() => { beforeEach(() => {});
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
});
describe('importGroup', () => { describe('importGroup', () => {
it('sets import status to CREATED when request completes', async () => { it('sets import status to CREATED for successful groups when request completes', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.OK, [{ success: true, id: 1 }]);
await client.mutate({ await client.mutate({
mutation: importGroupsMutation, mutation: importGroupsMutation,
variables: { variables: {
...@@ -185,9 +187,37 @@ describe('Bulk import resolvers', () => { ...@@ -185,9 +187,37 @@ describe('Bulk import resolvers', () => {
await axios.waitForAll(); await axios.waitForAll();
expect(results[0].progress.status).toBe(STATUSES.CREATED); expect(results[0].progress.status).toBe(STATUSES.CREATED);
}); });
it('sets import status to FAILED and sets progress message for failed groups when request completes', async () => {
const FAKE_ERROR_MESSAGE = 'foo';
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.OK, [{ success: false, id: 1, message: FAKE_ERROR_MESSAGE }]);
await client.mutate({
mutation: importGroupsMutation,
variables: {
importRequests: [
{
sourceGroupId: statusEndpointFixture.importable_data[0].id,
newName: 'test',
targetNamespace: 'root',
},
],
},
});
await axios.waitForAll();
expect(results[0].progress.status).toBe(STATUSES.FAILED);
expect(results[0].progress.message).toBe(FAKE_ERROR_MESSAGE);
});
}); });
it('updateImportStatus updates status', async () => { it('updateImportStatus updates status', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.OK, [{ success: true, id: 1 }]);
const NEW_STATUS = 'dummy'; const NEW_STATUS = 'dummy';
await client.mutate({ await client.mutate({
mutation: importGroupsMutation, mutation: importGroupsMutation,
...@@ -216,6 +246,7 @@ describe('Bulk import resolvers', () => { ...@@ -216,6 +246,7 @@ describe('Bulk import resolvers', () => {
expect(statusInResponse).toStrictEqual({ expect(statusInResponse).toStrictEqual({
__typename: clientTypenames.BulkImportProgress, __typename: clientTypenames.BulkImportProgress,
id, id,
message: null,
status: NEW_STATUS, status: NEW_STATUS,
}); });
}); });
......
import { STATUSES } from '~/import_entities/constants'; import { STATUSES } from '~/import_entities/constants';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
export const generateFakeEntry = ({ id, status, ...rest }) => ({ export const generateFakeEntry = ({ id, status, message, ...rest }) => ({
__typename: clientTypenames.BulkImportSourceGroup, __typename: clientTypenames.BulkImportSourceGroup,
webUrl: `https://fake.host/${id}`, webUrl: `https://fake.host/${id}`,
fullPath: `fake_group_${id}`, fullPath: `fake_group_${id}`,
...@@ -18,6 +18,7 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({ ...@@ -18,6 +18,7 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({
: { : {
id, id,
status, status,
message: message || '',
}, },
...rest, ...rest,
}); });
......
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