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'; ...@@ -4,6 +4,7 @@ import { decimalBytes } from '~/lib/utils/unit_format';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql'; import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql';
import resetCorpus from '../graphql/mutations/reset_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 getCorpusesQuery from '../graphql/queries/get_corpuses.query.graphql';
import CorpusUploadForm from './corpus_upload_form.vue'; import CorpusUploadForm from './corpus_upload_form.vue';
...@@ -79,7 +80,13 @@ export default { ...@@ -79,7 +80,13 @@ export default {
resetCorpus() { resetCorpus() {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: resetCorpus, 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 { ...@@ -110,7 +117,11 @@ export default {
@primary="addCorpus" @primary="addCorpus"
@canceled="resetCorpus" @canceled="resetCorpus"
> >
<corpus-upload-form :states="states" /> <corpus-upload-form
:states="states"
@beginFileUpload="beginFileUpload"
@resetCorpus="resetCorpus"
/>
</gl-modal> </gl-modal>
</div> </div>
</template> </template>
...@@ -9,8 +9,6 @@ import { ...@@ -9,8 +9,6 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { VALID_CORPUS_MIMETYPE } from '../constants'; 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 { export default {
components: { components: {
...@@ -40,7 +38,6 @@ export default { ...@@ -40,7 +38,6 @@ export default {
attachmentName: '', attachmentName: '',
corpusName: '', corpusName: '',
files: [], files: [],
uploadTimeout: null,
}; };
}, },
computed: { computed: {
...@@ -86,29 +83,13 @@ export default { ...@@ -86,29 +83,13 @@ export default {
this.files = []; this.files = [];
}, },
cancelUpload() { cancelUpload() {
clearTimeout(this.uploadTimeout); this.$emit('resetCorpus');
this.$apollo.mutate({
mutation: resetCorpus,
variables: { name: this.corpusName, projectPath: this.projectFullPath },
});
}, },
openFileUpload() { openFileUpload() {
this.$refs.fileUpload.click(); this.$refs.fileUpload.click();
}, },
beginFileUpload() { beginFileUpload() {
// Simulate incrementing file upload progress this.$emit('beginFileUpload', { name: this.corpusName, files: this.files });
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);
}
});
}, },
onFileUploadChange(e) { onFileUploadChange(e) {
this.attachmentName = e.target.files[0].name; this.attachmentName = e.target.files[0].name;
......
mutation resetCorpus($projectPath: ID!, $name: String!) { mutation resetCorpus($projectPath: ID!) {
resetCorpus(projectPath: $projectPath, name: $name) @client { resetCorpus(projectPath: $projectPath) @client {
errors errors
} }
} }
mutation updateProgress($projectPath: ID!, $progress: Int!) {
updateProgress(projectPath: $projectPath, progress: $progress) @client {
errors
}
}
mutation uploadCorpus($projectPath: ID!, $name: String!) { mutation uploadCorpus($projectPath: ID!, $name: String!, $files: [Upload!]!) {
uploadCorpus(projectPath: $projectPath, name: $name) @client { uploadCorpus(projectPath: $projectPath, name: $name, files: $files) @client {
errors errors
} }
} }
...@@ -6,5 +6,6 @@ query getCorpuses($projectPath: ID!) { ...@@ -6,5 +6,6 @@ query getCorpuses($projectPath: ID!) {
uploadState(projectPath: $projectPath) @client { uploadState(projectPath: $projectPath) @client {
isUploading isUploading
progress progress
cancelSource
} }
} }
import produce from 'immer'; 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 axios from '~/lib/utils/axios_utils';
import getCorpusesQuery from '../queries/get_corpuses.query.graphql'; import getCorpusesQuery from '../queries/get_corpuses.query.graphql';
import updateProgress from '../mutations/update_progress.mutation.graphql';
export default { export default {
Query: { Query: {
...@@ -18,6 +21,7 @@ export default { ...@@ -18,6 +21,7 @@ export default {
return { return {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
cancelSource: null,
__typename: 'UploadState', __typename: 'UploadState',
}; };
}, },
...@@ -71,7 +75,37 @@ export default { ...@@ -71,7 +75,37 @@ export default {
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); 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({ const sourceData = cache.readQuery({
query: getCorpusesQuery, query: getCorpusesQuery,
variables: { projectPath }, variables: { projectPath },
...@@ -80,27 +114,29 @@ export default { ...@@ -80,27 +114,29 @@ export default {
const data = produce(sourceData, (draftState) => { const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = true; uploadState.isUploading = true;
// Simulate incrementing file upload progress uploadState.progress = progress;
uploadState.progress += 10;
if (uploadState.progress >= 100) { if (progress >= 100) {
uploadState.isUploading = false; uploadState.isUploading = false;
uploadState.cancelSource = null;
} }
}); });
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } });
return data.uploadState.progress;
}, },
resetCorpus: (_, { name, projectPath }, { cache }) => { resetCorpus: (_, { projectPath }, { cache }) => {
const sourceData = cache.readQuery({ const sourceData = cache.readQuery({
query: getCorpusesQuery, query: getCorpusesQuery,
variables: { projectPath }, variables: { projectPath },
}); });
sourceData.uploadState.cancelSource?.cancel();
const data = produce(sourceData, (draftState) => { const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = false; uploadState.isUploading = false;
uploadState.progress = 0; uploadState.progress = 0;
uploadState.cancelToken = null;
}); });
cache.writeQuery({ query: getCorpusesQuery, data, variables: { projectPath } }); 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