Commit 4e4d9fd7 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Enrique Alcántara

Add schedules info to the saved scans list

parent 81992919
...@@ -311,10 +311,10 @@ export default { ...@@ -311,10 +311,10 @@ export default {
this.form.fields.name.value = name ?? this.form.fields.name.value; this.form.fields.name.value = name ?? this.form.fields.name.value;
this.form.fields.description.value = description ?? this.form.fields.description.value; this.form.fields.description.value = description ?? this.form.fields.description.value;
this.selectedBranch = selectedBranch; this.selectedBranch = selectedBranch;
this.profileSchedule = profileSchedule ?? this.profileSchedule;
// precedence is given to profile IDs passed from the query params // precedence is given to profile IDs passed from the query params
this.selectedSiteProfileId = this.selectedSiteProfileId ?? selectedSiteProfileId; this.selectedSiteProfileId = this.selectedSiteProfileId ?? selectedSiteProfileId;
this.selectedScannerProfileId = this.selectedScannerProfileId ?? selectedScannerProfileId; this.selectedScannerProfileId = this.selectedScannerProfileId ?? selectedScannerProfileId;
this.profileSchedule = this.profileSchedule ?? profileSchedule;
}, },
}, },
}; };
...@@ -454,7 +454,11 @@ export default { ...@@ -454,7 +454,11 @@ export default {
:has-conflict="hasProfilesConflict" :has-conflict="hasProfilesConflict"
/> />
<scan-schedule v-if="glFeatures.dastOnDemandScansScheduler" v-model="profileSchedule" /> <scan-schedule
v-if="glFeatures.dastOnDemandScansScheduler"
v-model="profileSchedule"
class="gl-mb-5"
/>
<profile-conflict-alert <profile-conflict-alert
v-if="hasProfilesConflict" v-if="hasProfilesConflict"
......
...@@ -8,31 +8,7 @@ import { ...@@ -8,31 +8,7 @@ import {
} from '~/lib/utils/datetime/date_format_utility'; } from '~/lib/utils/datetime/date_format_utility';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import { SCAN_CADENCE_OPTIONS } from '../settings'; import { SCAN_CADENCE_OPTIONS } from '../settings';
import { toGraphQLCadence, fromGraphQLCadence } from '../utils';
/**
* Converts a cadence option string into the proper schedule parameter.
* @param {String} str Cadence option's string representation.
* @returns {Object} Corresponding schedule parameter.
*/
const toGraphQLCadence = (str) => {
if (!str) {
return '';
}
const [unit, duration] = str.split('_');
return { unit, duration: Number(duration) };
};
/**
* Converts a schedule parameter into the corresponding string option.
* @param {Object} obj Schedule paramter.
* @returns {String} Corresponding cadence option's string representation.
*/
const fromGraphQLCadence = (obj) => {
if (!obj) {
return '';
}
return `${obj.unit}_${obj.duration}`.toUpperCase();
};
export default { export default {
name: 'ScanSchedule', name: 'ScanSchedule',
...@@ -49,17 +25,17 @@ export default { ...@@ -49,17 +25,17 @@ export default {
value: { value: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: null,
}, },
}, },
data() { data() {
return { return {
form: { form: {
isScheduledScan: this.value.active ?? false, isScheduledScan: this.value?.active ?? false,
selectedTimezone: this.value.timezone ?? null, selectedTimezone: this.value?.timezone ?? null,
startDate: null, startDate: null,
startTime: null, startTime: null,
cadence: fromGraphQLCadence(this.value.cadence) ?? SCAN_CADENCE_OPTIONS[0].value, cadence: fromGraphQLCadence(this.value?.cadence),
}, },
}; };
}, },
...@@ -79,7 +55,7 @@ export default { ...@@ -79,7 +55,7 @@ export default {
}, },
}, },
created() { created() {
const date = this.value.startsAt ?? null; const date = this.value?.startsAt ?? null;
if (date !== null) { if (date !== null) {
const localeDate = new Date( const localeDate = new Date(
stripTimezoneFromISODate(date, this.selectedTimezoneData?.offset), stripTimezoneFromISODate(date, this.selectedTimezoneData?.offset),
......
...@@ -39,10 +39,51 @@ const YEAR_1 = 'YEAR_1'; ...@@ -39,10 +39,51 @@ const YEAR_1 = 'YEAR_1';
export const SCAN_CADENCE_OPTIONS = [ export const SCAN_CADENCE_OPTIONS = [
{ value: '', text: __('Never') }, { value: '', text: __('Never') },
{ value: DAY_1, text: __('Every day') }, {
{ value: WEEK_1, text: __('Every week') }, value: DAY_1,
{ value: MONTH_1, text: __('Every month') }, text: __('Every day'),
{ value: MONTH_3, text: __('Every 3 months') }, description: {
{ value: MONTH_6, text: __('Every 6 months') }, text: __('Every day at %{time} %{timezone}'),
{ value: YEAR_1, text: __('Every year') }, },
},
{
value: WEEK_1,
text: __('Every week'),
description: {
text: __('Every week on %{day} at %{time} %{timezone}'),
dayFormat: { weekday: 'long' },
},
},
{
value: MONTH_1,
text: __('Every month'),
description: {
text: __('Every month on the %{day} at %{time} %{timezone}'),
dayFormat: { day: 'numeric' },
},
},
{
value: MONTH_3,
text: __('Every 3 months'),
description: {
text: __('Every 3 months on the %{day} at %{time} %{timezone}'),
dayFormat: { day: 'numeric' },
},
},
{
value: MONTH_6,
text: __('Every 6 months'),
description: {
text: __('Every 6 months on the %{day} at %{time} %{timezone}'),
dayFormat: { day: 'numeric' },
},
},
{
value: YEAR_1,
text: __('Every year'),
description: {
text: __('Every year on %{day} at %{time} %{timezone}'),
dayFormat: { month: 'long', day: 'numeric' },
},
},
]; ];
import { SCAN_CADENCE_OPTIONS } from './settings';
/**
* Converts a cadence option string into the proper schedule parameter.
* @param {String} str Cadence option's string representation.
* @returns {Object} Corresponding schedule parameter.
*/
export const toGraphQLCadence = (str) => {
if (!str) {
return {};
}
const [unit, duration] = str.split('_');
return { unit, duration: Number(duration) };
};
/**
* Converts a schedule parameter into the corresponding string option.
* @param {Object} obj Schedule paramter.
* @returns {String} Corresponding cadence option's string representation.
*/
export const fromGraphQLCadence = (obj) => {
if (!obj?.unit || !obj?.duration) {
return SCAN_CADENCE_OPTIONS[0].value;
}
return `${obj.unit}_${obj.duration}`.toUpperCase();
};
...@@ -7,6 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -7,6 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql'; import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql';
import ProfilesList from './dast_profiles_list.vue'; import ProfilesList from './dast_profiles_list.vue';
import DastScanBranch from './dast_scan_branch.vue'; import DastScanBranch from './dast_scan_branch.vue';
import ScanSchedule from './dast_scan_schedule.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue'; import ScanTypeBadge from './dast_scan_type_badge.vue';
export default { export default {
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
GlButton, GlButton,
ProfilesList, ProfilesList,
DastScanBranch, DastScanBranch,
ScanSchedule,
ScanTypeBadge, ScanTypeBadge,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
...@@ -115,6 +117,11 @@ export default { ...@@ -115,6 +117,11 @@ export default {
<scan-type-badge :scan-type="value" /> <scan-type-badge :scan-type="value" />
</template> </template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastProfileSchedule)="{ value }">
<scan-schedule :schedule="value || null" />
</template>
<template #actions="{ profile }"> <template #actions="{ profile }">
<gl-button <gl-button
size="small" size="small"
......
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { SCAN_CADENCE_OPTIONS } from 'ee/on_demand_scans/settings';
import { fromGraphQLCadence } from 'ee/on_demand_scans/utils';
import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility';
import { sprintf } from '~/locale';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['timezones'],
props: {
schedule: {
type: Object,
required: false,
default: null,
},
},
computed: {
isScheduled() {
return Boolean(this.schedule?.active);
},
cadence() {
return fromGraphQLCadence(this.schedule.cadence);
},
cadenceOption() {
return SCAN_CADENCE_OPTIONS.find((option) => option.value === this.cadence);
},
isRepeating() {
return this.isScheduled && this.cadence;
},
timezone() {
const { timezone } = this.schedule;
return this.timezones.find(({ identifier }) => identifier === timezone) ?? {};
},
runDate() {
return new Date(stripTimezoneFromISODate(this.schedule.startsAt));
},
text() {
if (this.isRepeating) {
return this.cadenceOption.text;
}
return this.runDate.toLocaleDateString(window.navigator.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
},
tooltip() {
const time = this.runDate.toLocaleTimeString(window.navigator.language, {
hour: '2-digit',
minute: '2-digit',
});
const { abbr: timezone = '' } = this.timezone;
if (this.isRepeating) {
const { text, dayFormat } = this.cadenceOption.description;
const day = dayFormat
? this.runDate.toLocaleDateString(window.navigator.language, dayFormat)
: null;
return sprintf(text, {
day,
time,
timezone,
});
}
return `${time} ${timezone}`;
},
},
};
</script>
<template>
<span v-if="!isScheduled">-</span>
<span v-else v-gl-tooltip="tooltip">{{ text }}</span>
</template>
...@@ -15,6 +15,7 @@ export default () => { ...@@ -15,6 +15,7 @@ export default () => {
newDastScannerProfilePath, newDastScannerProfilePath,
newDastSiteProfilePath, newDastSiteProfilePath,
projectFullPath, projectFullPath,
timezones,
}, },
} = el; } = el;
...@@ -30,6 +31,9 @@ export default () => { ...@@ -30,6 +31,9 @@ export default () => {
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
provide: {
timezones: JSON.parse(timezones),
},
render(h) { render(h) {
return h(DastProfiles, { return h(DastProfiles, {
props, props,
......
...@@ -18,6 +18,16 @@ query DastProfiles($fullPath: ID!, $after: String, $before: String, $first: Int, ...@@ -18,6 +18,16 @@ query DastProfiles($fullPath: ID!, $after: String, $before: String, $first: Int,
id id
scanType scanType
} }
dastProfileSchedule {
id
active
startsAt
timezone
cadence {
unit
duration
}
}
branch { branch {
name name
exists exists
......
...@@ -39,6 +39,10 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({ ...@@ -39,6 +39,10 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
label: s__('DastProfiles|Scan mode'), label: s__('DastProfiles|Scan mode'),
key: 'dastScannerProfile.scanType', key: 'dastScannerProfile.scanType',
}, },
{
label: s__('DastProfiles|Schedule'),
key: 'dastProfileSchedule',
},
], ],
i18n: { i18n: {
createNewLinkText: s__('DastProfiles|DAST Scan'), createNewLinkText: s__('DastProfiles|DAST Scan'),
......
...@@ -6,7 +6,8 @@ module Projects::Security::DastProfilesHelper ...@@ -6,7 +6,8 @@ module Projects::Security::DastProfilesHelper
'new_dast_saved_scan_path' => new_project_on_demand_scan_path(project), 'new_dast_saved_scan_path' => new_project_on_demand_scan_path(project),
'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project), 'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project),
'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project), 'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project),
'project_full_path' => project.path_with_namespace 'project_full_path' => project.path_with_namespace,
'timezones' => timezone_data(format: :full).to_json
} }
end end
end end
...@@ -96,7 +96,7 @@ describe('ScanSchedule', () => { ...@@ -96,7 +96,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[0]).toEqual([ expect(wrapper.emitted().input[0]).toEqual([
{ {
active: true, active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value, cadence: {},
startsAt: null, startsAt: null,
timezone: null, timezone: null,
}, },
...@@ -111,7 +111,7 @@ describe('ScanSchedule', () => { ...@@ -111,7 +111,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[2]).toEqual([ expect(wrapper.emitted().input[2]).toEqual([
{ {
active: true, active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value, cadence: {},
startsAt: '2021-08-12T11:00:00.000Z', startsAt: '2021-08-12T11:00:00.000Z',
timezone: null, timezone: null,
}, },
...@@ -126,7 +126,7 @@ describe('ScanSchedule', () => { ...@@ -126,7 +126,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[2]).toEqual([ expect(wrapper.emitted().input[2]).toEqual([
{ {
active: true, active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value, cadence: {},
startsAt: null, startsAt: null,
timezone: null, timezone: null,
}, },
...@@ -148,7 +148,7 @@ describe('ScanSchedule', () => { ...@@ -148,7 +148,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[1]).toEqual([ expect(wrapper.emitted().input[1]).toEqual([
{ {
active: false, active: false,
cadence: SCAN_CADENCE_OPTIONS[0].value, cadence: {},
startsAt: null, startsAt: null,
timezone: null, timezone: null,
}, },
...@@ -157,26 +157,40 @@ describe('ScanSchedule', () => { ...@@ -157,26 +157,40 @@ describe('ScanSchedule', () => {
}); });
describe('editing a schedule', () => { describe('editing a schedule', () => {
const startsAt = '2001-09-27T08:45:00.000Z'; const schedule = {
active: true,
startsAt: '2001-09-27T08:45:00.000Z',
cadence: { unit: 'MONTH', duration: 1 },
timezone: timezoneSST.identifier,
};
beforeEach(() => { it('initializes fields with provided values', () => {
createComponent({ createComponent({
propsData: { propsData: {
value: { value: {
active: true, ...schedule,
startsAt,
cadence: { unit: 'MONTH', duration: 1 }, cadence: { unit: 'MONTH', duration: 1 },
timezone: timezoneSST.identifier,
}, },
}, },
}); });
});
it('initializes fields with provided values', () => {
expect(findCheckbox().props('checked')).toBe(true); expect(findCheckbox().props('checked')).toBe(true);
expect(findDatepicker().props('value')).toEqual(new Date(startsAt)); expect(findDatepicker().props('value')).toEqual(new Date(schedule.startsAt));
expect(findTimeInput().element.value).toBe('08:45'); expect(findTimeInput().element.value).toBe('08:45');
expect(findCadenceInput().props('value')).toBe(SCAN_CADENCE_OPTIONS[3].value); expect(findCadenceInput().props('value')).toBe(SCAN_CADENCE_OPTIONS[3].value);
}); });
it('uses default cadence if stored value is empty', () => {
createComponent({
propsData: {
value: {
...schedule,
cadence: {},
},
},
});
expect(findCadenceInput().props('value')).toBe(SCAN_CADENCE_OPTIONS[0].value);
});
}); });
}); });
import { toGraphQLCadence, fromGraphQLCadence } from 'ee/on_demand_scans/utils';
describe('On-demand scans utils', () => {
describe('toGraphQLCadence', () => {
it.each(['', null, undefined])('returns an empty object if argument is falsy', (argument) => {
expect(toGraphQLCadence(argument)).toEqual({});
});
it.each`
input | expectedOutput
${'UNIT_1'} | ${{ unit: 'UNIT', duration: 1 }}
${'MONTH_3'} | ${{ unit: 'MONTH', duration: 3 }}
`('properly computes $input', ({ input, expectedOutput }) => {
expect(toGraphQLCadence(input)).toEqual(expectedOutput);
});
});
describe('fromGraphQLCadence', () => {
it.each(['', null, undefined, {}, { unit: null, duration: null }])(
'returns an empty string if argument is invalid',
(argument) => {
expect(fromGraphQLCadence(argument)).toBe('');
},
);
it.each`
input | expectedOutput
${{ unit: 'UNIT', duration: 1 }} | ${'UNIT_1'}
${{ unit: 'MONTH', duration: 3 }} | ${'MONTH_3'}
`('properly computes $input', ({ input, expectedOutput }) => {
expect(fromGraphQLCadence(input)).toEqual(expectedOutput);
});
});
});
import { shallowMount } from '@vue/test-utils';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const mockTimezones = getJSONFixture('timezones/full.json');
describe('EE - DastScanSchedule', () => {
let wrapper;
const wrapperFactory = (mountFn = shallowMount) => (schedule) => {
wrapper = mountFn(DastScanSchedule, {
provide: {
timezones: mockTimezones,
},
propsData: {
schedule,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
const createComponent = wrapperFactory();
afterEach(() => {
wrapper.destroy();
});
it.each`
description | schedule
${'scan is not scheduled'} | ${null}
${'schedule is disabled'} | ${{ active: false }}
`(`renders '-' if $description`, ({ schedule }) => {
createComponent(schedule);
expect(wrapper.text()).toBe('-');
});
describe.each(['', {}, { unit: null, duration: null }])(
'non-repeating schedule with cadence = %s',
(cadence) => {
const schedule = {
active: true,
cadence,
startsAt: '2021-09-08T10:00:00+02:00',
timezone: 'Europe/Paris',
};
beforeEach(() => {
createComponent(schedule);
});
it('renders the run date', () => {
expect(wrapper.text()).toBe('September 8, 2021');
});
it('attaches a tooltip with the run time', () => {
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe('10:00 AM CEST');
});
},
);
describe.each`
unit | duration | expectedText | expectedTooltip
${'DAY'} | ${1} | ${'Every day'} | ${'Every day at 10:00 AM CEST'}
${'WEEK'} | ${1} | ${'Every week'} | ${'Every week on Wednesday at 10:00 AM CEST'}
${'MONTH'} | ${1} | ${'Every month'} | ${'Every month on the 8 at 10:00 AM CEST'}
${'MONTH'} | ${3} | ${'Every 3 months'} | ${'Every 3 months on the 8 at 10:00 AM CEST'}
${'YEAR'} | ${1} | ${'Every year'} | ${'Every year on September 8 at 10:00 AM CEST'}
`(
'repeating schedule ($expectedTooltip)',
({ unit, duration, expectedText, expectedTooltip }) => {
const schedule = {
active: true,
cadence: { unit, duration },
startsAt: '2021-09-08T10:00:00+02:00',
timezone: 'Europe/Paris',
};
beforeEach(() => {
createComponent(schedule);
});
it('renders the cadence text', () => {
expect(wrapper.text()).toBe(expectedText);
});
it('attaches a tooltip with the recurrence details', () => {
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(expectedTooltip);
});
},
);
describe('unknown timezone', () => {
it("attaches a tooltip without the timezone's code", () => {
createComponent({
active: true,
startsAt: '2021-09-08T10:00:00+02:00',
timezone: 'TanukiLand/GitLabCity',
});
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe('10:00 AM ');
});
});
});
...@@ -12,7 +12,8 @@ RSpec.describe Projects::Security::DastProfilesHelper do ...@@ -12,7 +12,8 @@ RSpec.describe Projects::Security::DastProfilesHelper do
'new_dast_saved_scan_path' => new_project_on_demand_scan_path(project), 'new_dast_saved_scan_path' => new_project_on_demand_scan_path(project),
'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project), 'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project),
'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project), 'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project),
'project_full_path' => project.path_with_namespace 'project_full_path' => project.path_with_namespace,
'timezones' => helper.timezone_data(format: :full).to_json
} }
) )
end end
......
...@@ -10429,6 +10429,9 @@ msgstr "" ...@@ -10429,6 +10429,9 @@ msgstr ""
msgid "DastProfiles|Scanner name" msgid "DastProfiles|Scanner name"
msgstr "" msgstr ""
msgid "DastProfiles|Schedule"
msgstr ""
msgid "DastProfiles|Select branch" msgid "DastProfiles|Select branch"
msgstr "" msgstr ""
...@@ -13522,21 +13525,33 @@ msgstr "" ...@@ -13522,21 +13525,33 @@ msgstr ""
msgid "Every 3 months" msgid "Every 3 months"
msgstr "" msgstr ""
msgid "Every 3 months on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every 6 months" msgid "Every 6 months"
msgstr "" msgstr ""
msgid "Every 6 months on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every day" msgid "Every day"
msgstr "" msgstr ""
msgid "Every day (at %{time})" msgid "Every day (at %{time})"
msgstr "" msgstr ""
msgid "Every day at %{time} %{timezone}"
msgstr ""
msgid "Every month" msgid "Every month"
msgstr "" msgstr ""
msgid "Every month (Day %{day} at %{time})" msgid "Every month (Day %{day} at %{time})"
msgstr "" msgstr ""
msgid "Every month on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every three months" msgid "Every three months"
msgstr "" msgstr ""
...@@ -13551,9 +13566,15 @@ msgstr[1] "" ...@@ -13551,9 +13566,15 @@ msgstr[1] ""
msgid "Every week (%{weekday} at %{time})" msgid "Every week (%{weekday} at %{time})"
msgstr "" msgstr ""
msgid "Every week on %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every year" msgid "Every year"
msgstr "" msgstr ""
msgid "Every year on %{day} at %{time} %{timezone}"
msgstr ""
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr ""
......
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