Commit fa4d1a20 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '331395-tooltip-for-cron-syntax' into 'master'

Add cron syntax daily limit message

See merge request gitlab-org/gitlab!63425
parents c61122f9 0df34a8b
<script>
import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui';
import {
GlFormRadio,
GlFormRadioGroup,
GlIcon,
GlLink,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const KEY_EVERY_DAY = 'everyDay';
const KEY_EVERY_WEEK = 'everyWeek';
......@@ -12,15 +20,25 @@ export default {
components: {
GlFormRadio,
GlFormRadioGroup,
GlIcon,
GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
dailyLimit: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -80,6 +98,17 @@ export default {
weekday() {
return getWeekdayNames()[this.randomWeekDayIndex];
},
parsedDailyLimit() {
return this.dailyLimit ? (24 * 60) / this.dailyLimit : null;
},
scheduleDailyLimitMsg() {
return sprintf(
__(
'Scheduled pipelines cannot run more frequently than once per %{limit} minutes. A pipeline configured to run more frequently only starts after %{limit} minutes have elapsed since the last time it ran.',
),
{ limit: this.parsedDailyLimit },
);
},
},
watch: {
cronInterval() {
......@@ -111,6 +140,11 @@ export default {
generateRandomDay() {
return Math.floor(Math.random() * 28);
},
showDailyLimitMessage({ value }) {
return (
value === KEY_CUSTOM && this.glFeatures.ciDailyLimitForPipelineSchedules && this.dailyLimit
);
},
},
};
</script>
......@@ -131,7 +165,15 @@ export default {
</gl-link>
</template>
</gl-sprintf>
<template v-else>{{ option.text }}</template>
<gl-icon
v-if="showDailyLimitMessage(option)"
v-gl-tooltip.hover
name="question"
:title="scheduleDailyLimitMsg"
/>
</gl-form-radio>
</gl-form-radio-group>
<input
......
......@@ -12,6 +12,7 @@ Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
return new Vue({
el: intervalPatternMount,
......@@ -22,6 +23,7 @@ function initIntervalPatternInput() {
return createElement('interval-pattern-input', {
props: {
initialCronInterval,
dailyLimit,
},
});
},
......
......@@ -10,6 +10,10 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action do
push_frontend_feature_flag(:ci_daily_limit_for_pipeline_schedules, @project, default_enabled: :yaml)
end
feature_category :continuous_integration
# rubocop: disable CodeReuse/ActiveRecord
......
......@@ -63,6 +63,10 @@ module Ci
.execute(self, fallback_method: method(:calculate_next_run_at))
end
def daily_limit
project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers)
end
private
def worker_cron_expression
......
......@@ -41,11 +41,11 @@ module Ci
def plan_cron
strong_memoize(:plan_cron) do
daily_scheduled_pipeline_limit = project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers)
daily_limit = @schedule.daily_limit
next unless daily_scheduled_pipeline_limit
next unless daily_limit
every_x_minutes = (1.day.in_minutes / daily_scheduled_pipeline_limit).to_i
every_x_minutes = (1.day.in_minutes / daily_limit).to_i
Gitlab::Ci::CronParser.parse_natural("every #{every_x_minutes} minutes", Time.zone.name)
end
......
......@@ -7,7 +7,7 @@
.form-group.row
.col-md-9
= f.label :cron, _('Interval Pattern'), class: 'label-bold'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
#interval-pattern-input{ data: { initial_interval: @schedule.cron, daily_limit: @schedule.daily_limit } }
.form-group.row
.col-md-9
= f.label :cron_timezone, _('Cron Timezone'), class: 'label-bold'
......
......@@ -28441,6 +28441,9 @@ msgstr ""
msgid "Scheduled a rebase of branch %{branch}."
msgstr ""
msgid "Scheduled pipelines cannot run more frequently than once per %{limit} minutes. A pipeline configured to run more frequently only starts after %{limit} minutes have elapsed since the last time it ran."
msgstr ""
msgid "Scheduled to merge this merge request (%{strategy})."
msgstr ""
......
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
......@@ -27,6 +28,7 @@ describe('Interval Pattern Input Component', () => {
const findAllLabels = () => wrapper.findAll('label');
const findSelectedRadio = () =>
wrapper.findAll('input[type="radio"]').wrappers.find((x) => x.element.checked);
const findIcon = () => wrapper.findComponent(GlIcon);
const findSelectedRadioKey = () => findSelectedRadio()?.attributes('data-testid');
const selectEveryDayRadio = () => findEveryDayRadio().trigger('click');
const selectEveryWeekRadio = () => findEveryWeekRadio().trigger('click');
......@@ -40,6 +42,11 @@ describe('Interval Pattern Input Component', () => {
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
provide: {
glFeatures: {
ciDailyLimitForPipelineSchedules: true,
},
},
data() {
return {
randomHour: data?.hour || mockHour,
......@@ -202,4 +209,24 @@ describe('Interval Pattern Input Component', () => {
expect(findSelectedRadioKey()).toBe(customKey);
});
});
describe('Custom cron syntax quota info', () => {
it('the info message includes 5 minutes', () => {
createWrapper({ dailyLimit: '288' });
expect(findIcon().attributes('title')).toContain('5 minutes');
});
it('the info message includes 60 minutes', () => {
createWrapper({ dailyLimit: '24' });
expect(findIcon().attributes('title')).toContain('60 minutes');
});
it('the info message icon is not shown when there is no daily limit', () => {
createWrapper();
expect(findIcon().exists()).toBe(false);
});
});
});
......@@ -122,6 +122,9 @@ RSpec.describe Ci::PipelineSchedule do
'*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
'*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
'*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 2.hours.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
'*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
'*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
'*/5 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0)
end
with_them do
......@@ -198,4 +201,26 @@ RSpec.describe Ci::PipelineSchedule do
it { is_expected.to contain_exactly(*pipeline_schedule_variables.map(&:to_runner_variable)) }
end
describe '#daily_limit' do
let(:pipeline_schedule) { build(:ci_pipeline_schedule) }
subject(:daily_limit) { pipeline_schedule.daily_limit }
context 'when there is no limit' do
before do
create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 0)
end
it { is_expected.to be_nil }
end
context 'when there is a limit' do
before do
create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 144)
end
it { is_expected.to eq(144) }
end
end
end
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