Commit fe46d96f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'djadmin-dast-local-storage' into 'master'

Persist On-demand Scans form values in localStorage

See merge request gitlab-org/gitlab!53466
parents c97300a8 f4b682ac
...@@ -20,6 +20,8 @@ import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site ...@@ -20,6 +20,8 @@ import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site
import { initFormField } from 'ee/security_configuration/utils'; import { initFormField } from 'ee/security_configuration/utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import validation from '~/vue_shared/directives/validation'; import validation from '~/vue_shared/directives/validation';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { serializeFormObject } from '~/lib/utils/forms';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { redirectTo, queryToObject } from '~/lib/utils/url_utility'; import { redirectTo, queryToObject } from '~/lib/utils/url_utility';
...@@ -42,6 +44,8 @@ import ProfileSelectorSummaryCell from './profile_selector/summary_cell.vue'; ...@@ -42,6 +44,8 @@ import ProfileSelectorSummaryCell from './profile_selector/summary_cell.vue';
import ScannerProfileSelector from './profile_selector/scanner_profile_selector.vue'; import ScannerProfileSelector from './profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from './profile_selector/site_profile_selector.vue'; import SiteProfileSelector from './profile_selector/site_profile_selector.vue';
export const ON_DEMAND_SCANS_STORAGE_KEY = 'on-demand-scans-new-form';
const createProfilesApolloOptions = (name, field, { fetchQuery, fetchError }) => ({ const createProfilesApolloOptions = (name, field, { fetchQuery, fetchError }) => ({
query: fetchQuery, query: fetchQuery,
variables() { variables() {
...@@ -80,6 +84,7 @@ export default { ...@@ -80,6 +84,7 @@ export default {
GlLink, GlLink,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
LocalStorageSync,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -157,6 +162,7 @@ export default { ...@@ -157,6 +162,7 @@ export default {
errorType: null, errorType: null,
errors: [], errors: [],
showAlert: false, showAlert: false,
clearStorage: false,
}; };
}, },
computed: { computed: {
...@@ -229,6 +235,14 @@ export default { ...@@ -229,6 +235,14 @@ export default {
} = this; } = this;
return isFormInvalid || (loading && loading !== saveScanBtnId); return isFormInvalid || (loading && loading !== saveScanBtnId);
}, },
formFieldValues() {
const { selectedScannerProfileId, selectedSiteProfileId } = this;
return {
...serializeFormObject(this.form.fields),
selectedScannerProfileId,
selectedSiteProfileId,
};
},
}, },
created() { created() {
const params = queryToObject(window.location.search); const params = queryToObject(window.location.search);
...@@ -264,8 +278,7 @@ export default { ...@@ -264,8 +278,7 @@ export default {
input = { input = {
...input, ...input,
...(this.isEdit ? { id: this.dastScan.id } : {}), ...(this.isEdit ? { id: this.dastScan.id } : {}),
name: this.form.fields.name.value, ...serializeFormObject(this.form.fields),
description: this.form.fields.description.value,
runAfterCreate, runAfterCreate,
}; };
} }
...@@ -285,7 +298,9 @@ export default { ...@@ -285,7 +298,9 @@ export default {
this.loading = false; this.loading = false;
} else if (this.glFeatures.dastSavedScans && !runAfterCreate) { } else if (this.glFeatures.dastSavedScans && !runAfterCreate) {
redirectTo(response.dastProfile.editPath); redirectTo(response.dastProfile.editPath);
this.clearStorage = true;
} else { } else {
this.clearStorage = true;
redirectTo(response.pipelineUrl); redirectTo(response.pipelineUrl);
} }
}) })
...@@ -305,12 +320,31 @@ export default { ...@@ -305,12 +320,31 @@ export default {
this.errors = []; this.errors = [];
this.showAlert = false; this.showAlert = false;
}, },
updateFromStorage(val) {
const { selectedSiteProfileId, selectedScannerProfileId, name, description } = val;
this.form.fields.name.value = name ?? this.form.fields.name.value;
this.form.fields.description.value = description ?? this.form.fields.description.value;
// precedence is given to profile IDs passed from the query params
this.selectedSiteProfileId = this.selectedSiteProfileId ?? selectedSiteProfileId;
this.selectedScannerProfileId = this.selectedScannerProfileId ?? selectedScannerProfileId;
},
}, },
ON_DEMAND_SCANS_STORAGE_KEY,
}; };
</script> </script>
<template> <template>
<gl-form novalidate @submit.prevent="onSubmit()"> <gl-form novalidate @submit.prevent="onSubmit()">
<local-storage-sync
v-if="glFeatures.dastSavedScans && !isEdit"
as-json
:storage-key="$options.ON_DEMAND_SCANS_STORAGE_KEY"
:clear="clearStorage"
:value="formFieldValues"
@input="updateFromStorage"
/>
<header class="gl-mb-6"> <header class="gl-mb-6">
<div class="gl-mt-6 gl-display-flex"> <div class="gl-mt-6 gl-display-flex">
<h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2> <h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2>
......
...@@ -13,6 +13,8 @@ import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/gr ...@@ -13,6 +13,8 @@ import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/gr
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql'; import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; import { redirectTo, setUrlParams } from '~/lib/utils/url_utility';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import * as responses from '../mocks/apollo_mocks'; import * as responses from '../mocks/apollo_mocks';
import { scannerProfiles, siteProfiles } from '../mocks/mock_data'; import { scannerProfiles, siteProfiles } from '../mocks/mock_data';
...@@ -43,6 +45,7 @@ const dastScan = { ...@@ -43,6 +45,7 @@ const dastScan = {
siteProfileId: validatedSiteProfile.id, siteProfileId: validatedSiteProfile.id,
}; };
useLocalStorageSpy();
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute, isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
queryToObject: jest.requireActual('~/lib/utils/url_utility').queryToObject, queryToObject: jest.requireActual('~/lib/utils/url_utility').queryToObject,
...@@ -50,6 +53,8 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -50,6 +53,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(), redirectTo: jest.fn(),
})); }));
const LOCAL_STORAGE_KEY = 'on-demand-scans-new-form';
describe('OnDemandScansForm', () => { describe('OnDemandScansForm', () => {
let localVue; let localVue;
let subject; let subject;
...@@ -143,6 +148,7 @@ describe('OnDemandScansForm', () => { ...@@ -143,6 +148,7 @@ describe('OnDemandScansForm', () => {
}, },
stubs: { stubs: {
GlFormInput: GlFormInputStub, GlFormInput: GlFormInputStub,
LocalStorageSync,
}, },
}, },
{ ...options, localVue, apolloProvider }, { ...options, localVue, apolloProvider },
...@@ -164,6 +170,7 @@ describe('OnDemandScansForm', () => { ...@@ -164,6 +170,7 @@ describe('OnDemandScansForm', () => {
afterEach(() => { afterEach(() => {
subject.destroy(); subject.destroy();
subject = null; subject = null;
localStorage.clear();
}); });
it('renders properly', () => { it('renders properly', () => {
...@@ -216,6 +223,45 @@ describe('OnDemandScansForm', () => { ...@@ -216,6 +223,45 @@ describe('OnDemandScansForm', () => {
}); });
}); });
describe('local storage', () => {
it('get updated when form is modified', async () => {
mountShallowSubject();
await setValidFormData();
expect(localStorage.setItem.mock.calls).toEqual([
[
LOCAL_STORAGE_KEY,
JSON.stringify({
name: 'My daily scan',
selectedScannerProfileId: 'gid://gitlab/DastScannerProfile/1',
selectedSiteProfileId: 'gid://gitlab/DastSiteProfile/1',
}),
],
]);
});
it('reload the form data when available', async () => {
localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify({
name: dastScan.name,
description: dastScan.description,
selectedScannerProfileId: dastScan.scannerProfileId,
selectedSiteProfileId: dastScan.siteProfileId,
}),
);
mountShallowSubject();
await subject.vm.$nextTick();
expect(findNameInput().attributes('value')).toBe(dastScan.name);
expect(findDescriptionInput().attributes('value')).toBe(dastScan.description);
expect(findScannerProfilesSelector().attributes('value')).toBe(dastScan.scannerProfileId);
expect(findSiteProfilesSelector().attributes('value')).toBe(dastScan.siteProfileId);
});
});
describe('submit button', () => { describe('submit button', () => {
let submitButton; let submitButton;
...@@ -271,7 +317,6 @@ describe('OnDemandScansForm', () => { ...@@ -271,7 +317,6 @@ describe('OnDemandScansForm', () => {
variables: { variables: {
input: { input: {
name: 'My daily scan', name: 'My daily scan',
description: '',
dastScannerProfileId: passiveScannerProfile.id, dastScannerProfileId: passiveScannerProfile.id,
dastSiteProfileId: nonValidatedSiteProfile.id, dastSiteProfileId: nonValidatedSiteProfile.id,
fullPath: projectPath, fullPath: projectPath,
...@@ -288,6 +333,10 @@ describe('OnDemandScansForm', () => { ...@@ -288,6 +333,10 @@ describe('OnDemandScansForm', () => {
it('does not show an alert', async () => { it('does not show an alert', async () => {
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
}); });
it('clears local storage', () => {
expect(localStorage.removeItem.mock.calls).toEqual([[LOCAL_STORAGE_KEY]]);
});
}); });
describe('when editing an existing scan', () => { describe('when editing an existing scan', () => {
...@@ -539,5 +588,25 @@ describe('OnDemandScansForm', () => { ...@@ -539,5 +588,25 @@ describe('OnDemandScansForm', () => {
expect(subject.find(SiteProfileSelector).attributes('value')).toBe(siteProfile.id); expect(subject.find(SiteProfileSelector).attributes('value')).toBe(siteProfile.id);
expect(subject.find(ScannerProfileSelector).attributes('value')).toBe(scannerProfile.id); expect(subject.find(ScannerProfileSelector).attributes('value')).toBe(scannerProfile.id);
}); });
it('when local storage data is available', async () => {
localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify({
selectedScannerProfileId: dastScan.scannerProfileId,
selectedSiteProfileId: dastScan.siteProfileId,
}),
);
global.jsdom.reconfigure({
url: setUrlParams({ site_profile_id: 1, scanner_profile_id: 1 }, URL_HOST),
});
mountShallowSubject();
await subject.vm.$nextTick();
expect(findScannerProfilesSelector().attributes('value')).toBe(scannerProfile.id);
expect(findSiteProfilesSelector().attributes('value')).toBe(siteProfile.id);
});
}); });
}); });
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