Commit 5650ee04 authored by Savas Vedova's avatar Savas Vedova

Add a utility to download files

- Migrate old custom implementations
- Provide a utility function to download inline files or from endpoints
parent 59530486
/**
* Helper function to trigger a download.
*
* - If the `fileName` is `_blank` it will open the file in a new tab.
* - If `fileData` is provided, it will inline the content and use data URLs to
* download the file. In this case the `url` property will be ignored. Please
* note that `fileData` needs to be Base64 encoded.
*/
export default ({ fileName, url, fileData }) => {
let href = url;
if (fileData) {
href = `data:text/plain;base64,${fileData}`;
}
const anchor = document.createElement('a');
anchor.download = fileName;
anchor.href = href;
anchor.click();
};
...@@ -5,6 +5,7 @@ import createFlash from '~/flash'; ...@@ -5,6 +5,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import pollUntilComplete from '~/lib/utils/poll_until_complete'; import pollUntilComplete from '~/lib/utils/poll_until_complete';
import download from '~/lib/utils/downloader';
export const STORAGE_KEY = 'vulnerability_csv_export_popover_dismissed'; export const STORAGE_KEY = 'vulnerability_csv_export_popover_dismissed';
...@@ -46,10 +47,10 @@ export default { ...@@ -46,10 +47,10 @@ export default {
.post(this.vulnerabilitiesExportEndpoint) .post(this.vulnerabilitiesExportEndpoint)
.then(({ data }) => pollUntilComplete(data._links.self)) .then(({ data }) => pollUntilComplete(data._links.self))
.then(({ data }) => { .then(({ data }) => {
const anchor = document.createElement('a'); download({
anchor.download = `csv-export-${formatDate(new Date(), 'isoDateTime')}.csv`; fileName: `csv-export-${formatDate(new Date(), 'isoDateTime')}.csv`,
anchor.href = data._links.download; url: data._links.download,
anchor.click(); });
}) })
.catch(() => { .catch(() => {
createFlash(s__('SecurityReports|There was an error while generating the report.')); createFlash(s__('SecurityReports|There was an error while generating the report.'));
......
import $ from 'jquery'; import $ from 'jquery';
import _ from 'lodash'; import _ from 'lodash';
import downloadPatchHelper from 'ee/vue_shared/security_reports/store/utils/download_patch_helper'; import download from '~/lib/utils/downloader';
import axios from '~/lib/utils/axios_utils'; 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__, n__, sprintf } from '~/locale'; import { s__, n__, sprintf } from '~/locale';
...@@ -450,7 +450,7 @@ export const downloadPatch = ({ state }) => { ...@@ -450,7 +450,7 @@ export const downloadPatch = ({ state }) => {
https://gitlab.com/gitlab-org/gitlab-ui/issues/188#note_165808493 https://gitlab.com/gitlab-org/gitlab-ui/issues/188#note_165808493
*/ */
const { vulnerability } = state.modal; const { vulnerability } = state.modal;
downloadPatchHelper(vulnerability.remediations[0].diff); download({ fileData: vulnerability.remediations[0].diff, fileName: `remediation.patch` });
$('#modal-mrwidget-security-issue').modal('hide'); $('#modal-mrwidget-security-issue').modal('hide');
}; };
......
import $ from 'jquery'; import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import download from '~/lib/utils/downloader';
import pollUntilComplete from '~/lib/utils/poll_until_complete'; import pollUntilComplete from '~/lib/utils/poll_until_complete';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import toast from '~/vue_shared/plugins/global_toast'; import toast from '~/vue_shared/plugins/global_toast';
import * as types from './mutation_types'; import * as types from './mutation_types';
import downloadPatchHelper from './utils/download_patch_helper';
/** /**
* A lot of this file has duplicate actions to * A lot of this file has duplicate actions to
...@@ -445,7 +445,7 @@ export const downloadPatch = ({ state }) => { ...@@ -445,7 +445,7 @@ export const downloadPatch = ({ state }) => {
https://gitlab.com/gitlab-org/gitlab-ui/issues/188#note_165808493 https://gitlab.com/gitlab-org/gitlab-ui/issues/188#note_165808493
*/ */
const { vulnerability } = state.modal; const { vulnerability } = state.modal;
downloadPatchHelper(vulnerability.remediations[0].diff); download({ fileData: vulnerability.remediations[0].diff, fileName: 'remediation.patch' });
$('#modal-mrwidget-security-issue').modal('hide'); $('#modal-mrwidget-security-issue').modal('hide');
}; };
......
const downloadPatchHelper = (patch, opts = {}) => {
const mergedOpts = {
isEncoded: true,
...opts,
};
const url = `data:text/plain;base64,${mergedOpts.isEncoded ? patch : btoa(patch)}`;
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'remediation.patch');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export { downloadPatchHelper as default };
...@@ -31,6 +31,10 @@ describe('vulnerabilities count actions', () => { ...@@ -31,6 +31,10 @@ describe('vulnerabilities count actions', () => {
state = initialState(); state = initialState();
}); });
afterEach(() => {
jest.clearAllMocks();
});
describe('setPipelineId', () => { describe('setPipelineId', () => {
const pipelineId = 123; const pipelineId = 123;
...@@ -443,9 +447,8 @@ describe('openModal', () => { ...@@ -443,9 +447,8 @@ describe('openModal', () => {
describe('downloadPatch', () => { describe('downloadPatch', () => {
it('creates a download link and clicks on it to download the file', () => { it('creates a download link and clicks on it to download the file', () => {
jest.spyOn(document, 'createElement'); const a = { click: jest.fn() };
jest.spyOn(document.body, 'appendChild'); jest.spyOn(document, 'createElement').mockImplementation(() => a);
jest.spyOn(document.body, 'removeChild');
actions.downloadPatch({ actions.downloadPatch({
state: { state: {
...@@ -462,8 +465,10 @@ describe('downloadPatch', () => { ...@@ -462,8 +465,10 @@ describe('downloadPatch', () => {
}); });
expect(document.createElement).toHaveBeenCalledTimes(1); expect(document.createElement).toHaveBeenCalledTimes(1);
expect(document.body.appendChild).toHaveBeenCalledTimes(1); expect(document.createElement).toHaveBeenCalledWith('a');
expect(document.body.removeChild).toHaveBeenCalledTimes(1); expect(a.click).toHaveBeenCalledTimes(1);
expect(a.download).toBe('remediation.patch');
expect(a.href).toContain('data:text/plain;base64');
}); });
}); });
......
...@@ -94,6 +94,10 @@ const createDismissedVulnerability = options => ...@@ -94,6 +94,10 @@ const createDismissedVulnerability = options =>
isDismissed: true, isDismissed: true,
}); });
afterEach(() => {
jest.clearAllMocks();
});
describe('security reports actions', () => { describe('security reports actions', () => {
let mockedState; let mockedState;
let mock; let mock;
...@@ -870,9 +874,8 @@ describe('security reports actions', () => { ...@@ -870,9 +874,8 @@ describe('security reports actions', () => {
describe('downloadPatch', () => { describe('downloadPatch', () => {
it('creates a download link and clicks on it to download the file', () => { it('creates a download link and clicks on it to download the file', () => {
jest.spyOn(document, 'createElement'); const a = { click: jest.fn() };
jest.spyOn(document.body, 'appendChild'); jest.spyOn(document, 'createElement').mockImplementation(() => a);
jest.spyOn(document.body, 'removeChild');
downloadPatch({ downloadPatch({
state: { state: {
...@@ -889,8 +892,10 @@ describe('security reports actions', () => { ...@@ -889,8 +892,10 @@ describe('security reports actions', () => {
}); });
expect(document.createElement).toHaveBeenCalledTimes(1); expect(document.createElement).toHaveBeenCalledTimes(1);
expect(document.body.appendChild).toHaveBeenCalledTimes(1); expect(document.createElement).toHaveBeenCalledWith('a');
expect(document.body.removeChild).toHaveBeenCalledTimes(1); expect(a.click).toHaveBeenCalledTimes(1);
expect(a.download).toBe('remediation.patch');
expect(a.href).toContain('data:text/plain;base64');
}); });
}); });
......
import downloadPatchHelper from 'ee/vue_shared/security_reports/store/utils/download_patch_helper';
describe('downloadPatchHelper', () => {
beforeEach(() => {
jest.spyOn(document, 'createElement');
jest.spyOn(document.body, 'appendChild');
jest.spyOn(document.body, 'removeChild');
});
describe('with a base64 encoded string', () => {
it('creates a download link and clicks on it to download the file', done => {
const base64String = btoa('abcdef');
document.onclick = e => {
expect(e.target.download).toBe('remediation.patch');
expect(e.target.href).toBe('data:text/plain;base64,YWJjZGVm');
done();
};
downloadPatchHelper(base64String);
expect(document.createElement).toHaveBeenCalledWith('a');
expect(document.body.appendChild).toHaveBeenCalledTimes(1);
expect(document.body.removeChild).toHaveBeenCalledTimes(1);
});
});
describe('without a base64 encoded string', () => {
it('creates a download link and clicks on it to download the file', done => {
const unencodedString = 'abcdef';
document.onclick = e => {
expect(e.target.download).toBe('remediation.patch');
expect(e.target.href).toBe('data:text/plain;base64,YWJjZGVm');
done();
};
downloadPatchHelper(unencodedString, { isEncoded: false });
expect(document.createElement).toHaveBeenCalledWith('a');
expect(document.body.appendChild).toHaveBeenCalledTimes(1);
expect(document.body.removeChild).toHaveBeenCalledTimes(1);
});
});
});
import downloader from '~/lib/utils/downloader';
describe('Downloader', () => {
let a;
beforeEach(() => {
a = { click: jest.fn() };
jest.spyOn(document, 'createElement').mockImplementation(() => a);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('when inline file content is provided', () => {
const fileData = 'inline content';
const fileName = 'test.csv';
it('uses the data urls to download the file', () => {
downloader({ fileName, fileData });
expect(document.createElement).toHaveBeenCalledWith('a');
expect(a.download).toBe(fileName);
expect(a.href).toBe(`data:text/plain;base64,${fileData}`);
expect(a.click).toHaveBeenCalledTimes(1);
});
});
describe('when an endpoint is provided', () => {
const url = 'https://gitlab.com/test.csv';
const fileName = 'test.csv';
it('uses the endpoint to download the file', () => {
downloader({ fileName, url });
expect(document.createElement).toHaveBeenCalledWith('a');
expect(a.download).toBe(fileName);
expect(a.href).toBe(url);
expect(a.click).toHaveBeenCalledTimes(1);
});
});
});
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