Commit a72caacd authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '342580-corpus-upload-commit-corpus' into 'master'

Implement corpus upload  - Commit Corpus

See merge request gitlab-org/gitlab!73893
parents b54b04ac 712188d9
...@@ -15,3 +15,4 @@ export const TYPE_USER = 'User'; ...@@ -15,3 +15,4 @@ export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability'; export const TYPE_VULNERABILITY = 'Vulnerability';
export const TYPE_NOTE = 'Note'; export const TYPE_NOTE = 'Note';
export const TYPE_DISCUSSION = 'Discussion'; export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
...@@ -74,7 +74,11 @@ export default { ...@@ -74,7 +74,11 @@ export default {
addCorpus() { addCorpus() {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: addCorpusMutation, mutation: addCorpusMutation,
variables: { name: this.$options.i18n.newCorpus, projectPath: this.projectFullPath }, variables: {
name: this.$options.i18n.newCorpus,
projectPath: this.projectFullPath,
packageId: this.states.uploadState.uploadedPackageId,
},
}); });
}, },
resetCorpus() { resetCorpus() {
......
...@@ -30,7 +30,7 @@ export default { ...@@ -30,7 +30,7 @@ export default {
corpusName: s__('CorpusManagement|Corpus name'), corpusName: s__('CorpusManagement|Corpus name'),
uploadButtonText: __('Choose File...'), uploadButtonText: __('Choose File...'),
uploadMessage: s__( uploadMessage: s__(
'CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 10GB', 'CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB',
), ),
}, },
data() { data() {
...@@ -170,7 +170,9 @@ export default { ...@@ -170,7 +170,9 @@ export default {
<div v-if="isUploading" data-testid="upload-status" class="gl-mt-2"> <div v-if="isUploading" data-testid="upload-status" class="gl-mt-2">
<gl-loading-icon inline size="sm" /> <gl-loading-icon inline size="sm" />
{{ progressText }} {{ progressText }}
<gl-button size="small" @click="cancelUpload"> {{ __('Cancel') }} </gl-button> <gl-button size="small" data-testid="cancel-upload" @click="cancelUpload">
{{ __('Cancel') }}
</gl-button>
</div> </div>
</gl-form> </gl-form>
</template> </template>
mutation addCorpus($projectPath: ID!, $name: String!) { mutation addCorpus($projectPath: ID!, $name: String!, $packageId: Int!) {
addCorpus(projectPath: $projectPath, name: $name) @client { addCorpus(projectPath: $projectPath, name: $name, packageId: $packageId) @client {
errors errors
} }
} }
mutation CorpusCreate($input: CorpusCreateInput!) {
corpusCreate(input: $input) {
errors
}
}
mutation uploadComplete($projectPath: ID!, $packageId: Int!) {
uploadComplete(projectPath: $projectPath, packageId: $packageId) @client {
errors
}
}
...@@ -7,5 +7,6 @@ query getCorpuses($projectPath: ID!) { ...@@ -7,5 +7,6 @@ query getCorpuses($projectPath: ID!) {
isUploading isUploading
progress progress
cancelSource cancelSource
uploadedPackageId
} }
} }
...@@ -2,8 +2,12 @@ import produce from 'immer'; ...@@ -2,8 +2,12 @@ import produce from 'immer';
import { corpuses } from 'ee_jest/security_configuration/corpus_management/mock_data'; import { corpuses } from 'ee_jest/security_configuration/corpus_management/mock_data';
import { publishPackage } from '~/api/packages_api'; import { publishPackage } from '~/api/packages_api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
import getCorpusesQuery from '../queries/get_corpuses.query.graphql'; import getCorpusesQuery from '../queries/get_corpuses.query.graphql';
import updateProgress from '../mutations/update_progress.mutation.graphql'; import updateProgress from '../mutations/update_progress.mutation.graphql';
import uploadComplete from '../mutations/upload_complete.mutation.graphql';
import corpusCreate from '../mutations/corpus_create.mutation.graphql';
export default { export default {
Query: { Query: {
...@@ -22,12 +26,13 @@ export default { ...@@ -22,12 +26,13 @@ export default {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
cancelSource: null, cancelSource: null,
uploadedPackageId: null,
__typename: 'UploadState', __typename: 'UploadState',
}; };
}, },
}, },
Mutation: { Mutation: {
addCorpus: (_, { name, projectPath }, { cache }) => { addCorpus: (_, { name, projectPath, packageId }, { cache, client }) => {
const sourceData = cache.readQuery({ const sourceData = cache.readQuery({
query: getCorpusesQuery, query: getCorpusesQuery,
variables: { projectPath }, variables: { projectPath },
...@@ -54,6 +59,16 @@ export default { ...@@ -54,6 +59,16 @@ export default {
}); });
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
client.mutate({
mutation: corpusCreate,
variables: {
input: {
fullPath: projectPath,
packageId: convertToGraphQLId(TYPE_PACKAGES_PACKAGE, packageId),
},
},
});
}, },
deleteCorpus: (_, { name, projectPath }, { cache }) => { deleteCorpus: (_, { name, projectPath }, { cache }) => {
const sourceData = cache.readQuery({ const sourceData = cache.readQuery({
...@@ -91,19 +106,43 @@ export default { ...@@ -91,19 +106,43 @@ export default {
variables: { projectPath }, variables: { projectPath },
}); });
const data = produce(sourceData, (draftState) => { const targetData = produce(sourceData, (draftState) => {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = true; uploadState.isUploading = true;
uploadState.cancelSource = source; uploadState.cancelSource = source;
}); });
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); cache.writeQuery({ query: getCorpusesQuery, data: targetData, variables: { projectPath } });
publishPackage( publishPackage(
{ projectPath, name, version: 0, fileName: name, files }, { projectPath, name, version: 0, fileName: name, files },
{ status: 'hidden', select: 'package_file' }, { status: 'hidden', select: 'package_file' },
{ onUploadProgress, cancelToken: source.token }, { onUploadProgress, cancelToken: source.token },
); )
.then(({ data }) => {
client.mutate({
mutation: uploadComplete,
variables: { projectPath, packageId: data.package_id },
});
})
.catch((e) => {
/* TODO: Error handling */
});
},
uploadComplete: (_, { projectPath, packageId }, { cache }) => {
const sourceData = cache.readQuery({
query: getCorpusesQuery,
variables: { projectPath },
});
const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState;
uploadState.isUploading = false;
uploadState.cancelSource = null;
uploadState.uploadedPackageId = packageId;
});
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
}, },
updateProgress: (_, { projectPath, progress }, { cache }) => { updateProgress: (_, { projectPath, progress }, { cache }) => {
const sourceData = cache.readQuery({ const sourceData = cache.readQuery({
...@@ -115,11 +154,6 @@ export default { ...@@ -115,11 +154,6 @@ export default {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = true; uploadState.isUploading = true;
uploadState.progress = progress; uploadState.progress = progress;
if (progress >= 100) {
uploadState.isUploading = false;
uploadState.cancelSource = null;
}
}); });
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
......
...@@ -18,6 +18,7 @@ exports[`Corpus upload modal corpus modal uploading state does show the upload p ...@@ -18,6 +18,7 @@ exports[`Corpus upload modal corpus modal uploading state does show the upload p
<button <button
class="btn btn-default btn-sm gl-button" class="btn btn-default btn-sm gl-button"
data-testid="cancel-upload"
type="button" type="button"
> >
<!----> <!---->
...@@ -27,7 +28,9 @@ exports[`Corpus upload modal corpus modal uploading state does show the upload p ...@@ -27,7 +28,9 @@ exports[`Corpus upload modal corpus modal uploading state does show the upload p
<span <span
class="gl-button-text" class="gl-button-text"
> >
Cancel Cancel
</span> </span>
</button> </button>
</div> </div>
......
import { createLocalVue, mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue'; import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
const TEST_PROJECT_FULL_PATH = '/namespace/project'; const TEST_PROJECT_FULL_PATH = '/namespace/project';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Corpus upload modal', () => { describe('Corpus upload modal', () => {
let wrapper; let wrapper;
...@@ -15,12 +10,12 @@ describe('Corpus upload modal', () => { ...@@ -15,12 +10,12 @@ describe('Corpus upload modal', () => {
const findUploadAttachment = () => wrapper.find('[data-testid="upload-attachment-button"]'); const findUploadAttachment = () => wrapper.find('[data-testid="upload-attachment-button"]');
const findUploadCorpus = () => wrapper.find('[data-testid="upload-corpus"]'); const findUploadCorpus = () => wrapper.find('[data-testid="upload-corpus"]');
const findUploadStatus = () => wrapper.find('[data-testid="upload-status"]'); const findUploadStatus = () => wrapper.find('[data-testid="upload-status"]');
const findFileInput = () => wrapper.find({ ref: 'fileUpload' });
const findCancelButton = () => wrapper.find('[data-testid="cancel-upload"]');
const createComponent = (propsData, options = {}) => { const createComponent = (propsData, options = {}) => {
wrapper = mount(CorpusUploadForm, { wrapper = mount(CorpusUploadForm, {
localVue,
propsData, propsData,
apolloProvider: createMockApollo(),
provide: { provide: {
projectFullPath: TEST_PROJECT_FULL_PATH, projectFullPath: TEST_PROJECT_FULL_PATH,
}, },
...@@ -46,10 +41,6 @@ describe('Corpus upload modal', () => { ...@@ -46,10 +41,6 @@ describe('Corpus upload modal', () => {
const props = { const props = {
states: { states: {
mockedPackages: {
totalSize: 0,
data: [],
},
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
...@@ -75,6 +66,15 @@ describe('Corpus upload modal', () => { ...@@ -75,6 +66,15 @@ describe('Corpus upload modal', () => {
it('does not show the upload progress', () => { it('does not show the upload progress', () => {
expect(findUploadStatus().exists()).toBe(false); expect(findUploadStatus().exists()).toBe(false);
}); });
describe('selecting a file', () => {
it('transitions to selected state', async () => {
jest.spyOn(wrapper.vm, 'onFileUploadChange').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findFileInput().trigger('change');
expect(wrapper.vm.onFileUploadChange).toHaveBeenCalled();
});
});
}); });
describe('file selected state', () => { describe('file selected state', () => {
...@@ -87,16 +87,11 @@ describe('Corpus upload modal', () => { ...@@ -87,16 +87,11 @@ describe('Corpus upload modal', () => {
attachmentName, attachmentName,
corpusName, corpusName,
files: [attachmentName], files: [attachmentName],
uploadTimeout: null,
}; };
}; };
const props = { const props = {
states: { states: {
mockedPackages: {
totalSize: 0,
data: [],
},
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
...@@ -122,6 +117,15 @@ describe('Corpus upload modal', () => { ...@@ -122,6 +117,15 @@ describe('Corpus upload modal', () => {
it('does not show the upload progress', () => { it('does not show the upload progress', () => {
expect(findUploadStatus().exists()).toBe(false); expect(findUploadStatus().exists()).toBe(false);
}); });
describe('clicking upload file', () => {
it('begins the file upload', async () => {
jest.spyOn(wrapper.vm, 'beginFileUpload').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findUploadCorpus().trigger('click');
expect(wrapper.vm.beginFileUpload).toHaveBeenCalled();
});
});
}); });
describe('uploading state', () => { describe('uploading state', () => {
...@@ -134,16 +138,11 @@ describe('Corpus upload modal', () => { ...@@ -134,16 +138,11 @@ describe('Corpus upload modal', () => {
attachmentName, attachmentName,
corpusName, corpusName,
files: [attachmentName], files: [attachmentName],
uploadTimeout: null,
}; };
}; };
const props = { const props = {
states: { states: {
mockedPackages: {
totalSize: 0,
data: [],
},
uploadState: { uploadState: {
isUploading: true, isUploading: true,
progress: 25, progress: 25,
...@@ -171,6 +170,13 @@ describe('Corpus upload modal', () => { ...@@ -171,6 +170,13 @@ describe('Corpus upload modal', () => {
expect(findUploadStatus().exists()).toBe(true); expect(findUploadStatus().exists()).toBe(true);
expect(findUploadStatus().element).toMatchSnapshot(); expect(findUploadStatus().element).toMatchSnapshot();
}); });
describe('clicking cancel button', () => {
it('emits the reset corpus event', () => {
findCancelButton().trigger('click');
expect(wrapper.emitted().resetCorpus).toBeTruthy();
});
});
}); });
describe('file uploaded state', () => { describe('file uploaded state', () => {
...@@ -183,16 +189,11 @@ describe('Corpus upload modal', () => { ...@@ -183,16 +189,11 @@ describe('Corpus upload modal', () => {
attachmentName, attachmentName,
corpusName, corpusName,
files: [attachmentName], files: [attachmentName],
uploadTimeout: null,
}; };
}; };
const props = { const props = {
states: { states: {
mockedPackages: {
totalSize: 0,
data: [],
},
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 100, progress: 100,
......
import { GlButton } from '@gitlab/ui'; import { GlButton, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue'; import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
const TEST_PROJECT_FULL_PATH = '/namespace/project'; const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('Corpus Upload', () => { describe('Corpus Upload', () => {
let wrapper; let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm);
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { totalSize: 4e8 }; const defaultProps = { totalSize: 4e8 };
wrapper = mountFn(CorpusUpload, { wrapper = mountFn(CorpusUpload, {
...@@ -37,5 +41,43 @@ describe('Corpus Upload', () => { ...@@ -37,5 +41,43 @@ describe('Corpus Upload', () => {
expect(wrapper.findComponent(GlButton).exists()).toBe(true); expect(wrapper.findComponent(GlButton).exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('addCorpus mutation', () => {
it('gets called when the add button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'addCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('primary');
expect(wrapper.vm.addCorpus).toHaveBeenCalled();
});
});
describe('resetCorpus mutation', () => {
it('gets called when the cancel button is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findModal().vm.$emit('canceled');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
it('gets called when the upload form triggers a reset', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'resetCorpus').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('resetCorpus');
expect(wrapper.vm.resetCorpus).toHaveBeenCalled();
});
});
describe('uploadCorpus mutation', () => {
it('gets called when the upload file is clicked from the modal', async () => {
createComponent();
jest.spyOn(wrapper.vm, 'beginFileUpload').mockImplementation(() => {});
await wrapper.vm.$forceUpdate();
findCorpusUploadForm().vm.$emit('beginFileUpload');
expect(wrapper.vm.beginFileUpload).toHaveBeenCalled();
});
});
}); });
}); });
...@@ -9563,7 +9563,7 @@ msgstr "" ...@@ -9563,7 +9563,7 @@ msgstr ""
msgid "CorpusManagement|Latest Job:" msgid "CorpusManagement|Latest Job:"
msgstr "" msgstr ""
msgid "CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 10GB" msgid "CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB"
msgstr "" msgstr ""
msgid "CorpusManagement|New upload" msgid "CorpusManagement|New upload"
......
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