Commit 97c1d0d8 authored by Peter Hegman's avatar Peter Hegman

Merge branch '342433-corpus-upload-progress' into 'master'

Corpus upload modal with graphQL and upload progress

See merge request gitlab-org/gitlab!73460
parents f526246b c36a6ad1
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const PUBLISH_PACKAGE_PATH =
'/api/:version/projects/:id/packages/generic/:package_name/:package_version/:file_name';
export function publishPackage(
{ projectPath, name, version, fileName, files },
options,
axiosOptions = {},
) {
const url = buildApiUrl(PUBLISH_PACKAGE_PATH)
.replace(':id', encodeURIComponent(projectPath))
.replace(':package_name', name)
.replace(':package_version', version)
.replace(':file_name', fileName);
const defaults = {
status: 'default',
};
const formData = new FormData();
formData.append('file', files[0]);
return axios.put(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
params: Object.assign(defaults, options),
...axiosOptions,
});
}
......@@ -4,6 +4,7 @@ import { decimalBytes } from '~/lib/utils/unit_format';
import { s__, __ } from '~/locale';
import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql';
import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql';
import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql';
import getCorpusesQuery from '../graphql/queries/get_corpuses.query.graphql';
import CorpusUploadForm from './corpus_upload_form.vue';
......@@ -79,7 +80,13 @@ export default {
resetCorpus() {
this.$apollo.mutate({
mutation: resetCorpus,
variables: { name: '', projectPath: this.projectFullPath },
variables: { projectPath: this.projectFullPath },
});
},
beginFileUpload({ name, files }) {
this.$apollo.mutate({
mutation: uploadCorpus,
variables: { name, projectPath: this.projectFullPath, files },
});
},
},
......@@ -110,7 +117,11 @@ export default {
@primary="addCorpus"
@canceled="resetCorpus"
>
<corpus-upload-form :states="states" />
<corpus-upload-form
:states="states"
@beginFileUpload="beginFileUpload"
@resetCorpus="resetCorpus"
/>
</gl-modal>
</div>
</template>
......@@ -9,8 +9,6 @@ import {
} from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { VALID_CORPUS_MIMETYPE } from '../constants';
import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql';
import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql';
export default {
components: {
......@@ -40,7 +38,6 @@ export default {
attachmentName: '',
corpusName: '',
files: [],
uploadTimeout: null,
};
},
computed: {
......@@ -86,29 +83,13 @@ export default {
this.files = [];
},
cancelUpload() {
clearTimeout(this.uploadTimeout);
this.$apollo.mutate({
mutation: resetCorpus,
variables: { name: this.corpusName, projectPath: this.projectFullPath },
});
this.$emit('resetCorpus');
},
openFileUpload() {
this.$refs.fileUpload.click();
},
beginFileUpload() {
// Simulate incrementing file upload progress
return this.$apollo
.mutate({
mutation: uploadCorpus,
variables: { name: this.corpusName, projectPath: this.projectFullPath },
})
.then(({ data }) => {
if (data.uploadCorpus < 100) {
this.uploadTimeout = setTimeout(() => {
this.beginFileUpload();
}, 500);
}
});
this.$emit('beginFileUpload', { name: this.corpusName, files: this.files });
},
onFileUploadChange(e) {
this.attachmentName = e.target.files[0].name;
......
mutation resetCorpus($projectPath: ID!, $name: String!) {
resetCorpus(projectPath: $projectPath, name: $name) @client {
mutation resetCorpus($projectPath: ID!) {
resetCorpus(projectPath: $projectPath) @client {
errors
}
}
mutation updateProgress($projectPath: ID!, $progress: Int!) {
updateProgress(projectPath: $projectPath, progress: $progress) @client {
errors
}
}
mutation uploadCorpus($projectPath: ID!, $name: String!) {
uploadCorpus(projectPath: $projectPath, name: $name) @client {
mutation uploadCorpus($projectPath: ID!, $name: String!, $files: [Upload!]!) {
uploadCorpus(projectPath: $projectPath, name: $name, files: $files) @client {
errors
}
}
......@@ -6,5 +6,6 @@ query getCorpuses($projectPath: ID!) {
uploadState(projectPath: $projectPath) @client {
isUploading
progress
cancelSource
}
}
import produce from 'immer';
import { corpuses } from 'ee_jest/security_configuration/corpus_management/mock_data';
import { publishPackage } from '~/api/packages_api';
import axios from '~/lib/utils/axios_utils';
import getCorpusesQuery from '../queries/get_corpuses.query.graphql';
import updateProgress from '../mutations/update_progress.mutation.graphql';
export default {
Query: {
......@@ -18,6 +21,7 @@ export default {
return {
isUploading: false,
progress: 0,
cancelSource: null,
__typename: 'UploadState',
};
},
......@@ -71,7 +75,37 @@ export default {
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
},
uploadCorpus: (_, { name, projectPath }, { cache }) => {
uploadCorpus: (_, { projectPath, name, files }, { cache, client }) => {
const onUploadProgress = (e) => {
client.mutate({
mutation: updateProgress,
variables: { projectPath, progress: Math.round((e.loaded / e.total) * 100) },
});
};
const { CancelToken } = axios;
const source = CancelToken.source();
const sourceData = cache.readQuery({
query: getCorpusesQuery,
variables: { projectPath },
});
const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState;
uploadState.isUploading = true;
uploadState.cancelSource = source;
});
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
publishPackage(
{ projectPath, name, version: 0, fileName: name, files },
{ status: 'hidden', select: 'package_file' },
{ onUploadProgress, cancelToken: source.token },
);
},
updateProgress: (_, { projectPath, progress }, { cache }) => {
const sourceData = cache.readQuery({
query: getCorpusesQuery,
variables: { projectPath },
......@@ -80,27 +114,29 @@ export default {
const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState;
uploadState.isUploading = true;
// Simulate incrementing file upload progress
uploadState.progress += 10;
uploadState.progress = progress;
if (uploadState.progress >= 100) {
if (progress >= 100) {
uploadState.isUploading = false;
uploadState.cancelSource = null;
}
});
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
return data.uploadState.progress;
},
resetCorpus: (_, { name, projectPath }, { cache }) => {
resetCorpus: (_, { projectPath }, { cache }) => {
const sourceData = cache.readQuery({
query: getCorpusesQuery,
variables: { projectPath },
});
sourceData.uploadState.cancelSource?.cancel();
const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState;
uploadState.isUploading = false;
uploadState.progress = 0;
uploadState.cancelToken = null;
});
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
......
import MockAdapter from 'axios-mock-adapter';
import { publishPackage } from '~/api/packages_api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
describe('Api', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
};
let originalGon;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
originalGon = window.gon;
window.gon = { ...dummyGon };
});
afterEach(() => {
mock.restore();
window.gon = originalGon;
});
describe('packages', () => {
const projectPath = 'project_a';
const name = 'foo';
const packageVersion = '0';
const apiResponse = [{ id: 1, name: 'foo' }];
describe('publishPackage', () => {
it('publishes the package', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/packages/generic/${name}/${packageVersion}/${name}`;
jest.spyOn(axios, 'put');
mock.onPut(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return publishPackage(
{ projectPath, name, version: 0, fileName: name, files: [{}] },
{ status: 'hidden', select: 'package_file' },
).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.put).toHaveBeenCalledWith(expectedUrl, expect.any(FormData), {
headers: { 'Content-Type': 'multipart/form-data' },
params: { select: 'package_file', status: 'hidden' },
});
});
});
});
});
});
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