Skip to content

Commit 77b6ba3

Browse files
author
ci bot
committed
Merge branch 'cron-starting-point' into 'enterprise'
feat(ui): add starting at/on offset to cron schedule editor See merge request dkinternal/testgen/dataops-testgen!398
2 parents fb96c0b + b975f6f commit 77b6ba3

5 files changed

Lines changed: 237 additions & 29 deletions

File tree

testgen/ui/components/frontend/js/components/crontab_input.js

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* @property {CronSample?} sample
2222
* @property {InitialValue?} value
2323
* @property {('x_hours'|'x_days'|'certain_days'|'custom'))[]?} modes
24+
* @property {boolean?} hideExpression
2425
* @property {((expr: string) => void)?} onChange
2526
*/
2627
import { getRandomId, getValue, loadStylesheet } from '../utils.js';
@@ -92,6 +93,7 @@ const CrontabInput = (/** @type Options */ props) => {
9293
onClose: () => opened.val = false,
9394
sample: props.sample,
9495
modes: props.modes,
96+
hideExpression: props.hideExpression,
9597
},
9698
expression,
9799
),
@@ -110,11 +112,13 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
110112
const xHoursState = {
111113
hours: van.state(1),
112114
minute: van.state(0),
115+
startHour: van.state(0),
113116
};
114117
const xDaysState = {
115118
days: van.state(1),
116119
hour: van.state(1),
117120
minute: van.state(0),
121+
startDay: van.state(1),
118122
};
119123
const certainDaysState = {
120124
sunday: van.state(false),
@@ -135,12 +139,30 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
135139
if (mode.val === 'x_hours') {
136140
const hours = xHoursState.hours.val;
137141
const minute = xHoursState.minute.val;
138-
options.onChange(`${minute ?? 0} ${(hours && hours !== 1) ? '*/' + hours : '*'} * * *`);
142+
const startHour = xHoursState.startHour.val;
143+
let hourField;
144+
if (!hours || hours <= 1) {
145+
hourField = '*';
146+
} else if (startHour > 0) {
147+
hourField = generateSteppedValues(startHour, hours, 23);
148+
} else {
149+
hourField = '*/' + hours;
150+
}
151+
options.onChange(`${minute ?? 0} ${hourField} * * *`);
139152
} else if (mode.val === 'x_days') {
140153
const days = xDaysState.days.val;
141154
const hour = xDaysState.hour.val;
142155
const minute = xDaysState.minute.val;
143-
options.onChange(`${minute ?? 0} ${hour ?? 0} ${(days && days !== 1) ? '*/' + days : '*'} * *`);
156+
const startDay = xDaysState.startDay.val;
157+
let dayField;
158+
if (!days || days <= 1) {
159+
dayField = '*';
160+
} else if (startDay > 1) {
161+
dayField = generateSteppedValues(startDay, days, 31);
162+
} else {
163+
dayField = '*/' + days;
164+
}
165+
options.onChange(`${minute ?? 0} ${hour ?? 0} ${dayField} * *`);
144166
} else if (mode.val === 'certain_days') {
145167
const days = [];
146168
const dayMap = [
@@ -225,16 +247,19 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
225247
span({}, 'Every'),
226248
() => Select({
227249
label: "",
228-
options: Array.from({length: 24}, (_, i) => i).map(i => ({label: i.toString(), value: i})),
250+
options: Array.from({length: 24}, (_, i) => i + 1).map(i => ({label: i.toString(), value: i})),
229251
triggerStyle: 'inline',
230252
portalClass: 'tg-crontab--select-portal',
231253
value: xHoursState.hours,
232-
onChange: (value) => xHoursState.hours.val = value,
254+
onChange: (value) => {
255+
xHoursState.hours.val = value;
256+
if (value <= 1) xHoursState.startHour.val = 0;
257+
},
233258
}),
234259
span({}, 'hours'),
235260
),
236261
div(
237-
{class: 'flex-row fx-gap-2'},
262+
{class: () => `flex-row fx-gap-2 ${xHoursState.hours.val > 1 ? 'mb-2' : ''}`},
238263
span({}, 'on'),
239264
span({}, 'minute'),
240265
() => Select({
@@ -246,6 +271,18 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
246271
onChange: (value) => xHoursState.minute.val = value,
247272
}),
248273
),
274+
div(
275+
{class: () => `flex-row fx-gap-2 ${xHoursState.hours.val > 1 ? '' : 'hidden'}`},
276+
span({}, 'starting at hour'),
277+
() => Select({
278+
label: "",
279+
options: Array.from({length: 24}, (_, i) => i).map(i => ({label: i.toString(), value: i})),
280+
triggerStyle: 'inline',
281+
portalClass: 'tg-crontab--select-portal',
282+
value: xHoursState.startHour,
283+
onChange: (value) => xHoursState.startHour.val = value,
284+
}),
285+
),
249286
),
250287
div(
251288
{ class: () => `${mode.val === 'x_days' ? '' : 'hidden'}`},
@@ -258,12 +295,15 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
258295
triggerStyle: 'inline',
259296
portalClass: 'tg-crontab--select-portal',
260297
value: xDaysState.days,
261-
onChange: (value) => xDaysState.days.val = value,
298+
onChange: (value) => {
299+
xDaysState.days.val = value;
300+
if (value <= 1) xDaysState.startDay.val = 1;
301+
},
262302
}),
263303
span({}, 'days'),
264304
),
265305
div(
266-
{class: 'flex-row fx-gap-2'},
306+
{class: () => `flex-row fx-gap-2 ${xDaysState.days.val > 1 ? 'mb-2' : ''}`},
267307
span({}, 'at'),
268308
() => Select({
269309
label: "",
@@ -282,6 +322,18 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
282322
onChange: (value) => xDaysState.minute.val = value,
283323
}),
284324
),
325+
div(
326+
{class: () => `flex-row fx-gap-2 ${xDaysState.days.val > 1 ? '' : 'hidden'}`},
327+
span({}, 'starting on day'),
328+
() => Select({
329+
label: "",
330+
options: Array.from({length: 31}, (_, i) => i + 1).map(i => ({label: i.toString(), value: i})),
331+
triggerStyle: 'inline',
332+
portalClass: 'tg-crontab--select-portal',
333+
value: xDaysState.startDay,
334+
onChange: (value) => xDaysState.startDay.val = value,
335+
}),
336+
),
285337
),
286338
div(
287339
{ class: () => `${mode.val === 'certain_days' ? '' : 'hidden'}`},
@@ -370,7 +422,7 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
370422
div(
371423
{class: 'flex-column fx-gap-1 mt-3 text-secondary'},
372424
() => span(
373-
{ class: mode.val === 'custom' ? 'hidden': '' },
425+
{ class: mode.val === 'custom' || getValue(options.hideExpression) ? 'hidden': '' },
374426
`Cron Expression: ${expr.val ?? ''}`,
375427
),
376428
() => div(
@@ -409,6 +461,25 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
409461
);
410462
};
411463

464+
function generateSteppedValues(start, step, max) {
465+
const values = [];
466+
for (let i = start; i <= max; i += step) {
467+
values.push(i);
468+
}
469+
return values.join(',');
470+
}
471+
472+
function parseSteppedList(field) {
473+
const values = field.split(',').map(Number);
474+
if (values.length < 2 || values.some(isNaN)) return null;
475+
const step = values[1] - values[0];
476+
if (step <= 0) return null;
477+
for (let i = 2; i < values.length; i++) {
478+
if (values[i] - values[i - 1] !== step) return null;
479+
}
480+
return { start: values[0], step };
481+
}
482+
412483
/**
413484
* Populates the state variables for the initial mode based on the cron expression
414485
* @param {string} expr
@@ -420,21 +491,35 @@ const CrontabEditorPortal = ({sample, ...options}, expr) => {
420491
function populateInitialModeState(expr, mode, xHoursState, xDaysState, certainDaysState) {
421492
const parts = (expr || '').trim().split(/\s+/);
422493
if (mode === 'x_hours' && parts.length === 5) {
423-
// e.g. "M */H * * *" or "M * * * *"
424494
xHoursState.minute.val = Number(parts[0]) || 0;
425495
if (parts[1].startsWith('*/')) {
426496
xHoursState.hours.val = Number(parts[1].slice(2)) || 1;
497+
xHoursState.startHour.val = 0;
498+
} else if (parts[1].includes(',')) {
499+
const parsed = parseSteppedList(parts[1]);
500+
if (parsed) {
501+
xHoursState.hours.val = parsed.step;
502+
xHoursState.startHour.val = parsed.start;
503+
}
427504
} else {
428505
xHoursState.hours.val = 1;
506+
xHoursState.startHour.val = 0;
429507
}
430508
} else if (mode === 'x_days' && parts.length === 5) {
431-
// e.g. "M H */D * *" or "M H * * *"
432509
xDaysState.minute.val = Number(parts[0]) || 0;
433510
xDaysState.hour.val = Number(parts[1]) || 0;
434511
if (parts[2].startsWith('*/')) {
435512
xDaysState.days.val = Number(parts[2].slice(2)) || 1;
513+
xDaysState.startDay.val = 1;
514+
} else if (parts[2].includes(',')) {
515+
const parsed = parseSteppedList(parts[2]);
516+
if (parsed) {
517+
xDaysState.days.val = parsed.step;
518+
xDaysState.startDay.val = parsed.start;
519+
}
436520
} else {
437521
xDaysState.days.val = 1;
522+
xDaysState.startDay.val = 1;
438523
}
439524
} else if (mode === 'certain_days' && parts.length === 5) {
440525
// e.g. "M H * * DAY[,DAY...]"
@@ -465,14 +550,22 @@ function populateInitialModeState(expr, mode, xHoursState, xDaysState, certainDa
465550
function determineMode(expression) {
466551
// Normalize whitespace
467552
const expr = (expression || '').trim().replace(/\s+/g, ' ');
468-
// x_hours: "M */H * * *" or "M * * * *"
553+
// x_hours: "M */H * * *" or "M * * * *" or "M H1,H2,... * * *"
469554
if (/^\d{1,2} \*\/\d+ \* \* \*$/.test(expr) || /^\d{1,2} \* \* \* \*$/.test(expr)) {
470555
return 'x_hours';
471556
}
472-
// x_days: "M H */D * *" or "M H * * *"
557+
if (/^\d{1,2} \d+(,\d+)+ \* \* \*$/.test(expr)) {
558+
const hourField = expr.split(' ')[1];
559+
if (parseSteppedList(hourField)) return 'x_hours';
560+
}
561+
// x_days: "M H */D * *" or "M H * * *" or "M H D1,D2,... * *"
473562
if (/^\d{1,2} \d{1,2} \*\/\d+ \* \*$/.test(expr) || /^\d{1,2} \d{1,2} \* \* \*$/.test(expr)) {
474563
return 'x_days';
475564
}
565+
if (/^\d{1,2} \d{1,2} \d+(,\d+)+ \* \*$/.test(expr)) {
566+
const dayField = expr.split(' ')[2];
567+
if (parseSteppedList(dayField)) return 'x_days';
568+
}
476569
// certain_days: "M H * * DAY[,DAY...]" (DAY = SUN,MON,...)
477570
if (/^\d{1,2} \d{1,2} \* \* ((SUN|MON|TUE|WED|THU|FRI|SAT)(-(SUN|MON|TUE|WED|THU|FRI|SAT))?(,)?)+$/.test(expr)) {
478571
return 'certain_days';
@@ -533,4 +626,4 @@ stylesheet.replace(`
533626
}
534627
`);
535628

536-
export { CrontabInput };
629+
export { CrontabInput, parseSteppedList };

testgen/ui/components/frontend/js/components/monitor_settings_form.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { RadioGroup } from './radio_group.js';
3939
import { Caption } from './caption.js';
4040
import { Select } from './select.js';
4141
import { Checkbox } from './checkbox.js';
42-
import { CrontabInput } from './crontab_input.js';
42+
import { CrontabInput, parseSteppedList } from './crontab_input.js';
4343
import { Icon } from './icon.js';
4444
import { Link } from './link.js';
4545
import { withTooltip } from './tooltip.js';
@@ -241,6 +241,7 @@ const ScheduleForm = (
241241
sample: options.cronSample,
242242
value: cronEditorValue,
243243
modes: ['x_hours', 'x_days'],
244+
hideExpression: true,
244245
onChange: (value) => cronExpression.val = value,
245246
}),
246247
),
@@ -372,6 +373,11 @@ function determineDuration(expression) {
372373
if (match) {
373374
return Number(match[1]) * 60 * 60; // H hours
374375
}
376+
// "M H1,H2,... * * *" (stepped hours with starting offset)
377+
if (/^\d{1,2} \d+(,\d+)+ \* \* \*$/.test(expr)) {
378+
const parsed = parseSteppedList(expr.split(' ')[1]);
379+
if (parsed) return parsed.step * 60 * 60;
380+
}
375381
// "M H * * *"
376382
if (/^\d{1,2} \d{1,2} \* \* \*$/.test(expr)) {
377383
return 24 * 60 * 60; // 1 day
@@ -381,6 +387,11 @@ function determineDuration(expression) {
381387
if (match) {
382388
return Number(match[1]) * 24 * 60 * 60; // D days
383389
}
390+
// "M H D1,D2,... * *" (stepped days with starting offset)
391+
if (/^\d{1,2} \d{1,2} \d+(,\d+)+ \* \*$/.test(expr)) {
392+
const parsed = parseSteppedList(expr.split(' ')[2]);
393+
if (parsed) return parsed.step * 24 * 60 * 60;
394+
}
384395
return null;
385396
}
386397

0 commit comments

Comments
 (0)