diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/constants.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/constants.js index 51aea317faeb146caf95cdb944980a85afb4878d..99ca39fd4431f0e1b4cfcab35dfafb71ae3802dd 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/constants.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/constants.js @@ -62,6 +62,18 @@ content: - project: '' `; +export const HOUR_IN_SECONDS = 3600; +export const DAILY = 'daily'; +export const DEFAULT_SCHEDULE = { + type: DAILY, + start_time: '00:00', + time_window: { + value: HOUR_IN_SECONDS, + distribution: 'random', + }, + timezone: 'America/New_York', +}; + export const CONDITIONS_LABEL = s__('ScanExecutionPolicy|Conditions'); export const SCHEDULE = 'schedule'; diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue index 61f0cf0a31125900eac6f6c1570521de0a8f260b..2b9aad58c2783ceb4799997fa4b22df2f0b1a799 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/editor_component.vue @@ -22,7 +22,12 @@ import DisabledSection from '../disabled_section.vue'; import ActionSection from './action/action_section.vue'; import RuleSection from './rule/rule_section.vue'; import { createPolicyObject, getInitialPolicy } from './utils'; -import { CONDITIONS_LABEL, DEFAULT_PIPELINE_EXECUTION_POLICY, SCHEDULE } from './constants'; +import { + CONDITIONS_LABEL, + DEFAULT_SCHEDULE, + DEFAULT_PIPELINE_EXECUTION_POLICY, + SCHEDULE, +} from './constants'; export default { ACTION: 'actions', @@ -118,6 +123,9 @@ export default { content() { return this.policy?.content || {}; }, + schedules() { + return this.policy?.schedules; + }, }, watch: { content(newVal) { @@ -169,6 +177,10 @@ export default { this.disableSubmit = true; } }, + handleUpdateSchedules(schedule) { + this.policy = { ...this.policy, schedules: [schedule] }; + this.updateYamlEditorValue(this.policy); + }, handleUpdateProperty(property, value) { this.policy[property] = value; this.updateYamlEditorValue(this.policy); @@ -177,7 +189,7 @@ export default { this.handleUpdateProperty('pipeline_config_strategy', value); if (value === SCHEDULE) { - this.policy = { ...this.policy, schedules: {} }; + this.policy = { ...this.policy, schedules: [DEFAULT_SCHEDULE] }; } else { const { schedules, ...policy } = this.policy; this.policy = policy; @@ -222,7 +234,12 @@ export default { <template #title> <h4>{{ $options.i18n.CONDITIONS_LABEL }}</h4> </template> - <rule-section class="gl-mb-4" :strategy="strategy" /> + <rule-section + class="gl-mb-4" + :strategy="strategy" + :schedules="schedules" + @changed="handleUpdateSchedules" + /> </disabled-section> </template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section.vue index e4c6b90f13bbd40d03ea0b33e60b2e04fa382ec0..d45e1a06de3ff36d3618ec27e43ecc52206f87d5 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section.vue @@ -3,7 +3,7 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { INJECT, SCHEDULE } from '../constants'; +import { DEFAULT_SCHEDULE, INJECT, SCHEDULE } from '../constants'; import ScheduleForm from './schedule_form.vue'; export default { @@ -20,6 +20,11 @@ export default { }, mixins: [glFeatureFlagsMixin()], props: { + schedules: { + type: Array, + required: false, + default: () => [DEFAULT_SCHEDULE], + }, strategy: { type: String, required: false, @@ -30,10 +35,18 @@ export default { hasScheduledPipelines() { return this.glFeatures.scheduledPipelineExecutionPolicies; }, + schedule() { + return this.schedules[0]; + }, showTriggeredMessage() { return !this.hasScheduledPipelines || this.strategy !== SCHEDULE; }, }, + methods: { + handleUpdateSchedules(schedule) { + this.$emit('changed', schedule); + }, + }, }; </script> @@ -46,6 +59,6 @@ export default { <gl-link :href="$options.i18n.helpPageLink" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> - <schedule-form v-else /> + <schedule-form v-else :schedule="schedule" @changed="handleUpdateSchedules" /> </div> </template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue index c0d060306bee378910ace29bf2f42063fd460952..5835ac16ad9a64d8e8f698aef90ae7321f434892 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue @@ -1,16 +1,47 @@ <script> -import { s__ } from '~/locale'; +import { GlCollapsibleListbox, GlSprintf } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import { CADENCE_OPTIONS, updateScheduleCadence } from './utils'; export default { name: 'ScheduleForm', + CADENCE_OPTIONS, i18n: { - message: s__('SecurityOrchestration|Schedules'), + cadence: __('Cadence'), + message: s__( + 'SecurityOrchestration|Schedule a pipeline on a %{cadenceSelector} cadence for branches', + ), + }, + components: { + GlCollapsibleListbox, + GlSprintf, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + methods: { + updateCadence(value) { + const updatedSchedule = updateScheduleCadence({ schedule: this.schedule, cadence: value }); + this.$emit('changed', updatedSchedule); + }, }, }; </script> <template> - <div> - {{ $options.i18n.message }} + <div class="gl-flex gl-flex-wrap gl-items-center gl-gap-3"> + <gl-sprintf :message="$options.i18n.message"> + <template #cadenceSelector> + <gl-collapsible-listbox + :aria-label="$options.i18n.cadence" + :items="$options.CADENCE_OPTIONS" + :selected="schedule.type" + @select="updateCadence" + /> + </template> + </gl-sprintf> </div> </template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/utils.js b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..9079d13f775eceed9d816494bb4664047a8144f6 --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/pipeline_execution/rule/utils.js @@ -0,0 +1,47 @@ +import { __ } from '~/locale'; +import { DAILY, HOUR_IN_SECONDS } from '../constants'; + +const DAY_IN_SECONDS = HOUR_IN_SECONDS * 24; +const DEFAULT_START_TIME = '00:00'; +const DEFAULT_START_WEEKDAY = 'monday'; +const DEFAULT_START_MONTH_DAY = '1'; +const WEEKLY = 'weekly'; +const MONTHLY = 'monthly'; + +export const CADENCE_OPTIONS = [ + { value: DAILY, text: __('Daily') }, + { value: WEEKLY, text: __('Weekly') }, + { value: MONTHLY, text: __('Monthly') }, +]; + +const CADENCE_CONFIG = { + [DAILY]: { + start_time: DEFAULT_START_TIME, + time_window: { value: HOUR_IN_SECONDS }, + }, + [WEEKLY]: { + days: DEFAULT_START_WEEKDAY, + time_window: { value: DAY_IN_SECONDS }, + }, + [MONTHLY]: { + days_of_month: DEFAULT_START_MONTH_DAY, + time_window: { value: DAY_IN_SECONDS }, + }, +}; + +export const updateScheduleCadence = ({ schedule, cadence }) => { + const { start_time, days, days_of_month, ...updatedSchedule } = schedule; + updatedSchedule.type = cadence; + + if (CADENCE_CONFIG[cadence]) { + Object.assign(updatedSchedule, { + ...CADENCE_CONFIG[cadence], + time_window: { + ...updatedSchedule.time_window, + value: CADENCE_CONFIG[cadence].time_window.value, + }, + }); + } + + return updatedSchedule; +}; diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js index 9674291649c50d0724175e960f8a620a0431a890..e7d75915feedb5d74e82785a1bbe43d77e70766c 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/editor_component_spec.js @@ -9,6 +9,9 @@ import EditorLayout from 'ee/security_orchestration/components/policy_editor/edi import SkipCiSelector from 'ee/security_orchestration/components/policy_editor/skip_ci_selector.vue'; import { DEFAULT_PIPELINE_EXECUTION_POLICY, + DEFAULT_SCHEDULE, + INJECT, + SCHEDULE, SUFFIX_NEVER, SUFFIX_ON_CONFLICT, } from 'ee/security_orchestration/components/policy_editor/pipeline_execution/constants'; @@ -50,6 +53,7 @@ describe('EditorComponent', () => { const policyEditorEmptyStateSvgPath = 'path/to/svg'; const scanPolicyDocumentationPath = 'path/to/docs'; const defaultProjectPath = 'path/to/project'; + const defaultSchedules = [{ type: 'weekly', days: 'monday' }]; const factory = ({ propsData = {}, provide = {} } = {}) => { wrapper = shallowMountExtended(EditorComponent, { @@ -185,7 +189,30 @@ describe('EditorComponent', () => { describe('rule section', () => { it('passes the strategy to rule section', () => { factory(); - expect(findRuleSection().props('strategy')).toBe('inject_policy'); + expect(findRuleSection().props('strategy')).toBe(INJECT); + }); + + it('passes schedules prop', () => { + factoryWithExistingPolicy({ policy: { schedules: defaultSchedules } }); + expect(findRuleSection().props('schedules')).toEqual(defaultSchedules); + }); + + it('updates "schedules" in policy', async () => { + factory(); + expect(findPolicyEditorLayout().props('policy')).not.toContain('schedules'); + await findRuleSection().vm.$emit('changed', defaultSchedules[0]); + expect(findPolicyEditorLayout().props('policy')).toEqual( + expect.objectContaining({ schedules: [defaultSchedules[0]] }), + ); + }); + + it('updates "schedules" in YAML', async () => { + factory(); + expect(findPolicyEditorLayout().props('yamlEditorValue')).not.toContain('schedules'); + await findRuleSection().vm.$emit('changed', defaultSchedules[0]); + expect(findPolicyEditorLayout().props('yamlEditorValue')).toContain( + ' schedules:\n - type: weekly\n days: monday', + ); }); }); @@ -193,18 +220,21 @@ describe('EditorComponent', () => { it('adds "schedules" property if strategy is updated to "schedule"', async () => { factory(); expect(findPolicyEditorLayout().props('policy')).not.toHaveProperty('schedules'); - await findActionSection().vm.$emit('update-strategy', 'schedule'); + await findActionSection().vm.$emit('update-strategy', SCHEDULE); expect(findPolicyEditorLayout().props('policy')).toEqual( - expect.objectContaining({ pipeline_config_strategy: 'schedule', schedules: {} }), + expect.objectContaining({ + pipeline_config_strategy: SCHEDULE, + schedules: [DEFAULT_SCHEDULE], + }), ); }); it('removes "schedules" property if strategy is updated to "inject_policy" from "schedule"', async () => { factory(); - await findActionSection().vm.$emit('update-strategy', 'schedule'); - await findActionSection().vm.$emit('update-strategy', 'inject_policy'); + await findActionSection().vm.$emit('update-strategy', SCHEDULE); + await findActionSection().vm.$emit('update-strategy', INJECT); expect(findPolicyEditorLayout().props('policy')).toEqual( - expect.objectContaining({ pipeline_config_strategy: 'inject_policy' }), + expect.objectContaining({ pipeline_config_strategy: INJECT }), ); expect(findPolicyEditorLayout().props('policy')).not.toHaveProperty('schedules'); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section_spec.js index 57384eac450f3edf7cbac9d02652fde16aad9429..f0553b80ea771da25f663086395c629d3a86093b 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section_spec.js @@ -1,5 +1,10 @@ import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + DEFAULT_SCHEDULE, + INJECT, + SCHEDULE, +} from 'ee/security_orchestration/components/policy_editor/pipeline_execution/constants'; import RuleSection from 'ee/security_orchestration/components/policy_editor/pipeline_execution/rule/rule_section.vue'; import ScheduleForm from 'ee/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue'; @@ -23,30 +28,56 @@ describe('RuleSection', () => { describe('rendering', () => { describe('when feature flag is off', () => { it('renders inject/override message when schedule is not selected', () => { - createComponent({ propsData: { strategy: 'inject' } }); + createComponent({ propsData: { strategy: INJECT } }); expect(wrapper.findComponent(GlSprintf).exists()).toBe(true); expect(findScheduleForm().exists()).toBe(false); }); }); describe('when feature flag is on', () => { - it('renders schedule form when schedule is selected', () => { - createComponent({ - propsData: { strategy: 'schedule' }, - provide: { glFeatures: { scheduledPipelineExecutionPolicies: true } }, - }); - expect(wrapper.findComponent(GlSprintf).exists()).toBe(false); - expect(findScheduleForm().exists()).toBe(true); - }); - it('renders inject/override message when schedule is not selected', () => { createComponent({ - propsData: { strategy: 'inject' }, + propsData: { strategy: INJECT }, provide: { glFeatures: { scheduledPipelineExecutionPolicies: true } }, }); expect(wrapper.findComponent(GlSprintf).exists()).toBe(true); expect(findScheduleForm().exists()).toBe(false); }); + + describe('schedule form', () => { + it('renders schedule form when schedule is selected', () => { + createComponent({ + propsData: { strategy: SCHEDULE }, + provide: { glFeatures: { scheduledPipelineExecutionPolicies: true } }, + }); + expect(wrapper.findComponent(GlSprintf).exists()).toBe(false); + expect(findScheduleForm().exists()).toBe(true); + expect(findScheduleForm().props('schedule')).toEqual(DEFAULT_SCHEDULE); + }); + + it('passes schedule prop to ScheduleForm component', () => { + const customSchedule = { type: 'weekly', days: 'monday' }; + createComponent({ + propsData: { schedules: [customSchedule], strategy: SCHEDULE }, + provide: { glFeatures: { scheduledPipelineExecutionPolicies: true } }, + }); + + expect(findScheduleForm().props(SCHEDULE)).toEqual(customSchedule); + }); + + it('listens for changed event from schedule form', async () => { + createComponent({ + propsData: { strategy: SCHEDULE }, + provide: { glFeatures: { scheduledPipelineExecutionPolicies: true } }, + }); + + const updatedSchedule = { type: 'monthly', days_of_month: '15' }; + await findScheduleForm().vm.$emit('changed', updatedSchedule); + + expect(wrapper.emitted('changed')).toHaveLength(1); + expect(wrapper.emitted('changed')[0][0]).toEqual(updatedSchedule); + }); + }); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form_spec.js index c3ddf33c5524ee31d66ede357137a0f967dc7bd3..5f057596b6b0a7204ac396a1cc9c2abda5ab0ae5 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form_spec.js @@ -1,20 +1,96 @@ import { shallowMount } from '@vue/test-utils'; +import { GlCollapsibleListbox, GlSprintf } from '@gitlab/ui'; import ScheduleForm from 'ee/security_orchestration/components/policy_editor/pipeline_execution/rule/schedule_form.vue'; describe('ScheduleForm', () => { let wrapper; + const defaultSchedule = { type: 'daily', time_window: { value: 3600 } }; - const createComponent = () => { - wrapper = shallowMount(ScheduleForm); + const createComponent = (props = {}) => { + wrapper = shallowMount(ScheduleForm, { + propsData: { schedule: defaultSchedule, ...props }, + stubs: { GlSprintf }, + }); }; - it('renders the component', () => { - createComponent(); - expect(wrapper.exists()).toBe(true); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + describe('rendering', () => { + it('renders the component', () => { + createComponent(); + expect(wrapper.exists()).toBe(true); + }); + + it('displays the correct message', () => { + createComponent(); + expect(wrapper.text()).toContain('Schedule a pipeline on a cadence for branches'); + }); + + it('renders the cadence selector with correct options', () => { + createComponent(); + const listbox = findListbox(); + expect(listbox.exists()).toBe(true); + expect(listbox.props('items')).toEqual([ + { value: 'daily', text: 'Daily' }, + { value: 'weekly', text: 'Weekly' }, + { value: 'monthly', text: 'Monthly' }, + ]); + }); + + it('sets the selected value based on schedule prop', () => { + createComponent({ schedule: { type: 'weekly' } }); + expect(findListbox().props('selected')).toBe('weekly'); + }); }); - it('displays the correct message', () => { - createComponent(); - expect(wrapper.text()).toBe('Schedules'); + describe('updateCadence', () => { + it('emits changed event with daily schedule when daily is selected', async () => { + createComponent(); + await findListbox().vm.$emit('select', 'daily'); + + expect(wrapper.emitted('changed')).toHaveLength(1); + expect(wrapper.emitted('changed')).toMatchObject([ + [{ type: 'daily', start_time: '00:00', time_window: { value: 3600 } }], + ]); + }); + + it('emits changed event with weekly schedule when weekly is selected', async () => { + createComponent(); + await findListbox().vm.$emit('select', 'weekly'); + + expect(wrapper.emitted('changed')).toHaveLength(1); + expect(wrapper.emitted('changed')).toMatchObject([ + [{ type: 'weekly', days: 'monday', time_window: { value: 86400 } }], + ]); + }); + + it('emits changed event with monthly schedule when monthly is selected', async () => { + createComponent(); + await findListbox().vm.$emit('select', 'monthly'); + + expect(wrapper.emitted('changed')).toHaveLength(1); + expect(wrapper.emitted('changed')).toMatchObject([ + [{ type: 'monthly', days_of_month: '1', time_window: { value: 86400 } }], + ]); + }); + + it('removes irrelevant properties when changing cadence type', async () => { + createComponent({ + schedule: { + type: 'daily', + start_time: '12:00', + days: 'friday', + days_of_month: '15', + time_window: { value: 3600 }, + }, + }); + + await findListbox().vm.$emit('select', 'weekly'); + + const emittedSchedule = wrapper.emitted('changed')[0][0]; + expect(emittedSchedule).toHaveProperty('days'); + expect(emittedSchedule).not.toHaveProperty('start_time'); + expect(emittedSchedule).not.toHaveProperty('days_of_month'); + }); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/utils_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/utils_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4854281038d5875a4548b10301fc618e7a161c41 --- /dev/null +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/pipeline_execution/rule/utils_spec.js @@ -0,0 +1,82 @@ +import { updateScheduleCadence } from 'ee/security_orchestration/components/policy_editor/pipeline_execution/rule/utils'; + +describe('Pipeline execution rule utils', () => { + describe('updateScheduleCadence', () => { + const baseSchedule = { + time_window: { value: 3600, distribution: 'random' }, + timezone: 'America/New_York', + }; + + const dailySchedule = { + type: 'daily', + start_time: '00:00', + ...baseSchedule, + }; + + const weeklySchedule = { + type: 'weekly', + days: 'monday', + ...baseSchedule, + }; + + const monthlySchedule = { + type: 'monthly', + days_of_month: '1', + ...baseSchedule, + }; + + it('updates to daily cadence correctly', () => { + expect(updateScheduleCadence({ schedule: weeklySchedule, cadence: 'daily' })).toEqual( + expect.objectContaining({ + ...dailySchedule, + time_window: { value: 3600, distribution: 'random' }, + }), + ); + }); + + it('updates to weekly cadence correctly', () => { + expect(updateScheduleCadence({ schedule: monthlySchedule, cadence: 'weekly' })).toEqual( + expect.objectContaining({ + ...weeklySchedule, + time_window: { value: 86400, distribution: 'random' }, + }), + ); + }); + + it('updates to monthly cadence correctly', () => { + expect(updateScheduleCadence({ schedule: dailySchedule, cadence: 'monthly' })).toEqual( + expect.objectContaining({ + ...monthlySchedule, + time_window: { value: 86400, distribution: 'random' }, + }), + ); + }); + + it('removes irrelevant properties when changing cadence type', () => { + const result = updateScheduleCadence({ + schedule: baseSchedule, + cadence: 'weekly', + }); + + expect(result).toHaveProperty('days'); + expect(result).not.toHaveProperty('start_time'); + expect(result).not.toHaveProperty('days_of_month'); + }); + + it('preserves additional properties not related to cadence', () => { + const scheduleWithExtra = { + ...baseSchedule, + custom_property: 'value', + another_property: 123, + }; + + const result = updateScheduleCadence({ + schedule: scheduleWithExtra, + cadence: 'weekly', + }); + + expect(result.custom_property).toBe('value'); + expect(result.another_property).toBe(123); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d852766897b1b525af36429030c34d60c76d3415..a1e280fc630f4c2ee0f8937672a685a46a5c050f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11455,6 +11455,9 @@ msgstr "" msgid "CVS|Only a project maintainer or owner can toggle this feature." msgstr "" +msgid "Cadence" +msgstr "" + msgid "Cadence is not automated" msgstr "" @@ -18393,6 +18396,9 @@ msgstr "" msgid "DSN" msgstr "" +msgid "Daily" +msgstr "" + msgid "Dashboard" msgstr "" @@ -37272,6 +37278,9 @@ msgstr "" msgid "Month" msgstr "" +msgid "Monthly" +msgstr "" + msgid "Months" msgstr "" @@ -52857,7 +52866,7 @@ msgstr "" msgid "SecurityOrchestration|Schedule a new" msgstr "" -msgid "SecurityOrchestration|Schedules" +msgid "SecurityOrchestration|Schedule a pipeline on a %{cadenceSelector} cadence for branches" msgstr "" msgid "SecurityOrchestration|Scope" @@ -65282,6 +65291,9 @@ msgstr "" msgid "Weekday" msgstr "" +msgid "Weekly" +msgstr "" + msgid "Weeks" msgstr ""