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
import { initFormField } from 'ee/security_configuration/utils';
import { s__ } from '~/locale';
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 glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { redirectTo, queryToObject } from '~/lib/utils/url_utility';
......@@ -42,6 +44,8 @@ import ProfileSelectorSummaryCell from './profile_selector/summary_cell.vue';
import ScannerProfileSelector from './profile_selector/scanner_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 }) => ({
query: fetchQuery,
variables() {
......@@ -80,6 +84,7 @@ export default {
GlLink,
GlSkeletonLoader,
GlSprintf,
LocalStorageSync,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -157,6 +162,7 @@ export default {
errorType: null,
errors: [],
showAlert: false,
clearStorage: false,
};
},
computed: {
......@@ -229,6 +235,14 @@ export default {
} = this;
return isFormInvalid || (loading && loading !== saveScanBtnId);
},
formFieldValues() {
const { selectedScannerProfileId, selectedSiteProfileId } = this;
return {
...serializeFormObject(this.form.fields),
selectedScannerProfileId,
selectedSiteProfileId,
};
},
},
created() {
const params = queryToObject(window.location.search);
......@@ -264,8 +278,7 @@ export default {
input = {
...input,
...(this.isEdit ? { id: this.dastScan.id } : {}),
name: this.form.fields.name.value,
description: this.form.fields.description.value,
...serializeFormObject(this.form.fields),
runAfterCreate,
};
}
......@@ -285,7 +298,9 @@ export default {
this.loading = false;
} else if (this.glFeatures.dastSavedScans && !runAfterCreate) {
redirectTo(response.dastProfile.editPath);
this.clearStorage = true;
} else {
this.clearStorage = true;
redirectTo(response.pipelineUrl);
}
})
......@@ -305,12 +320,31 @@ export default {
this.errors = [];
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>
<template>
<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">
<div class="gl-mt-6 gl-display-flex">
<h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2>
......
......@@ -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 { stubComponent } from 'helpers/stub_component';
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 { scannerProfiles, siteProfiles } from '../mocks/mock_data';
......@@ -43,6 +45,7 @@ const dastScan = {
siteProfileId: validatedSiteProfile.id,
};
useLocalStorageSpy();
jest.mock('~/lib/utils/url_utility', () => ({
isAbsolute: jest.requireActual('~/lib/utils/url_utility').isAbsolute,
queryToObject: jest.requireActual('~/lib/utils/url_utility').queryToObject,
......@@ -50,6 +53,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const LOCAL_STORAGE_KEY = 'on-demand-scans-new-form';
describe('OnDemandScansForm', () => {
let localVue;
let subject;
......@@ -143,6 +148,7 @@ describe('OnDemandScansForm', () => {
},
stubs: {
GlFormInput: GlFormInputStub,
LocalStorageSync,
},
},
{ ...options, localVue, apolloProvider },
......@@ -164,6 +170,7 @@ describe('OnDemandScansForm', () => {
afterEach(() => {
subject.destroy();
subject = null;
localStorage.clear();
});
it('renders properly', () => {
......@@ -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', () => {
let submitButton;
......@@ -271,7 +317,6 @@ describe('OnDemandScansForm', () => {
variables: {
input: {
name: 'My daily scan',
description: '',
dastScannerProfileId: passiveScannerProfile.id,
dastSiteProfileId: nonValidatedSiteProfile.id,
fullPath: projectPath,
......@@ -288,6 +333,10 @@ describe('OnDemandScansForm', () => {
it('does not show an alert', async () => {
expect(findAlert().exists()).toBe(false);
});
it('clears local storage', () => {
expect(localStorage.removeItem.mock.calls).toEqual([[LOCAL_STORAGE_KEY]]);
});
});
describe('when editing an existing scan', () => {
......@@ -539,5 +588,25 @@ describe('OnDemandScansForm', () => {
expect(subject.find(SiteProfileSelector).attributes('value')).toBe(siteProfile.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