Commit 5384f326 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '328753-scan-schedules' into 'master'

Add schedules info to the saved scans list

See merge request gitlab-org/gitlab!69895
parents 810ecc87 4e4d9fd7
......@@ -311,10 +311,10 @@ export default {
this.form.fields.name.value = name ?? this.form.fields.name.value;
this.form.fields.description.value = description ?? this.form.fields.description.value;
this.selectedBranch = selectedBranch;
this.profileSchedule = profileSchedule ?? this.profileSchedule;
// precedence is given to profile IDs passed from the query params
this.selectedSiteProfileId = this.selectedSiteProfileId ?? selectedSiteProfileId;
this.selectedScannerProfileId = this.selectedScannerProfileId ?? selectedScannerProfileId;
this.profileSchedule = this.profileSchedule ?? profileSchedule;
},
},
};
......@@ -454,7 +454,11 @@ export default {
: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
v-if="hasProfilesConflict"
......
......@@ -8,31 +8,7 @@ import {
} from '~/lib/utils/datetime/date_format_utility';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
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.
*/
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();
};
import { toGraphQLCadence, fromGraphQLCadence } from '../utils';
export default {
name: 'ScanSchedule',
......@@ -49,17 +25,17 @@ export default {
value: {
type: Object,
required: false,
default: () => ({}),
default: null,
},
},
data() {
return {
form: {
isScheduledScan: this.value.active ?? false,
selectedTimezone: this.value.timezone ?? null,
isScheduledScan: this.value?.active ?? false,
selectedTimezone: this.value?.timezone ?? null,
startDate: null,
startTime: null,
cadence: fromGraphQLCadence(this.value.cadence) ?? SCAN_CADENCE_OPTIONS[0].value,
cadence: fromGraphQLCadence(this.value?.cadence),
},
};
},
......@@ -79,7 +55,7 @@ export default {
},
},
created() {
const date = this.value.startsAt ?? null;
const date = this.value?.startsAt ?? null;
if (date !== null) {
const localeDate = new Date(
stripTimezoneFromISODate(date, this.selectedTimezoneData?.offset),
......
......@@ -39,10 +39,51 @@ const YEAR_1 = 'YEAR_1';
export const SCAN_CADENCE_OPTIONS = [
{ value: '', text: __('Never') },
{ value: DAY_1, text: __('Every day') },
{ value: WEEK_1, text: __('Every week') },
{ value: MONTH_1, text: __('Every month') },
{ value: MONTH_3, text: __('Every 3 months') },
{ value: MONTH_6, text: __('Every 6 months') },
{ value: YEAR_1, text: __('Every year') },
{
value: DAY_1,
text: __('Every day'),
description: {
text: __('Every day at %{time} %{timezone}'),
},
},
{
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';
import dastProfileRunMutation from '../graphql/dast_profile_run.mutation.graphql';
import ProfilesList from './dast_profiles_list.vue';
import DastScanBranch from './dast_scan_branch.vue';
import ScanSchedule from './dast_scan_schedule.vue';
import ScanTypeBadge from './dast_scan_type_badge.vue';
export default {
......@@ -14,6 +15,7 @@ export default {
GlButton,
ProfilesList,
DastScanBranch,
ScanSchedule,
ScanTypeBadge,
},
mixins: [glFeatureFlagsMixin()],
......@@ -115,6 +117,11 @@ export default {
<scan-type-badge :scan-type="value" />
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastProfileSchedule)="{ value }">
<scan-schedule :schedule="value || null" />
</template>
<template #actions="{ profile }">
<gl-button
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 () => {
newDastScannerProfilePath,
newDastSiteProfilePath,
projectFullPath,
timezones,
},
} = el;
......@@ -30,6 +31,9 @@ export default () => {
return new Vue({
el,
apolloProvider,
provide: {
timezones: JSON.parse(timezones),
},
render(h) {
return h(DastProfiles, {
props,
......
......@@ -18,6 +18,16 @@ query DastProfiles($fullPath: ID!, $after: String, $before: String, $first: Int,
id
scanType
}
dastProfileSchedule {
id
active
startsAt
timezone
cadence {
unit
duration
}
}
branch {
name
exists
......
......@@ -39,6 +39,10 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
label: s__('DastProfiles|Scan mode'),
key: 'dastScannerProfile.scanType',
},
{
label: s__('DastProfiles|Schedule'),
key: 'dastProfileSchedule',
},
],
i18n: {
createNewLinkText: s__('DastProfiles|DAST Scan'),
......
......@@ -6,7 +6,8 @@ module Projects::Security::DastProfilesHelper
'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_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
......@@ -96,7 +96,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[0]).toEqual([
{
active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value,
cadence: {},
startsAt: null,
timezone: null,
},
......@@ -111,7 +111,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[2]).toEqual([
{
active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value,
cadence: {},
startsAt: '2021-08-12T11:00:00.000Z',
timezone: null,
},
......@@ -126,7 +126,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[2]).toEqual([
{
active: true,
cadence: SCAN_CADENCE_OPTIONS[0].value,
cadence: {},
startsAt: null,
timezone: null,
},
......@@ -148,7 +148,7 @@ describe('ScanSchedule', () => {
expect(wrapper.emitted().input[1]).toEqual([
{
active: false,
cadence: SCAN_CADENCE_OPTIONS[0].value,
cadence: {},
startsAt: null,
timezone: null,
},
......@@ -157,26 +157,40 @@ describe('ScanSchedule', () => {
});
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({
propsData: {
value: {
active: true,
startsAt,
...schedule,
cadence: { unit: 'MONTH', duration: 1 },
timezone: timezoneSST.identifier,
},
},
});
});
it('initializes fields with provided values', () => {
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(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
'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_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
......
......@@ -10432,6 +10432,9 @@ msgstr ""
msgid "DastProfiles|Scanner name"
msgstr ""
msgid "DastProfiles|Schedule"
msgstr ""
msgid "DastProfiles|Select branch"
msgstr ""
......@@ -13525,21 +13528,33 @@ msgstr ""
msgid "Every 3 months"
msgstr ""
msgid "Every 3 months on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every 6 months"
msgstr ""
msgid "Every 6 months on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every day"
msgstr ""
msgid "Every day (at %{time})"
msgstr ""
msgid "Every day at %{time} %{timezone}"
msgstr ""
msgid "Every month"
msgstr ""
msgid "Every month (Day %{day} at %{time})"
msgstr ""
msgid "Every month on the %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every three months"
msgstr ""
......@@ -13554,9 +13569,15 @@ msgstr[1] ""
msgid "Every week (%{weekday} at %{time})"
msgstr ""
msgid "Every week on %{day} at %{time} %{timezone}"
msgstr ""
msgid "Every year"
msgstr ""
msgid "Every year on %{day} at %{time} %{timezone}"
msgstr ""
msgid "Everyone"
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