mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Complex schedules UI (#12445)
* refactor ScheduleFormFields into own file * refactor ScheduleForm * wip complex schedules form * build rruleset from inputs * update schedule form validation for multiple repeat frequencies * add basic rrule set parsing when opening schedule form * complex schedule bugfixes, handle edge cases, etc * fix schedule saving/parsing for single-occurrence schedules * working with timezone issues * fix rrule until times to be in UTC * update tests for new schedule form format * update ouiaIds * tweak schedules spacing * update ScheduleForm tests * show message for unsupported schedule types * default schedules to browser timezone * show error type/message in ErrorDetail * shows frequencies on ScheduleDetails view * handles nullish values
This commit is contained in:
parent
993dd61024
commit
cae2c06190
@ -24,6 +24,7 @@ const CardBody = styled(PFCardBody)`
|
||||
|
||||
const Expandable = styled(PFExpandable)`
|
||||
text-align: left;
|
||||
max-width: 75vw;
|
||||
|
||||
& .pf-c-expandable__toggle {
|
||||
padding-left: 10px;
|
||||
@ -54,7 +55,7 @@ function ErrorDetail({ error }) {
|
||||
{response?.config?.method.toUpperCase()} {response?.config?.url}{' '}
|
||||
<strong>{response?.status}</strong>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<CardBody css="max-width: 70vw">
|
||||
{Array.isArray(message) ? (
|
||||
<ul>
|
||||
{message.map((m) =>
|
||||
@ -70,9 +71,16 @@ function ErrorDetail({ error }) {
|
||||
};
|
||||
|
||||
const renderStack = () => (
|
||||
<CardBody css="white-space: pre; font-family: var(--pf-global--FontFamily--monospace)">
|
||||
{error.stack}
|
||||
</CardBody>
|
||||
<>
|
||||
<CardBody>
|
||||
<strong>
|
||||
{error.name}: {error.message}
|
||||
</strong>
|
||||
</CardBody>
|
||||
<CardBody css="white-space: pre; font-family: var(--pf-global--FontFamily--monospace)">
|
||||
{error.stack}
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -10,6 +10,11 @@ export const FormColumnLayout = styled.div`
|
||||
@media (min-width: 1210px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.stacked &&
|
||||
`border-bottom: 1px solid var(--pf-global--BorderColor--100);
|
||||
padding: var(--pf-global--spacer--sm) 0 var(--pf-global--spacer--md) `}
|
||||
`;
|
||||
|
||||
export const FormFullWidthLayout = styled.div`
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
||||
import { func, shape } from 'prop-types';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import yaml from 'js-yaml';
|
||||
import { parseVariableField } from 'util/yaml';
|
||||
@ -12,7 +11,7 @@ import mergeExtraVars from 'util/prompt/mergeExtraVars';
|
||||
import getSurveyValues from 'util/prompt/getSurveyValues';
|
||||
import { getAddedAndRemoved } from 'util/lists';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import buildRuleSet from '../shared/buildRuleSet';
|
||||
import { CardBody } from '../../Card';
|
||||
|
||||
function ScheduleAdd({
|
||||
@ -36,21 +35,12 @@ function ScheduleAdd({
|
||||
) => {
|
||||
const {
|
||||
inventory,
|
||||
extra_vars,
|
||||
originalCredentials,
|
||||
end,
|
||||
frequency,
|
||||
interval,
|
||||
frequencyOptions,
|
||||
exceptionFrequency,
|
||||
exceptionOptions,
|
||||
timezone,
|
||||
occurrences,
|
||||
runOn,
|
||||
runOnTheDay,
|
||||
runOnTheMonth,
|
||||
runOnDayMonth,
|
||||
runOnDayNumber,
|
||||
runOnTheOccurrence,
|
||||
credentials,
|
||||
daysOfWeek,
|
||||
...submitValues
|
||||
} = values;
|
||||
const { added } = getAddedAndRemoved(
|
||||
@ -83,11 +73,13 @@ function ScheduleAdd({
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values));
|
||||
const ruleSet = buildRuleSet(values);
|
||||
const requestData = {
|
||||
...submitValues,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
rrule: ruleSet.toString().replace(/\n/g, ' '),
|
||||
};
|
||||
delete requestData.startDate;
|
||||
delete requestData.startTime;
|
||||
|
||||
if (Object.keys(values).includes('daysToKeep')) {
|
||||
if (requestData.extra_data) {
|
||||
@ -98,10 +90,6 @@ function ScheduleAdd({
|
||||
});
|
||||
}
|
||||
}
|
||||
delete requestData.startDate;
|
||||
delete requestData.startTime;
|
||||
delete requestData.endDate;
|
||||
delete requestData.endTime;
|
||||
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
|
||||
@ -80,9 +80,7 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
frequency: [],
|
||||
name: 'Run once schedule',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:00 AM',
|
||||
@ -98,15 +96,19 @@ describe('<ScheduleAdd />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Successfully creates a schedule with 10 minute repeat frequency after 10 occurrences', async () => {
|
||||
test('Successfully creates a schedule with 10 minute repeat frequency and 10 occurrences', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'after',
|
||||
frequency: 'minute',
|
||||
interval: 10,
|
||||
frequency: ['minute'],
|
||||
frequencyOptions: {
|
||||
minute: {
|
||||
end: 'after',
|
||||
interval: 10,
|
||||
occurrences: 10,
|
||||
},
|
||||
},
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
occurrences: 10,
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:30 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -125,11 +127,15 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'onDate',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '10:45 AM',
|
||||
frequency: 'hour',
|
||||
interval: 1,
|
||||
frequency: ['hour'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
end: 'onDate',
|
||||
interval: 1,
|
||||
endDate: '2020-03-26',
|
||||
endTime: '10:45 AM',
|
||||
},
|
||||
},
|
||||
name: 'Run every hour until date',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
@ -141,7 +147,7 @@ describe('<ScheduleAdd />', () => {
|
||||
name: 'Run every hour until date',
|
||||
extra_data: {},
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T144500Z',
|
||||
});
|
||||
});
|
||||
|
||||
@ -149,9 +155,13 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'day',
|
||||
interval: 1,
|
||||
frequency: ['day'],
|
||||
frequencyOptions: {
|
||||
day: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run daily',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
@ -170,13 +180,17 @@ describe('<ScheduleAdd />', () => {
|
||||
test('Successfully creates a schedule with weekly repeat frequency on mon/wed/fri', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'week',
|
||||
interval: 1,
|
||||
frequency: ['week'],
|
||||
frequencyOptions: {
|
||||
week: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
},
|
||||
},
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
occurrences: 1,
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -194,13 +208,17 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
interval: 1,
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run on the first day of the month',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 1,
|
||||
startTime: '10:45 AM',
|
||||
startDate: '2020-04-01',
|
||||
timezone: 'America/New_York',
|
||||
@ -219,16 +237,20 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '11:00 AM',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
end: 'never',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '11:00 AM',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheDay: 'tuesday',
|
||||
runOnTheOccurrence: -1,
|
||||
},
|
||||
},
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheDay: 'tuesday',
|
||||
runOnTheOccurrence: -1,
|
||||
startDate: '2020-03-31',
|
||||
startTime: '11:00 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -242,18 +264,23 @@ describe('<ScheduleAdd />', () => {
|
||||
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU',
|
||||
});
|
||||
});
|
||||
|
||||
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the first day of March',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 1,
|
||||
startDate: '2020-03-01',
|
||||
startTime: '12:00 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -272,15 +299,19 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'friday',
|
||||
runOnTheMonth: 4,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the second Friday in April',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'friday',
|
||||
runOnTheMonth: 4,
|
||||
startDate: '2020-04-10',
|
||||
startTime: '11:15 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -299,15 +330,19 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'weekday',
|
||||
runOnTheMonth: 10,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the first weekday in October',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'weekday',
|
||||
runOnTheMonth: 10,
|
||||
startDate: '2020-04-10',
|
||||
startTime: '11:15 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -376,17 +411,7 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
name: 'Schedule',
|
||||
end: 'never',
|
||||
endDate: '2021-01-29',
|
||||
endTime: '2:15 PM',
|
||||
frequency: 'none',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
frequency: [],
|
||||
skip_tags: '',
|
||||
inventory: { name: 'inventory', id: 45 },
|
||||
credentials: [
|
||||
@ -405,7 +430,7 @@ describe('<ScheduleAdd />', () => {
|
||||
inventory: 45,
|
||||
name: 'Schedule',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY',
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
skip_tags: '',
|
||||
});
|
||||
expect(SchedulesAPI.associateCredential).toBeCalledWith(3, 10);
|
||||
@ -462,9 +487,7 @@ describe('<ScheduleAdd />', () => {
|
||||
await act(async () => {
|
||||
scheduleSurveyWrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
frequency: [],
|
||||
name: 'Run once schedule',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:00 AM',
|
||||
|
||||
@ -0,0 +1,218 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { t, Plural, SelectOrdinal } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { formatDateString } from 'util/dates';
|
||||
import { DetailList, Detail } from '../../DetailList';
|
||||
|
||||
const Label = styled.div`
|
||||
margin-bottom: var(--pf-global--spacer--sm);
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
`;
|
||||
|
||||
export default function FrequencyDetails({ type, label, options, timezone }) {
|
||||
const getRunEveryLabel = () => {
|
||||
const { interval } = options;
|
||||
switch (type) {
|
||||
case 'minute':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} minute"
|
||||
other="{interval} minutes"
|
||||
/>
|
||||
);
|
||||
case 'hour':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} hour"
|
||||
other="{interval} hours"
|
||||
/>
|
||||
);
|
||||
case 'day':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} day"
|
||||
other="{interval} days"
|
||||
/>
|
||||
);
|
||||
case 'week':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} week"
|
||||
other="{interval} weeks"
|
||||
/>
|
||||
);
|
||||
case 'month':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} month"
|
||||
other="{interval} months"
|
||||
/>
|
||||
);
|
||||
case 'year':
|
||||
return (
|
||||
<Plural
|
||||
value={interval}
|
||||
one="{interval} year"
|
||||
other="{interval} years"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
throw new Error(t`Frequency did not match an expected value`);
|
||||
}
|
||||
};
|
||||
|
||||
const weekdays = {
|
||||
0: t`Monday`,
|
||||
1: t`Tuesday`,
|
||||
2: t`Wednesday`,
|
||||
3: t`Thursday`,
|
||||
4: t`Friday`,
|
||||
5: t`Saturday`,
|
||||
6: t`Sunday`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>{label}</Label>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={t`Run every`} value={getRunEveryLabel()} />
|
||||
{type === 'week' ? (
|
||||
<Detail
|
||||
label={t`On days`}
|
||||
value={options.daysOfWeek
|
||||
.sort(sortWeekday)
|
||||
.map((d) => weekdays[d.weekday])
|
||||
.join(', ')}
|
||||
/>
|
||||
) : null}
|
||||
<RunOnDetail type={type} options={options} />
|
||||
<Detail label={t`End`} value={getEndValue(type, options, timezone)} />
|
||||
</DetailList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sortWeekday(a, b) {
|
||||
if (a.weekday === 6) return -1;
|
||||
if (b.weekday === 6) return 1;
|
||||
return a.weekday - b.weekday;
|
||||
}
|
||||
|
||||
function RunOnDetail({ type, options }) {
|
||||
if (type === 'month') {
|
||||
if (options.runOn === 'day') {
|
||||
return (
|
||||
<Detail label={t`Run on`} value={t`Day ${options.runOnDayNumber}`} />
|
||||
);
|
||||
}
|
||||
const dayOfWeek = options.runOnTheDay;
|
||||
return (
|
||||
<Detail
|
||||
label={t`Run on`}
|
||||
value={
|
||||
options.runOnDayNumber === -1 ? (
|
||||
t`The last ${dayOfWeek}`
|
||||
) : (
|
||||
<SelectOrdinal
|
||||
value={options.runOnDayNumber}
|
||||
one={`The first ${dayOfWeek}`}
|
||||
two={`The second ${dayOfWeek}`}
|
||||
_3={`The third ${dayOfWeek}`}
|
||||
_4={`The fourth ${dayOfWeek}`}
|
||||
_5={`The fifth ${dayOfWeek}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === 'year') {
|
||||
const months = {
|
||||
1: t`January`,
|
||||
2: t`February`,
|
||||
3: t`March`,
|
||||
4: t`April`,
|
||||
5: t`May`,
|
||||
6: t`June`,
|
||||
7: t`July`,
|
||||
8: t`August`,
|
||||
9: t`September`,
|
||||
10: t`October`,
|
||||
11: t`November`,
|
||||
12: t`December`,
|
||||
};
|
||||
if (options.runOn === 'day') {
|
||||
return (
|
||||
<Detail
|
||||
label={t`Run on`}
|
||||
value={`${months[options.runOnTheMonth]} ${options.runOnDayMonth}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const weekdays = {
|
||||
sunday: t`Sunday`,
|
||||
monday: t`Monday`,
|
||||
tuesday: t`Tuesday`,
|
||||
wednesday: t`Wednesday`,
|
||||
thursday: t`Thursday`,
|
||||
friday: t`Friday`,
|
||||
saturday: t`Saturday`,
|
||||
day: t`day`,
|
||||
weekday: t`weekday`,
|
||||
weekendDay: t`weekend day`,
|
||||
};
|
||||
const weekday = weekdays[options.runOnTheDay];
|
||||
const month = months[options.runOnTheMonth];
|
||||
return (
|
||||
<Detail
|
||||
label={t`Run on`}
|
||||
value={
|
||||
options.runOnTheOccurrence === -1 ? (
|
||||
t`The last ${weekday} of ${month}`
|
||||
) : (
|
||||
<SelectOrdinal
|
||||
value={options.runOnTheOccurrence}
|
||||
one={`The first ${weekday} of ${month}`}
|
||||
two={`The second ${weekday} of ${month}`}
|
||||
_3={`The third ${weekday} of ${month}`}
|
||||
_4={`The fourth ${weekday} of ${month}`}
|
||||
_5={`The fifth ${weekday} of ${month}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getEndValue(type, options, timezone) {
|
||||
if (options.end === 'never') {
|
||||
return t`Never`;
|
||||
}
|
||||
if (options.end === 'after') {
|
||||
const numOccurrences = options.occurrences;
|
||||
return (
|
||||
<Plural
|
||||
value={numOccurrences}
|
||||
one="After {numOccurrences} occurrence"
|
||||
other="After {numOccurrences} occurrences"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const date = DateTime.fromFormat(
|
||||
`${options.endDate} ${options.endTime}`,
|
||||
'yyyy-MM-dd h:mm a',
|
||||
{
|
||||
zone: timezone,
|
||||
}
|
||||
);
|
||||
return formatDateString(date, timezone);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import 'styled-components/macro';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule, rrulestr } from 'rrule';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
@ -12,6 +11,8 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import { JobTemplatesAPI, SchedulesAPI, WorkflowJobTemplatesAPI } from 'api';
|
||||
import { parseVariableField, jsonToYaml } from 'util/yaml';
|
||||
import { useConfig } from 'contexts/Config';
|
||||
import parseRuleObj from '../shared/parseRuleObj';
|
||||
import FrequencyDetails from './FrequencyDetails';
|
||||
import AlertModal from '../../AlertModal';
|
||||
import { CardBody, CardActionsRow } from '../../Card';
|
||||
import ContentError from '../../ContentError';
|
||||
@ -41,6 +42,26 @@ const PromptTitle = styled(Title)`
|
||||
const PromptDetailList = styled(DetailList)`
|
||||
padding: 0px 20px;
|
||||
`;
|
||||
|
||||
const FrequencyDetailsContainer = styled.div`
|
||||
background-color: var(--pf-global--palette--black-150);
|
||||
margin-top: var(--pf-global--spacer--lg);
|
||||
margin-bottom: var(--pf-global--spacer--lg);
|
||||
margin-right: calc(var(--pf-c-card--child--PaddingRight) * -1);
|
||||
margin-left: calc(var(--pf-c-card--child--PaddingLeft) * -1);
|
||||
padding: var(--pf-c-card--child--PaddingRight);
|
||||
|
||||
& > p {
|
||||
margin-bottom: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
& > *:not(:first-child):not(:last-child) {
|
||||
margin-bottom: var(--pf-global--spacer--md);
|
||||
padding-bottom: var(--pf-global--spacer--md);
|
||||
border-bottom: 1px solid var(--pf-global--palette--black-300);
|
||||
}
|
||||
`;
|
||||
|
||||
function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
|
||||
const {
|
||||
id,
|
||||
@ -132,19 +153,18 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
|
||||
fetchCredentialsAndPreview();
|
||||
}, [fetchCredentialsAndPreview]);
|
||||
|
||||
const rule = rrulestr(rrule);
|
||||
let repeatFrequency =
|
||||
rule.options.freq === RRule.MINUTELY && dtstart === dtend
|
||||
? t`None (Run Once)`
|
||||
: rule.toText().replace(/^\w/, (c) => c.toUpperCase());
|
||||
// We should allow rrule tot handle this issue, and they have in version 2.6.8.
|
||||
// (https://github.com/jakubroztocil/rrule/commit/ab9c564a83de2f9688d6671f2a6df273ceb902bf)
|
||||
// However, we are unable to upgrade to that version because that
|
||||
// version throws and unexpected warning.
|
||||
// (https://github.com/jakubroztocil/rrule/issues/427)
|
||||
if (repeatFrequency.split(' ')[1] === 'minutes') {
|
||||
repeatFrequency = t`Every minute for ${rule.options.count} times`;
|
||||
}
|
||||
const frequencies = {
|
||||
minute: t`Minute`,
|
||||
hour: t`Hour`,
|
||||
day: t`Day`,
|
||||
week: t`Week`,
|
||||
month: t`Month`,
|
||||
year: t`Year`,
|
||||
};
|
||||
const { frequency, frequencyOptions } = parseRuleObj(schedule);
|
||||
const repeatFrequency = frequency.length
|
||||
? frequency.map((f) => frequencies[f]).join(', ')
|
||||
: t`None (Run Once)`;
|
||||
|
||||
const {
|
||||
ask_credential_on_launch,
|
||||
@ -268,6 +288,24 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
|
||||
helpText={helpText.localTimeZone(config)}
|
||||
/>
|
||||
<Detail label={t`Repeat Frequency`} value={repeatFrequency} />
|
||||
</DetailList>
|
||||
{frequency.length ? (
|
||||
<FrequencyDetailsContainer>
|
||||
<p>
|
||||
<strong>{t`Frequency Details`}</strong>
|
||||
</p>
|
||||
{frequency.map((freq) => (
|
||||
<FrequencyDetails
|
||||
key={freq}
|
||||
type={freq}
|
||||
label={frequencies[freq]}
|
||||
options={frequencyOptions[freq]}
|
||||
timezone={timezone}
|
||||
/>
|
||||
))}
|
||||
</FrequencyDetailsContainer>
|
||||
) : null}
|
||||
<DetailList gutter="sm">
|
||||
{hasDaysToKeepField ? (
|
||||
<Detail label={t`Days of Data to Keep`} value={daysToKeep} />
|
||||
) : null}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { shape } from 'prop-types';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import yaml from 'js-yaml';
|
||||
@ -12,7 +11,7 @@ import { parseVariableField } from 'util/yaml';
|
||||
import mergeExtraVars from 'util/prompt/mergeExtraVars';
|
||||
import getSurveyValues from 'util/prompt/getSurveyValues';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import buildRuleSet from '../shared/buildRuleSet';
|
||||
import { CardBody } from '../../Card';
|
||||
|
||||
function ScheduleEdit({
|
||||
@ -27,7 +26,7 @@ function ScheduleEdit({
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
const pathRoot = pathname.substring(0, pathname.indexOf('schedules'));
|
||||
|
||||
const handleSubmit = async (
|
||||
values,
|
||||
@ -38,18 +37,11 @@ function ScheduleEdit({
|
||||
const {
|
||||
inventory,
|
||||
credentials = [],
|
||||
end,
|
||||
frequency,
|
||||
interval,
|
||||
frequencyOptions,
|
||||
exceptionFrequency,
|
||||
exceptionOptions,
|
||||
timezone,
|
||||
occurences,
|
||||
runOn,
|
||||
runOnTheDay,
|
||||
runOnTheMonth,
|
||||
runOnDayMonth,
|
||||
runOnDayNumber,
|
||||
runOnTheOccurence,
|
||||
daysOfWeek,
|
||||
...submitValues
|
||||
} = values;
|
||||
const { added, removed } = getAddedAndRemoved(
|
||||
@ -91,15 +83,13 @@ function ScheduleEdit({
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values));
|
||||
const ruleSet = buildRuleSet(values);
|
||||
const requestData = {
|
||||
...submitValues,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
rrule: ruleSet.toString().replace(/\n/g, ' '),
|
||||
};
|
||||
delete requestData.startDate;
|
||||
delete requestData.startTime;
|
||||
delete requestData.endDate;
|
||||
delete requestData.endTime;
|
||||
|
||||
if (Object.keys(values).includes('daysToKeep')) {
|
||||
if (!requestData.extra_data) {
|
||||
|
||||
@ -195,9 +195,7 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
frequency: [],
|
||||
name: 'Run once schedule',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:00 AM',
|
||||
@ -218,11 +216,15 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'after',
|
||||
frequency: 'minute',
|
||||
interval: 10,
|
||||
frequency: ['minute'],
|
||||
frequencyOptions: {
|
||||
minute: {
|
||||
end: 'after',
|
||||
interval: 10,
|
||||
occurrences: 10,
|
||||
},
|
||||
},
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
occurrences: 10,
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:30 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -232,7 +234,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
extra_data: {},
|
||||
occurrences: 10,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10',
|
||||
});
|
||||
@ -242,11 +243,15 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'onDate',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '10:45 AM',
|
||||
frequency: 'hour',
|
||||
interval: 1,
|
||||
frequency: ['hour'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
end: 'onDate',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '10:45 AM',
|
||||
interval: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run every hour until date',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
@ -259,7 +264,7 @@ describe('<ScheduleEdit />', () => {
|
||||
name: 'Run every hour until date',
|
||||
extra_data: {},
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T144500Z',
|
||||
});
|
||||
});
|
||||
|
||||
@ -267,9 +272,13 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'day',
|
||||
interval: 1,
|
||||
frequency: ['day'],
|
||||
frequencyOptions: {
|
||||
day: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run daily',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
@ -284,16 +293,21 @@ describe('<ScheduleEdit />', () => {
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=DAILY',
|
||||
});
|
||||
});
|
||||
|
||||
test('Successfully creates a schedule with weekly repeat frequency on mon/wed/fri', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'week',
|
||||
interval: 1,
|
||||
frequency: ['week'],
|
||||
frequencyOptions: {
|
||||
week: {
|
||||
end: 'never',
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
occurrences: 1,
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:45 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -303,7 +317,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`,
|
||||
});
|
||||
});
|
||||
@ -312,13 +325,17 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
},
|
||||
name: 'Run on the first day of the month',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 1,
|
||||
startDate: '2020-04-01',
|
||||
startTime: '10:45 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -328,7 +345,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Run on the first day of the month',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1',
|
||||
});
|
||||
@ -338,15 +354,20 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
endDateTime: '2020-03-26T11:00:00',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
end: 'never',
|
||||
endDate: '2020-03-26',
|
||||
endTime: '11:00 AM',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheDay: 'tuesday',
|
||||
runOnTheOccurrence: -1,
|
||||
},
|
||||
},
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheDay: 'tuesday',
|
||||
runOnTheOccurrence: -1,
|
||||
startDate: '2020-03-31',
|
||||
startTime: '11:00 AM',
|
||||
timezone: 'America/New_York',
|
||||
@ -354,11 +375,8 @@ describe('<ScheduleEdit />', () => {
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
endDateTime: '2020-03-26T11:00:00',
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
runOnTheOccurrence: -1,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU',
|
||||
});
|
||||
@ -368,14 +386,18 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the first day of March',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 1,
|
||||
startTime: '12:00 AM',
|
||||
startDate: '2020-03-01',
|
||||
timezone: 'America/New_York',
|
||||
@ -385,7 +407,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first day of March',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1',
|
||||
});
|
||||
@ -395,15 +416,19 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'friday',
|
||||
runOnTheMonth: 4,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the second Friday in April',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'friday',
|
||||
runOnTheMonth: 4,
|
||||
startTime: '11:15 AM',
|
||||
startDate: '2020-04-10',
|
||||
timezone: 'America/New_York',
|
||||
@ -413,8 +438,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the second Friday in April',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
runOnTheOccurrence: 2,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4',
|
||||
});
|
||||
@ -424,15 +447,19 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
end: 'never',
|
||||
interval: 1,
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'weekday',
|
||||
runOnTheMonth: 10,
|
||||
},
|
||||
},
|
||||
name: 'Yearly on the first weekday in October',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'weekday',
|
||||
runOnTheMonth: 10,
|
||||
startTime: '11:15 AM',
|
||||
startDate: '2020-04-10',
|
||||
timezone: 'America/New_York',
|
||||
@ -442,8 +469,6 @@ describe('<ScheduleEdit />', () => {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first weekday in October',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
|
||||
});
|
||||
@ -522,17 +547,7 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
name: mockSchedule.name,
|
||||
end: 'never',
|
||||
endDate: '2021-01-29',
|
||||
endTime: '2:15 PM',
|
||||
frequency: 'none',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
frequency: [],
|
||||
skip_tags: '',
|
||||
startDate: '2021-01-28',
|
||||
startTime: '2:15 PM',
|
||||
@ -549,10 +564,8 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toBeCalledWith(27, {
|
||||
extra_data: {},
|
||||
name: 'mock schedule',
|
||||
occurrences: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY',
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
skip_tags: '',
|
||||
});
|
||||
expect(SchedulesAPI.disassociateCredential).toBeCalledWith(27, 75);
|
||||
@ -621,8 +634,6 @@ describe('<ScheduleEdit />', () => {
|
||||
startDateTime: undefined,
|
||||
description: '',
|
||||
extra_data: {},
|
||||
occurrences: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
name: 'foo',
|
||||
inventory: 702,
|
||||
rrule:
|
||||
@ -723,9 +734,8 @@ describe('<ScheduleEdit />', () => {
|
||||
await act(async () => {
|
||||
scheduleSurveyWrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
frequency: [],
|
||||
frequencyOptions: {},
|
||||
name: 'Run once schedule',
|
||||
startDate: '2020-03-25',
|
||||
startTime: '10:00 AM',
|
||||
|
||||
@ -16,11 +16,11 @@ const DateTimeGroup = styled.span`
|
||||
`;
|
||||
function DateTimePicker({ dateFieldName, timeFieldName, label }) {
|
||||
const [dateField, dateMeta, dateHelpers] = useField({
|
||||
name: `${dateFieldName}`,
|
||||
name: dateFieldName,
|
||||
validate: combine([required(null), isValidDate]),
|
||||
});
|
||||
const [timeField, timeMeta, timeHelpers] = useField({
|
||||
name: `${timeFieldName}`,
|
||||
name: timeFieldName,
|
||||
validate: combine([required(null), validateTime()]),
|
||||
});
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
Radio,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import { required } from 'util/validators';
|
||||
import { required, requiredPositiveInteger } from 'util/validators';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import FormField from '../../FormField';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
@ -45,65 +45,50 @@ const Checkbox = styled(_Checkbox)`
|
||||
}
|
||||
`;
|
||||
|
||||
export function requiredPositiveInteger() {
|
||||
return (value) => {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value)) {
|
||||
return t`This field must be an integer`;
|
||||
}
|
||||
if (value < 1) {
|
||||
return t`This field must be greater than 0`;
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
return t`Select a value for this field`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const FrequencyDetailSubform = () => {
|
||||
const FrequencyDetailSubform = ({ frequency, prefix }) => {
|
||||
const id = prefix.replace('.', '-');
|
||||
const [runOnDayMonth] = useField({
|
||||
name: 'runOnDayMonth',
|
||||
name: `${prefix}.runOnDayMonth`,
|
||||
});
|
||||
const [runOnDayNumber] = useField({
|
||||
name: 'runOnDayNumber',
|
||||
name: `${prefix}.runOnDayNumber`,
|
||||
});
|
||||
const [runOnTheOccurrence] = useField({
|
||||
name: 'runOnTheOccurrence',
|
||||
name: `${prefix}.runOnTheOccurrence`,
|
||||
});
|
||||
const [runOnTheDay] = useField({
|
||||
name: 'runOnTheDay',
|
||||
name: `${prefix}.runOnTheDay`,
|
||||
});
|
||||
const [runOnTheMonth] = useField({
|
||||
name: 'runOnTheMonth',
|
||||
name: `${prefix}.runOnTheMonth`,
|
||||
});
|
||||
const [startDate] = useField('startDate');
|
||||
const [{ name: dateFieldName }] = useField('endDate');
|
||||
const [{ name: timeFieldName }] = useField('endTime');
|
||||
const [startDate] = useField(`${prefix}.startDate`);
|
||||
|
||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
|
||||
name: 'daysOfWeek',
|
||||
validate: required(t`Select a value for this field`),
|
||||
name: `${prefix}.daysOfWeek`,
|
||||
validate: (val) => {
|
||||
if (frequency === 'week') {
|
||||
return required(t`Select a value for this field`)(val?.length > 0);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const [end, endMeta] = useField({
|
||||
name: 'end',
|
||||
name: `${prefix}.end`,
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [interval, intervalMeta] = useField({
|
||||
name: 'interval',
|
||||
name: `${prefix}.interval`,
|
||||
validate: requiredPositiveInteger(),
|
||||
});
|
||||
const [runOn, runOnMeta] = useField({
|
||||
name: 'runOn',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [frequency] = useField({
|
||||
name: 'frequency',
|
||||
});
|
||||
useField({
|
||||
name: 'occurrences',
|
||||
validate: requiredPositiveInteger(),
|
||||
name: `${prefix}.runOn`,
|
||||
validate: (val) => {
|
||||
if (frequency === 'month' || frequency === 'year') {
|
||||
return required(t`Select a value for this field`)(val);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const monthOptions = [
|
||||
@ -170,7 +155,8 @@ const FrequencyDetailSubform = () => {
|
||||
];
|
||||
|
||||
const updateDaysOfWeek = (day, checked) => {
|
||||
const newDaysOfWeek = [...daysOfWeek.value];
|
||||
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
||||
daysOfWeekHelpers.setTouched(true);
|
||||
if (checked) {
|
||||
newDaysOfWeek.push(day);
|
||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||
@ -181,10 +167,29 @@ const FrequencyDetailSubform = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getPeriodLabel = () => {
|
||||
switch (frequency) {
|
||||
case 'minute':
|
||||
return t`Minute`;
|
||||
case 'hour':
|
||||
return t`Hour`;
|
||||
case 'day':
|
||||
return t`Day`;
|
||||
case 'week':
|
||||
return t`Week`;
|
||||
case 'month':
|
||||
return t`Month`;
|
||||
case 'year':
|
||||
return t`Year`;
|
||||
default:
|
||||
throw new Error(t`Frequency did not match an expected value`);
|
||||
}
|
||||
};
|
||||
|
||||
const getRunEveryLabel = () => {
|
||||
const intervalValue = interval.value;
|
||||
|
||||
switch (frequency.value) {
|
||||
switch (frequency) {
|
||||
case 'minute':
|
||||
return <Plural value={intervalValue} one="minute" other="minutes" />;
|
||||
case 'hour':
|
||||
@ -202,12 +207,14 @@ const FrequencyDetailSubform = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
return (
|
||||
<>
|
||||
<p css="grid-column: 1/-1">
|
||||
<b>{getPeriodLabel()}</b>
|
||||
</p>
|
||||
<FormGroup
|
||||
name="interval"
|
||||
fieldId="schedule-run-every"
|
||||
name={`${prefix}.interval`}
|
||||
fieldId={`schedule-run-every-${id}`}
|
||||
helperTextInvalid={intervalMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
@ -218,7 +225,7 @@ const FrequencyDetailSubform = () => {
|
||||
<div css="display: flex">
|
||||
<TextInput
|
||||
css="margin-right: 10px;"
|
||||
id="schedule-run-every"
|
||||
id={`schedule-run-every-${id}`}
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
@ -230,10 +237,10 @@ const FrequencyDetailSubform = () => {
|
||||
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{frequency?.value === 'week' && (
|
||||
{frequency === 'week' && (
|
||||
<FormGroup
|
||||
name="daysOfWeek"
|
||||
fieldId="schedule-days-of-week"
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
fieldId={`schedule-days-of-week-${id}`}
|
||||
helperTextInvalid={daysOfWeekMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
@ -246,89 +253,89 @@ const FrequencyDetailSubform = () => {
|
||||
<div css="display: flex">
|
||||
<Checkbox
|
||||
label={t`Sun`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.SU)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SU, checked);
|
||||
}}
|
||||
aria-label={t`Sunday`}
|
||||
id="schedule-days-of-week-sun"
|
||||
ouiaId="schedule-days-of-week-sun"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-sun-${id}`}
|
||||
ouiaId={`schedule-days-of-week-sun-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Mon`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.MO)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.MO, checked);
|
||||
}}
|
||||
aria-label={t`Monday`}
|
||||
id="schedule-days-of-week-mon"
|
||||
ouiaId="schedule-days-of-week-mon"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-mon-${id}`}
|
||||
ouiaId={`schedule-days-of-week-mon-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Tue`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.TU)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TU, checked);
|
||||
}}
|
||||
aria-label={t`Tuesday`}
|
||||
id="schedule-days-of-week-tue"
|
||||
ouiaId="schedule-days-of-week-tue"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-tue-${id}`}
|
||||
ouiaId={`schedule-days-of-week-tue-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Wed`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.WE)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.WE, checked);
|
||||
}}
|
||||
aria-label={t`Wednesday`}
|
||||
id="schedule-days-of-week-wed"
|
||||
ouiaId="schedule-days-of-week-wed"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-wed-${id}`}
|
||||
ouiaId={`schedule-days-of-week-wed-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Thu`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.TH)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TH, checked);
|
||||
}}
|
||||
aria-label={t`Thursday`}
|
||||
id="schedule-days-of-week-thu"
|
||||
ouiaId="schedule-days-of-week-thu"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-thu-${id}`}
|
||||
ouiaId={`schedule-days-of-week-thu-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Fri`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.FR)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.FR, checked);
|
||||
}}
|
||||
aria-label={t`Friday`}
|
||||
id="schedule-days-of-week-fri"
|
||||
ouiaId="schedule-days-of-week-fri"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-fri-${id}`}
|
||||
ouiaId={`schedule-days-of-week-fri-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Sat`}
|
||||
isChecked={daysOfWeek.value.includes(RRule.SA)}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SA, checked);
|
||||
}}
|
||||
aria-label={t`Saturday`}
|
||||
id="schedule-days-of-week-sat"
|
||||
ouiaId="schedule-days-of-week-sat"
|
||||
name="daysOfWeek"
|
||||
id={`schedule-days-of-week-sat-${id}`}
|
||||
ouiaId={`schedule-days-of-week-sat-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
)}
|
||||
{(frequency?.value === 'month' || frequency?.value === 'year') &&
|
||||
!isNaN(new Date(startDate.value)) && (
|
||||
{(frequency === 'month' || frequency === 'year') &&
|
||||
!Number.isNaN(new Date(startDate.value)) && (
|
||||
<FormGroup
|
||||
name="runOn"
|
||||
fieldId="schedule-run-on"
|
||||
name={`${prefix}.runOn`}
|
||||
fieldId={`schedule-run-on-${id}`}
|
||||
helperTextInvalid={runOnMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
@ -337,11 +344,11 @@ const FrequencyDetailSubform = () => {
|
||||
label={t`Run on`}
|
||||
>
|
||||
<RunOnRadio
|
||||
id="schedule-run-on-day"
|
||||
name="runOn"
|
||||
id={`schedule-run-on-day-${id}`}
|
||||
name={`${prefix}.runOn`}
|
||||
label={
|
||||
<div css="display: flex;align-items: center;">
|
||||
{frequency?.value === 'month' && (
|
||||
{frequency === 'month' && (
|
||||
<span
|
||||
id="radio-schedule-run-on-day"
|
||||
css="margin-right: 10px;"
|
||||
@ -349,9 +356,9 @@ const FrequencyDetailSubform = () => {
|
||||
<Trans>Day</Trans>
|
||||
</span>
|
||||
)}
|
||||
{frequency?.value === 'year' && (
|
||||
{frequency === 'year' && (
|
||||
<AnsibleSelect
|
||||
id="schedule-run-on-day-month"
|
||||
id={`schedule-run-on-day-month-${id}`}
|
||||
css="margin-right: 10px"
|
||||
isDisabled={runOn.value !== 'day'}
|
||||
data={monthOptions}
|
||||
@ -359,7 +366,7 @@ const FrequencyDetailSubform = () => {
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
id="schedule-run-on-day-number"
|
||||
id={`schedule-run-on-day-number-${id}`}
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
@ -380,18 +387,18 @@ const FrequencyDetailSubform = () => {
|
||||
}}
|
||||
/>
|
||||
<RunOnRadio
|
||||
id="schedule-run-on-the"
|
||||
name="runOn"
|
||||
id={`schedule-run-on-the-${id}`}
|
||||
name={`${prefix}.runOn`}
|
||||
label={
|
||||
<div css="display: flex;align-items: center;">
|
||||
<span
|
||||
id="radio-schedule-run-on-the"
|
||||
id={`radio-schedule-run-on-the-${id}`}
|
||||
css="margin-right: 10px;"
|
||||
>
|
||||
<Trans>The</Trans>
|
||||
</span>
|
||||
<AnsibleSelect
|
||||
id="schedule-run-on-the-occurrence"
|
||||
id={`schedule-run-on-the-occurrence-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={[
|
||||
{ value: 1, key: 'first', label: t`First` },
|
||||
@ -412,7 +419,7 @@ const FrequencyDetailSubform = () => {
|
||||
{...runOnTheOccurrence}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id="schedule-run-on-the-day"
|
||||
id={`schedule-run-on-the-day-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={[
|
||||
{
|
||||
@ -464,16 +471,16 @@ const FrequencyDetailSubform = () => {
|
||||
]}
|
||||
{...runOnTheDay}
|
||||
/>
|
||||
{frequency?.value === 'year' && (
|
||||
{frequency === 'year' && (
|
||||
<>
|
||||
<span
|
||||
id="of-schedule-run-on-the-month"
|
||||
id={`of-schedule-run-on-the-month-${id}`}
|
||||
css="margin-left: 10px;"
|
||||
>
|
||||
<Trans>of</Trans>
|
||||
</span>
|
||||
<AnsibleSelect
|
||||
id="schedule-run-on-the-month"
|
||||
id={`schedule-run-on-the-month-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={monthOptions}
|
||||
{...runOnTheMonth}
|
||||
@ -492,16 +499,16 @@ const FrequencyDetailSubform = () => {
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
name="end"
|
||||
fieldId="schedule-end"
|
||||
name={`${prefix}.end`}
|
||||
fieldId={`schedule-end-${id}`}
|
||||
helperTextInvalid={endMeta.error}
|
||||
isRequired
|
||||
validated={!endMeta.touched || !endMeta.error ? 'default' : 'error'}
|
||||
label={t`End`}
|
||||
>
|
||||
<Radio
|
||||
id="end-never"
|
||||
name="end"
|
||||
id={`end-never-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`Never`}
|
||||
value="never"
|
||||
isChecked={end.value === 'never'}
|
||||
@ -509,11 +516,11 @@ const FrequencyDetailSubform = () => {
|
||||
event.target.value = 'never';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId="end-never-radio-button"
|
||||
ouiaId={`end-never-radio-button-${id}`}
|
||||
/>
|
||||
<Radio
|
||||
id="end-after"
|
||||
name="end"
|
||||
id={`end-after-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`After number of occurrences`}
|
||||
value="after"
|
||||
isChecked={end.value === 'after'}
|
||||
@ -521,11 +528,11 @@ const FrequencyDetailSubform = () => {
|
||||
event.target.value = 'after';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId="end-after-radio-button"
|
||||
ouiaId={`end-after-radio-button-${id}`}
|
||||
/>
|
||||
<Radio
|
||||
id="end-on-date"
|
||||
name="end"
|
||||
id={`end-on-date-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`On date`}
|
||||
value="onDate"
|
||||
isChecked={end.value === 'onDate'}
|
||||
@ -533,25 +540,24 @@ const FrequencyDetailSubform = () => {
|
||||
event.target.value = 'onDate';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId="end-on-radio-button"
|
||||
ouiaId={`end-on-radio-button-${id}`}
|
||||
/>
|
||||
</FormGroup>
|
||||
{end?.value === 'after' && (
|
||||
<FormField
|
||||
id="schedule-occurrences"
|
||||
id={`schedule-occurrences-${id}`}
|
||||
label={t`Occurrences`}
|
||||
name="occurrences"
|
||||
name={`${prefix}.occurrences`}
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
)}
|
||||
{end?.value === 'onDate' && (
|
||||
<DateTimePicker
|
||||
dateFieldName={dateFieldName}
|
||||
timeFieldName={timeFieldName}
|
||||
dateFieldName={`${prefix}.endDate`}
|
||||
timeFieldName={`${prefix}.endTime`}
|
||||
label={t`End date/time`}
|
||||
/>
|
||||
)}
|
||||
|
||||
55
awx/ui/src/components/Schedule/shared/FrequencySelect.js
Normal file
55
awx/ui/src/components/Schedule/shared/FrequencySelect.js
Normal file
@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
import { arrayOf, string } from 'prop-types';
|
||||
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||
|
||||
export default function FrequencySelect({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
placeholderText,
|
||||
children,
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onSelect = (event, selectedValue) => {
|
||||
if (selectedValue === 'none') {
|
||||
onChange([]);
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
const index = value.indexOf(selectedValue);
|
||||
if (index === -1) {
|
||||
onChange(value.concat(selectedValue));
|
||||
} else {
|
||||
onChange(value.slice(0, index).concat(value.slice(index + 1)));
|
||||
}
|
||||
};
|
||||
|
||||
const onToggle = (val) => {
|
||||
if (!val) {
|
||||
onBlur();
|
||||
}
|
||||
setIsOpen(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
variant={SelectVariant.checkbox}
|
||||
onSelect={onSelect}
|
||||
selections={value}
|
||||
placeholderText={placeholderText}
|
||||
onToggle={onToggle}
|
||||
isOpen={isOpen}
|
||||
ouiaId={`frequency-select-${id}`}
|
||||
>
|
||||
{children}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
FrequencySelect.propTypes = {
|
||||
value: arrayOf(string).isRequired,
|
||||
};
|
||||
|
||||
export { SelectOption, SelectVariant };
|
||||
@ -3,38 +3,23 @@ import { shape, func } from 'prop-types';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { Formik } from 'formik';
|
||||
import { RRule } from 'rrule';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Title,
|
||||
ActionGroup,
|
||||
// To be removed once UI completes complex schedules
|
||||
Alert,
|
||||
} from '@patternfly/react-core';
|
||||
import { Config, useConfig } from 'contexts/Config';
|
||||
import { Button, Form, ActionGroup } from '@patternfly/react-core';
|
||||
import { Config } from 'contexts/Config';
|
||||
import { SchedulesAPI } from 'api';
|
||||
import { dateToInputDateTime } from 'util/dates';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { required } from 'util/validators';
|
||||
import { parseVariableField } from 'util/yaml';
|
||||
import Popover from '../../Popover';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import ContentError from '../../ContentError';
|
||||
import ContentLoading from '../../ContentLoading';
|
||||
import FormField, { FormSubmitError } from '../../FormField';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
SubFormLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../FormLayout';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
import { FormSubmitError } from '../../FormField';
|
||||
import { FormColumnLayout, FormFullWidthLayout } from '../../FormLayout';
|
||||
import SchedulePromptableFields from './SchedulePromptableFields';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
import ScheduleFormFields from './ScheduleFormFields';
|
||||
import UnsupportedScheduleForm from './UnsupportedScheduleForm';
|
||||
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
|
||||
import buildRuleObj from './buildRuleObj';
|
||||
import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
||||
|
||||
const NUM_DAYS_PER_FREQUENCY = {
|
||||
week: 7,
|
||||
@ -42,173 +27,6 @@ const NUM_DAYS_PER_FREQUENCY = {
|
||||
year: 365,
|
||||
};
|
||||
|
||||
const generateRunOnTheDay = (days = []) => {
|
||||
if (
|
||||
[
|
||||
RRule.MO,
|
||||
RRule.TU,
|
||||
RRule.WE,
|
||||
RRule.TH,
|
||||
RRule.FR,
|
||||
RRule.SA,
|
||||
RRule.SU,
|
||||
].every((element) => days.indexOf(element) > -1)
|
||||
) {
|
||||
return 'day';
|
||||
}
|
||||
if (
|
||||
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
|
||||
(element) => days.indexOf(element) > -1
|
||||
)
|
||||
) {
|
||||
return 'weekday';
|
||||
}
|
||||
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
|
||||
return 'weekendDay';
|
||||
}
|
||||
if (days.indexOf(RRule.MO) > -1) {
|
||||
return 'monday';
|
||||
}
|
||||
if (days.indexOf(RRule.TU) > -1) {
|
||||
return 'tuesday';
|
||||
}
|
||||
if (days.indexOf(RRule.WE) > -1) {
|
||||
return 'wednesday';
|
||||
}
|
||||
if (days.indexOf(RRule.TH) > -1) {
|
||||
return 'thursday';
|
||||
}
|
||||
if (days.indexOf(RRule.FR) > -1) {
|
||||
return 'friday';
|
||||
}
|
||||
if (days.indexOf(RRule.SA) > -1) {
|
||||
return 'saturday';
|
||||
}
|
||||
if (days.indexOf(RRule.SU) > -1) {
|
||||
return 'sunday';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function ScheduleFormFields({ hasDaysToKeepField, zoneOptions, zoneLinks }) {
|
||||
const [timezone, timezoneMeta] = useField({
|
||||
name: 'timezone',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [frequency, frequencyMeta] = useField({
|
||||
name: 'frequency',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [{ name: dateFieldName }] = useField('startDate');
|
||||
const [{ name: timeFieldName }] = useField('startTime');
|
||||
const [timezoneMessage, setTimezoneMessage] = useState('');
|
||||
const warnLinkedTZ = (event, selectedValue) => {
|
||||
if (zoneLinks[selectedValue]) {
|
||||
setTimezoneMessage(
|
||||
`Warning: ${selectedValue} is a link to ${zoneLinks[selectedValue]} and will be saved as that.`
|
||||
);
|
||||
} else {
|
||||
setTimezoneMessage('');
|
||||
}
|
||||
timezone.onChange(event, selectedValue);
|
||||
};
|
||||
|
||||
let timezoneValidatedStatus = 'default';
|
||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||
timezoneValidatedStatus = 'error';
|
||||
} else if (timezoneMessage) {
|
||||
timezoneValidatedStatus = 'warning';
|
||||
}
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
id="schedule-name"
|
||||
label={t`Name`}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="schedule-description"
|
||||
label={t`Description`}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<DateTimePicker
|
||||
dateFieldName={dateFieldName}
|
||||
timeFieldName={timeFieldName}
|
||||
label={t`Start date/time`}
|
||||
/>
|
||||
<FormGroup
|
||||
name="timezone"
|
||||
fieldId="schedule-timezone"
|
||||
helperTextInvalid={timezoneMeta.error || timezoneMessage}
|
||||
isRequired
|
||||
validated={timezoneValidatedStatus}
|
||||
label={t`Local time zone`}
|
||||
helperText={timezoneMessage}
|
||||
labelIcon={<Popover content={helpText.localTimeZone(config)} />}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-timezone"
|
||||
data={zoneOptions}
|
||||
{...timezone}
|
||||
onChange={warnLinkedTZ}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name="frequency"
|
||||
fieldId="schedule-requency"
|
||||
helperTextInvalid={frequencyMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={t`Run frequency`}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-frequency"
|
||||
data={[
|
||||
{ value: 'none', key: 'none', label: t`None (run once)` },
|
||||
{ value: 'minute', key: 'minute', label: t`Minute` },
|
||||
{ value: 'hour', key: 'hour', label: t`Hour` },
|
||||
{ value: 'day', key: 'day', label: t`Day` },
|
||||
{ value: 'week', key: 'week', label: t`Week` },
|
||||
{ value: 'month', key: 'month', label: t`Month` },
|
||||
{ value: 'year', key: 'year', label: t`Year` },
|
||||
]}
|
||||
{...frequency}
|
||||
/>
|
||||
</FormGroup>
|
||||
{hasDaysToKeepField ? (
|
||||
<FormField
|
||||
id="schedule-days-to-keep"
|
||||
label={t`Days of Data to Keep`}
|
||||
name="daysToKeep"
|
||||
type="number"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
) : null}
|
||||
{frequency.value !== 'none' && (
|
||||
<SubFormLayout>
|
||||
<Title size="md" headingLevel="h4">
|
||||
{t`Frequency Details`}
|
||||
</Title>
|
||||
<FormColumnLayout>
|
||||
<FrequencyDetailSubform />
|
||||
</FormColumnLayout>
|
||||
</SubFormLayout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleForm({
|
||||
hasDaysToKeepField,
|
||||
handleCancel,
|
||||
@ -415,25 +233,72 @@ function ScheduleForm({
|
||||
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
||||
|
||||
const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
|
||||
const initialFrequencyOptions = {
|
||||
minute: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
},
|
||||
hour: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
},
|
||||
day: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
},
|
||||
week: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
daysOfWeek: [],
|
||||
},
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
runOn: 'day',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
year: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
runOn: 'day',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
end: 'never',
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
frequency: [],
|
||||
exceptionFrequency: [],
|
||||
frequencyOptions: initialFrequencyOptions,
|
||||
exceptionOptions: initialFrequencyOptions,
|
||||
name: schedule.name || '',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
startDate: currentDate,
|
||||
startTime: time,
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
timezone: schedule.timezone || now.zoneName,
|
||||
};
|
||||
const submitSchedule = (
|
||||
values,
|
||||
@ -465,132 +330,23 @@ function ScheduleForm({
|
||||
initialValues.daysToKeep = initialDaysToKeep;
|
||||
}
|
||||
|
||||
const overriddenValues = {};
|
||||
|
||||
if (Object.keys(schedule).length > 0) {
|
||||
if (schedule.rrule) {
|
||||
if (schedule.rrule.split(/\s+/).length > 2) {
|
||||
let overriddenValues = {};
|
||||
if (schedule.rrule) {
|
||||
try {
|
||||
overriddenValues = parseRuleObj(schedule);
|
||||
} catch (error) {
|
||||
if (error instanceof UnsupportedRRuleError) {
|
||||
return (
|
||||
<Form autoComplete="off">
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
ouiaId="form-submit-error-alert"
|
||||
title={t`Complex schedules are not supported in the UI yet, please use the API to manage this schedule.`}
|
||||
/>
|
||||
<b>{t`Schedule Rules`}:</b>
|
||||
<pre css="white-space: pre; font-family: var(--pf-global--FontFamily--monospace)">
|
||||
{schedule.rrule}
|
||||
</pre>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
ouiaId="schedule-form-cancel-button"
|
||||
aria-label={t`Cancel`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
<UnsupportedScheduleForm
|
||||
schedule={schedule}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
origOptions: {
|
||||
bymonth,
|
||||
bymonthday,
|
||||
bysetpos,
|
||||
byweekday,
|
||||
count,
|
||||
dtstart,
|
||||
freq,
|
||||
interval,
|
||||
},
|
||||
} = RRule.fromString(schedule.rrule.replace(' ', '\n'));
|
||||
|
||||
if (dtstart) {
|
||||
const [startDate, startTime] = dateToInputDateTime(
|
||||
schedule.dtstart,
|
||||
schedule.timezone
|
||||
);
|
||||
|
||||
overriddenValues.startDate = startDate;
|
||||
overriddenValues.startTime = startTime;
|
||||
}
|
||||
|
||||
if (schedule.until) {
|
||||
overriddenValues.end = 'onDate';
|
||||
|
||||
const [endDate, endTime] = dateToInputDateTime(
|
||||
schedule.until,
|
||||
schedule.timezone
|
||||
);
|
||||
|
||||
overriddenValues.endDate = endDate;
|
||||
overriddenValues.endTime = endTime;
|
||||
} else if (count) {
|
||||
overriddenValues.end = 'after';
|
||||
overriddenValues.occurrences = count;
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
overriddenValues.interval = interval;
|
||||
}
|
||||
|
||||
if (typeof freq === 'number') {
|
||||
switch (freq) {
|
||||
case RRule.MINUTELY:
|
||||
if (schedule.dtstart !== schedule.dtend) {
|
||||
overriddenValues.frequency = 'minute';
|
||||
}
|
||||
break;
|
||||
case RRule.HOURLY:
|
||||
overriddenValues.frequency = 'hour';
|
||||
break;
|
||||
case RRule.DAILY:
|
||||
overriddenValues.frequency = 'day';
|
||||
break;
|
||||
case RRule.WEEKLY:
|
||||
overriddenValues.frequency = 'week';
|
||||
if (byweekday) {
|
||||
overriddenValues.daysOfWeek = byweekday;
|
||||
}
|
||||
break;
|
||||
case RRule.MONTHLY:
|
||||
overriddenValues.frequency = 'month';
|
||||
if (bymonthday) {
|
||||
overriddenValues.runOnDayNumber = bymonthday;
|
||||
} else if (bysetpos) {
|
||||
overriddenValues.runOn = 'the';
|
||||
overriddenValues.runOnTheOccurrence = bysetpos;
|
||||
overriddenValues.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
}
|
||||
break;
|
||||
case RRule.YEARLY:
|
||||
overriddenValues.frequency = 'year';
|
||||
if (bymonthday) {
|
||||
overriddenValues.runOnDayNumber = bymonthday;
|
||||
overriddenValues.runOnDayMonth = bymonth;
|
||||
} else if (bysetpos) {
|
||||
overriddenValues.runOn = 'the';
|
||||
overriddenValues.runOnTheOccurrence = bysetpos;
|
||||
overriddenValues.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
overriddenValues.runOnTheMonth = bymonth;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
rruleError = error;
|
||||
}
|
||||
} else {
|
||||
rruleError = new Error(t`Schedule is missing rrule`);
|
||||
rruleError = error;
|
||||
}
|
||||
} else if (schedule.id) {
|
||||
rruleError = new Error(t`Schedule is missing rrule`);
|
||||
}
|
||||
|
||||
if (contentError || rruleError) {
|
||||
@ -601,54 +357,83 @@ function ScheduleForm({
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
const validate = (values) => {
|
||||
const errors = {};
|
||||
|
||||
values.frequency.forEach((freq) => {
|
||||
const options = values.frequencyOptions[freq];
|
||||
const freqErrors = {};
|
||||
|
||||
if (
|
||||
(freq === 'month' || freq === 'year') &&
|
||||
options.runOn === 'day' &&
|
||||
(options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
|
||||
) {
|
||||
freqErrors.runOn = t`Please select a day number between 1 and 31.`;
|
||||
}
|
||||
|
||||
if (options.end === 'after' && !options.occurrences) {
|
||||
freqErrors.occurrences = t`Please enter a number of occurrences.`;
|
||||
}
|
||||
|
||||
if (options.end === 'onDate') {
|
||||
if (
|
||||
DateTime.fromISO(values.startDate) >=
|
||||
DateTime.fromISO(options.endDate)
|
||||
) {
|
||||
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||
}
|
||||
|
||||
if (
|
||||
DateTime.fromISO(options.endDate)
|
||||
.diff(DateTime.fromISO(values.startDate), 'days')
|
||||
.toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
|
||||
) {
|
||||
const rule = new RRule(
|
||||
buildRuleObj({
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
frequency: freq,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
if (rule.all().length === 0) {
|
||||
errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||
freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(freqErrors).length > 0) {
|
||||
if (!errors.frequencyOptions) {
|
||||
errors.frequencyOptions = {};
|
||||
}
|
||||
errors.frequencyOptions[freq] = freqErrors;
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
return (
|
||||
<Config>
|
||||
{() => (
|
||||
<Formik
|
||||
initialValues={Object.assign(initialValues, overriddenValues)}
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
...overriddenValues,
|
||||
frequencyOptions: {
|
||||
...initialValues.frequencyOptions,
|
||||
...overriddenValues.frequencyOptions,
|
||||
},
|
||||
exceptionOptions: {
|
||||
...initialValues.exceptionOptions,
|
||||
...overriddenValues.exceptionOptions,
|
||||
},
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
submitSchedule(values, launchConfig, surveyConfig, credentials);
|
||||
}}
|
||||
validate={(values) => {
|
||||
const errors = {};
|
||||
const {
|
||||
end,
|
||||
endDate,
|
||||
frequency,
|
||||
runOn,
|
||||
runOnDayNumber,
|
||||
startDate,
|
||||
} = values;
|
||||
|
||||
if (
|
||||
end === 'onDate' &&
|
||||
DateTime.fromISO(endDate)
|
||||
.diff(DateTime.fromISO(startDate), 'days')
|
||||
.toObject().days < NUM_DAYS_PER_FREQUENCY[frequency]
|
||||
) {
|
||||
const rule = new RRule(buildRuleObj(values));
|
||||
if (rule.all().length === 0) {
|
||||
errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||
errors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
end === 'onDate' &&
|
||||
DateTime.fromISO(startDate) >= DateTime.fromISO(endDate)
|
||||
) {
|
||||
errors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||
}
|
||||
|
||||
if (
|
||||
(frequency === 'month' || frequency === 'year') &&
|
||||
runOn === 'day' &&
|
||||
(runOnDayNumber < 1 || runOnDayNumber > 31)
|
||||
) {
|
||||
errors.runOn = t`Please select a day number between 1 and 31.`;
|
||||
}
|
||||
return errors;
|
||||
}}
|
||||
validate={validate}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
|
||||
@ -94,7 +94,7 @@ const defaultFieldsVisible = () => {
|
||||
expect(
|
||||
wrapper.find('FormGroup[label="Local time zone"]').find('HelpIcon').length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run frequency"]').length).toBe(1);
|
||||
expect(wrapper.find('FrequencySelect').length).toBe(1);
|
||||
};
|
||||
|
||||
const nonRRuleValuesMatch = () => {
|
||||
@ -498,21 +498,19 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('DatePicker').prop('value')).toMatch(`${date}`);
|
||||
expect(wrapper.find('TimePicker').prop('time')).toMatch(`${time}`);
|
||||
expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
|
||||
'America/New_York'
|
||||
);
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'none'
|
||||
'UTC'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to minute', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['minute']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -523,20 +521,30 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-minute')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-minute').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#end-on-date-frequencyOptions-minute')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to hour', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('hour', {
|
||||
target: { value: 'hour', key: 'hour', label: 'Hour' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['hour']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -547,20 +555,28 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-hour')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-hour').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-hour').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-hour').prop('checked')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to day', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('day', {
|
||||
target: { value: 'day', key: 'day', label: 'Day' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['day']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -571,20 +587,28 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-day')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-day').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-day').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-day').prop('checked')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to week', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('week', {
|
||||
target: { value: 'week', key: 'week', label: 'Week' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['week']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -595,20 +619,28 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-week')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-week').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-week').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-week').prop('checked')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to month', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('month', {
|
||||
target: { value: 'month', key: 'month', label: 'Month' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['month']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -619,31 +651,45 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-month').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-month').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-month').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-frequencyOptions-month')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-number-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-the-frequencyOptions-month')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(0);
|
||||
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(0);
|
||||
});
|
||||
|
||||
test('correct frequency details fields and values shown when frequency changed to year', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
'FrequencySelect#schedule-frequency'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('year', {
|
||||
target: { value: 'year', key: 'year', label: 'Year' },
|
||||
});
|
||||
runFrequencySelect.invoke('onChange')(['year']);
|
||||
});
|
||||
wrapper.update();
|
||||
defaultFieldsVisible();
|
||||
@ -654,73 +700,125 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-year')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-year').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-year').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-year').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-frequencyOptions-year')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-number-frequencyOptions-year')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-the-frequencyOptions-year')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('select#schedule-run-on-day-month-frequencyOptions-year')
|
||||
.length
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('select#schedule-run-on-the-month-frequencyOptions-year')
|
||||
.length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(1);
|
||||
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1);
|
||||
});
|
||||
|
||||
test('occurrences field properly shown when end after selection is made', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
||||
'minute',
|
||||
]);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('FormGroup[label="Run frequency"] FormSelect')
|
||||
.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
.find('Radio#end-after-frequencyOptions-minute')
|
||||
.invoke('onChange')('after', {
|
||||
target: { name: 'frequencyOptions.minute.end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-after').invoke('onChange')('after', {
|
||||
target: { name: 'end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-minute').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#end-on-date-frequencyOptions-minute')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(1);
|
||||
expect(wrapper.find('input#schedule-occurrences').prop('value')).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-occurrences-frequencyOptions-minute')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-never').invoke('onChange')('never', {
|
||||
target: { name: 'end' },
|
||||
wrapper
|
||||
.find('Radio#end-never-frequencyOptions-minute')
|
||||
.invoke('onChange')('never', {
|
||||
target: { name: 'frequencyOptions.minute.end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
});
|
||||
|
||||
test('error shown when end date/time comes before start date/time', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
||||
'minute',
|
||||
]);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-minute').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('FormGroup[label="Run frequency"] FormSelect')
|
||||
.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
.find('input#end-on-date-frequencyOptions-minute')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-on-date').invoke('onChange')('onDate', {
|
||||
target: { name: 'end' },
|
||||
wrapper
|
||||
.find('Radio#end-on-date-frequencyOptions-minute')
|
||||
.invoke('onChange')('onDate', {
|
||||
target: { name: 'frequencyOptions.minute.end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#end-on-date-frequencyOptions-minute')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper.find('DatePicker[aria-label="End date"]').prop('onChange')(
|
||||
'2020-03-14',
|
||||
@ -739,26 +837,29 @@ describe('<ScheduleForm />', () => {
|
||||
});
|
||||
|
||||
test('error shown when on day number is not between 1 and 31', async () => {
|
||||
act(() => {
|
||||
wrapper.find('select[id="schedule-frequency"]').invoke('onChange')(
|
||||
{
|
||||
currentTarget: { value: 'month', type: 'change' },
|
||||
target: { name: 'frequency', value: 'month' },
|
||||
},
|
||||
'month'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
||||
'month',
|
||||
]);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper.find('input#schedule-run-on-day-number').simulate('change', {
|
||||
target: { value: 32, name: 'runOnDayNumber' },
|
||||
});
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-number-frequencyOptions-month')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
value: 32,
|
||||
name: 'frequencyOptions.month.runOnDayNumber',
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-number-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe(32);
|
||||
|
||||
await act(async () => {
|
||||
@ -766,9 +867,9 @@ describe('<ScheduleForm />', () => {
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('#schedule-run-on-helper').text()).toBe(
|
||||
'Please select a day number between 1 and 31.'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('#schedule-run-on-frequencyOptions-month-helper').text()
|
||||
).toBe('Please select a day number between 1 and 31.');
|
||||
});
|
||||
});
|
||||
|
||||
@ -928,9 +1029,9 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'none'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every 10 minutes', async () => {
|
||||
@ -966,13 +1067,25 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'minute'
|
||||
);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(10);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['minute']);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-minute')
|
||||
.prop('value')
|
||||
).toBe(10);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-minute').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-minute').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#end-on-date-frequencyOptions-minute')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every hour 10 times', async () => {
|
||||
@ -1009,14 +1122,28 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'hour'
|
||||
);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-occurrences').prop('value')).toBe(10);
|
||||
expect(
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['hour']);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-hour')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-hour').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-hour').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-hour').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-occurrences-frequencyOptions-hour')
|
||||
.prop('value')
|
||||
).toBe(10);
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every day', async () => {
|
||||
@ -1053,13 +1180,23 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'day'
|
||||
);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['day']);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-day').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-day').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-day').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-day')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every week on m/w/f until Jan 1, 2020', async () => {
|
||||
@ -1096,40 +1233,64 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'week'
|
||||
);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-sun').prop('checked')
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['week']);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-week')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-week').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-mon').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-tue').prop('checked')
|
||||
wrapper.find('input#end-after-frequencyOptions-week').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-wed').prop('checked')
|
||||
wrapper.find('input#end-on-date-frequencyOptions-week').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-thu').prop('checked')
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-sun-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-fri').prop('checked')
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-mon-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-sat').prop('checked')
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-tue-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-wed-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-thu-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-fri-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-days-of-week-sat-frequencyOptions-week')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('DatePicker[aria-label="End date"]').prop('value')
|
||||
).toBe('2021-01-01');
|
||||
expect(
|
||||
wrapper.find('TimePicker[aria-label="End time"]').prop('value')
|
||||
).toBe('1:00 AM');
|
||||
).toBe('12:00 AM');
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every month on the last weekday', async () => {
|
||||
@ -1169,25 +1330,43 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'month'
|
||||
);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
wrapper.find('select#schedule-run-on-the-occurrence').prop('value')
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['month']);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-month').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-month').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-month').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-frequencyOptions-month')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-the-frequencyOptions-month')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('select#schedule-run-on-the-occurrence-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe(-1);
|
||||
expect(wrapper.find('select#schedule-run-on-the-day').prop('value')).toBe(
|
||||
'weekday'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('select#schedule-run-on-the-day-frequencyOptions-month')
|
||||
.prop('value')
|
||||
).toBe('weekday');
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs every year on the May 6', async () => {
|
||||
@ -1224,24 +1403,42 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'year'
|
||||
);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
|
||||
true
|
||||
);
|
||||
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
wrapper.find('select#schedule-run-on-day-month').prop('value')
|
||||
wrapper.find('FrequencySelect#schedule-frequency').prop('value')
|
||||
).toEqual(['year']);
|
||||
expect(
|
||||
wrapper.find('input#end-never-frequencyOptions-year').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#end-after-frequencyOptions-year').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#end-on-date-frequencyOptions-year').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-every-frequencyOptions-year')
|
||||
.prop('value')
|
||||
).toBe(1);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-frequencyOptions-year')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('input#schedule-run-on-the-frequencyOptions-year')
|
||||
.prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('select#schedule-run-on-day-month-frequencyOptions-year')
|
||||
.prop('value')
|
||||
).toBe(5);
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
wrapper
|
||||
.find('input#schedule-run-on-day-number-frequencyOptions-year')
|
||||
.prop('value')
|
||||
).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
194
awx/ui/src/components/Schedule/shared/ScheduleFormFields.js
Normal file
194
awx/ui/src/components/Schedule/shared/ScheduleFormFields.js
Normal file
@ -0,0 +1,194 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { FormGroup, Title } from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import FormField from 'components/FormField';
|
||||
import { required } from 'util/validators';
|
||||
import { useConfig } from 'contexts/Config';
|
||||
import Popover from '../../Popover';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import FrequencySelect, { SelectOption } from './FrequencySelect';
|
||||
import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
||||
import { SubFormLayout, FormColumnLayout } from '../../FormLayout';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
import sortFrequencies from './sortFrequencies';
|
||||
|
||||
const SelectClearOption = styled(SelectOption)`
|
||||
& > input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ScheduleFormFields({
|
||||
hasDaysToKeepField,
|
||||
zoneOptions,
|
||||
zoneLinks,
|
||||
}) {
|
||||
const [timezone, timezoneMeta] = useField({
|
||||
name: 'timezone',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [frequency, frequencyMeta, frequencyHelper] = useField({
|
||||
name: 'frequency',
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [timezoneMessage, setTimezoneMessage] = useState('');
|
||||
const warnLinkedTZ = (event, selectedValue) => {
|
||||
if (zoneLinks[selectedValue]) {
|
||||
setTimezoneMessage(
|
||||
t`Warning: ${selectedValue} is a link to ${zoneLinks[selectedValue]} and will be saved as that.`
|
||||
);
|
||||
} else {
|
||||
setTimezoneMessage('');
|
||||
}
|
||||
timezone.onChange(event, selectedValue);
|
||||
};
|
||||
let timezoneValidatedStatus = 'default';
|
||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||
timezoneValidatedStatus = 'error';
|
||||
} else if (timezoneMessage) {
|
||||
timezoneValidatedStatus = 'warning';
|
||||
}
|
||||
const config = useConfig();
|
||||
|
||||
// const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
|
||||
// useField({
|
||||
// name: 'exceptionFrequency',
|
||||
// validate: required(t`Select a value for this field`),
|
||||
// });
|
||||
|
||||
const updateFrequency = (setFrequency) => (values) => {
|
||||
setFrequency(values.sort(sortFrequencies));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
id="schedule-name"
|
||||
label={t`Name`}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="schedule-description"
|
||||
label={t`Description`}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<DateTimePicker
|
||||
dateFieldName="startDate"
|
||||
timeFieldName="startTime"
|
||||
label={t`Start date/time`}
|
||||
/>
|
||||
<FormGroup
|
||||
name="timezone"
|
||||
fieldId="schedule-timezone"
|
||||
helperTextInvalid={timezoneMeta.error || timezoneMessage}
|
||||
isRequired
|
||||
validated={timezoneValidatedStatus}
|
||||
label={t`Local time zone`}
|
||||
helperText={timezoneMessage}
|
||||
labelIcon={<Popover content={helpText.localTimeZone(config)} />}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-timezone"
|
||||
data={zoneOptions}
|
||||
{...timezone}
|
||||
onChange={warnLinkedTZ}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name="frequency"
|
||||
fieldId="schedule-frequency"
|
||||
helperTextInvalid={frequencyMeta.error}
|
||||
validated={
|
||||
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={t`Repeat frequency`}
|
||||
>
|
||||
<FrequencySelect
|
||||
id="schedule-frequency"
|
||||
onChange={updateFrequency(frequencyHelper.setValue)}
|
||||
value={frequency.value}
|
||||
placeholderText={
|
||||
frequency.value.length ? t`Select frequency` : t`None (run once)`
|
||||
}
|
||||
onBlur={frequencyHelper.setTouched}
|
||||
>
|
||||
<SelectClearOption value="none">{t`None (run once)`}</SelectClearOption>
|
||||
<SelectOption value="minute">{t`Minute`}</SelectOption>
|
||||
<SelectOption value="hour">{t`Hour`}</SelectOption>
|
||||
<SelectOption value="day">{t`Day`}</SelectOption>
|
||||
<SelectOption value="week">{t`Week`}</SelectOption>
|
||||
<SelectOption value="month">{t`Month`}</SelectOption>
|
||||
<SelectOption value="year">{t`Year`}</SelectOption>
|
||||
</FrequencySelect>
|
||||
</FormGroup>
|
||||
{hasDaysToKeepField ? (
|
||||
<FormField
|
||||
id="schedule-days-to-keep"
|
||||
label={t`Days of Data to Keep`}
|
||||
name="daysToKeep"
|
||||
type="number"
|
||||
validate={required(null)}
|
||||
isRequired
|
||||
/>
|
||||
) : null}
|
||||
{frequency.value.length ? (
|
||||
<SubFormLayout>
|
||||
<Title size="md" headingLevel="h4">
|
||||
{t`Frequency Details`}
|
||||
</Title>
|
||||
{frequency.value.map((val) => (
|
||||
<FormColumnLayout key={val} stacked>
|
||||
<FrequencyDetailSubform
|
||||
frequency={val}
|
||||
prefix={`frequencyOptions.${val}`}
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
))}
|
||||
{/* <Title size="md" headingLevel="h4">{t`Exceptions`}</Title>
|
||||
<FormGroup
|
||||
name="exceptions"
|
||||
fieldId="exception-frequency"
|
||||
helperTextInvalid={exceptionFrequencyMeta.error}
|
||||
validated={
|
||||
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
|
||||
? 'default'
|
||||
: 'error'
|
||||
}
|
||||
label={t`Add exceptions`}
|
||||
>
|
||||
<FrequencySelect
|
||||
variant={SelectVariant.checkbox}
|
||||
onChange={exceptionFrequencyHelper.setValue}
|
||||
value={exceptionFrequency.value}
|
||||
placeholderText={t`None`}
|
||||
onBlur={exceptionFrequencyHelper.setTouched}
|
||||
>
|
||||
<SelectClearOption value="none">{t`None`}</SelectClearOption>
|
||||
<SelectOption value="minute">{t`Minute`}</SelectOption>
|
||||
<SelectOption value="hour">{t`Hour`}</SelectOption>
|
||||
<SelectOption value="day">{t`Day`}</SelectOption>
|
||||
<SelectOption value="week">{t`Week`}</SelectOption>
|
||||
<SelectOption value="month">{t`Month`}</SelectOption>
|
||||
<SelectOption value="year">{t`Year`}</SelectOption>
|
||||
</FrequencySelect>
|
||||
</FormGroup>
|
||||
{exceptionFrequency.value.map((val) => (
|
||||
<FormColumnLayout key={val} stacked>
|
||||
<FrequencyDetailSubform
|
||||
frequency={val}
|
||||
prefix={`exceptionOptions.${val}`}
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
))} */}
|
||||
</SubFormLayout>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -41,20 +41,9 @@ function SchedulePromptableFields({
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
daysOfWeek: values.daysOfWeek,
|
||||
description: values.description,
|
||||
end: values.end,
|
||||
endDateTime: values.endDateTime,
|
||||
frequency: values.frequency,
|
||||
interval: values.interval,
|
||||
name: values.name,
|
||||
occurences: values.occurances,
|
||||
runOn: values.runOn,
|
||||
runOnDayMonth: values.runOnDayMonth,
|
||||
runOnDayNumber: values.runOnDayNumber,
|
||||
runOnTheDay: values.runOnTheDay,
|
||||
runOnTheMonth: values.runOnTheMonth,
|
||||
runOnTheOccurence: values.runOnTheOccurance,
|
||||
startDateTime: values.startDateTime,
|
||||
timezone: values.timezone,
|
||||
},
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button, Form, ActionGroup, Alert } from '@patternfly/react-core';
|
||||
|
||||
export default function UnsupportedScheduleForm({ schedule, handleCancel }) {
|
||||
return (
|
||||
<Form autoComplete="off">
|
||||
<Alert
|
||||
variant="danger"
|
||||
isInline
|
||||
ouiaId="form-submit-error-alert"
|
||||
title={t`This schedule uses complex rules that are not supported in the
|
||||
UI. Please use the API to manage this schedule.`}
|
||||
/>
|
||||
<b>{t`Schedule Rules`}:</b>
|
||||
<pre css="white-space: pre; font-family: var(--pf-global--FontFamily--monospace)">
|
||||
{schedule.rrule.split(' ').join('\n')}
|
||||
</pre>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
ouiaId="schedule-form-cancel-button"
|
||||
aria-label={t`Cancel`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -3,30 +3,42 @@ import { RRule } from 'rrule';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getRRuleDayConstants } from 'util/dates';
|
||||
|
||||
window.RRule = RRule;
|
||||
window.DateTime = DateTime;
|
||||
|
||||
const parseTime = (time) => [
|
||||
DateTime.fromFormat(time, 'h:mm a').hour,
|
||||
DateTime.fromFormat(time, 'h:mm a').minute,
|
||||
];
|
||||
|
||||
export default function buildRuleObj(values) {
|
||||
export function buildDtStartObj(values) {
|
||||
// Dates are formatted like "YYYY-MM-DD"
|
||||
const [startYear, startMonth, startDay] = values.startDate.split('-');
|
||||
// Times are formatted like "HH:MM:SS" or "HH:MM" if no seconds
|
||||
// have been specified
|
||||
const [startHour, startMinute] = parseTime(values.startTime);
|
||||
|
||||
const dateString = `${startYear}${pad(startMonth)}${pad(startDay)}T${pad(
|
||||
startHour
|
||||
)}${pad(startMinute)}00`;
|
||||
const rruleString = values.timezone
|
||||
? `DTSTART;TZID=${values.timezone}:${dateString}`
|
||||
: `DTSTART:${dateString}Z`;
|
||||
const rule = RRule.fromString(rruleString);
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
function pad(num) {
|
||||
if (typeof num === 'string') {
|
||||
return num;
|
||||
}
|
||||
return num < 10 ? `0${num}` : num;
|
||||
}
|
||||
|
||||
export default function buildRuleObj(values) {
|
||||
const ruleObj = {
|
||||
interval: values.interval,
|
||||
dtstart: new Date(
|
||||
Date.UTC(
|
||||
startYear,
|
||||
parseInt(startMonth, 10) - 1,
|
||||
startDay,
|
||||
startHour,
|
||||
startMinute
|
||||
)
|
||||
),
|
||||
tzid: values.timezone,
|
||||
};
|
||||
|
||||
switch (values.frequency) {
|
||||
@ -79,22 +91,20 @@ export default function buildRuleObj(values) {
|
||||
ruleObj.count = values.occurrences;
|
||||
break;
|
||||
case 'onDate': {
|
||||
const [endYear, endMonth, endDay] = values.endDate.split('-');
|
||||
|
||||
const [endHour, endMinute] = parseTime(values.endTime);
|
||||
ruleObj.until = new Date(
|
||||
Date.UTC(
|
||||
endYear,
|
||||
parseInt(endMonth, 10) - 1,
|
||||
endDay,
|
||||
endHour,
|
||||
endMinute
|
||||
)
|
||||
);
|
||||
const localEndDate = DateTime.fromISO(`${values.endDate}T000000`, {
|
||||
zone: values.timezone,
|
||||
});
|
||||
const localEndTime = localEndDate.set({
|
||||
hour: endHour,
|
||||
minute: endMinute,
|
||||
second: 0,
|
||||
});
|
||||
ruleObj.until = localEndTime.toJSDate();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(t`End did not match an expected value`);
|
||||
throw new Error(t`End did not match an expected value (${values.end})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
awx/ui/src/components/Schedule/shared/buildRuleSet.js
Normal file
45
awx/ui/src/components/Schedule/shared/buildRuleSet.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { RRule, RRuleSet } from 'rrule';
|
||||
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
||||
|
||||
window.RRuleSet = RRuleSet;
|
||||
|
||||
const frequencies = ['minute', 'hour', 'day', 'week', 'month', 'year'];
|
||||
export default function buildRuleSet(values) {
|
||||
const set = new RRuleSet();
|
||||
|
||||
const startRule = buildDtStartObj({
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
});
|
||||
set.rrule(startRule);
|
||||
|
||||
if (values.frequency.length === 0) {
|
||||
const rule = buildRuleObj({
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
});
|
||||
set.rrule(new RRule(rule));
|
||||
}
|
||||
|
||||
frequencies.forEach((frequency) => {
|
||||
if (!values.frequency.includes(frequency)) {
|
||||
return;
|
||||
}
|
||||
const rule = buildRuleObj({
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency,
|
||||
...values.frequencyOptions[frequency],
|
||||
});
|
||||
set.rrule(new RRule(rule));
|
||||
});
|
||||
|
||||
// TODO: exclusions
|
||||
|
||||
return set;
|
||||
}
|
||||
246
awx/ui/src/components/Schedule/shared/buildRuleSet.test.js
Normal file
246
awx/ui/src/components/Schedule/shared/buildRuleSet.test.js
Normal file
@ -0,0 +1,246 @@
|
||||
import { RRule } from 'rrule';
|
||||
import buildRuleSet from './buildRuleSet';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
describe('buildRuleSet', () => {
|
||||
test('should build minutely recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['minute'],
|
||||
frequencyOptions: {
|
||||
minute: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=MINUTELY'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build hourly recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['hour'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=HOURLY'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build daily recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['day'],
|
||||
frequencyOptions: {
|
||||
day: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=DAILY'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build weekly recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['week'],
|
||||
frequencyOptions: {
|
||||
week: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
daysOfWeek: [RRule.SU],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=SU'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build monthly by day recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=15'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build monthly by weekday recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['month'],
|
||||
frequencyOptions: {
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'monday',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build yearly by day recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build yearly by weekday recurring rrule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['year'],
|
||||
frequencyOptions: {
|
||||
year: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 4,
|
||||
runOnTheDay: 'monday',
|
||||
runOnTheMonth: 6,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(
|
||||
'DTSTART:20220613T123000Z\nRRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=4;BYDAY=MO;BYMONTH=6'
|
||||
);
|
||||
});
|
||||
|
||||
test('should build combined frequencies', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: ['minute', 'month'],
|
||||
frequencyOptions: {
|
||||
minute: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
},
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'monday',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(`DTSTART:20220613T123000Z
|
||||
RRULE:INTERVAL=1;FREQ=MINUTELY
|
||||
RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO`);
|
||||
});
|
||||
|
||||
test('should build combined frequencies with end dates', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-01',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['hour', 'month'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
interval: 2,
|
||||
end: 'onDate',
|
||||
endDate: '2026-07-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'onDate',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'monday',
|
||||
runOnDayNumber: 1,
|
||||
endDate: '2026-06-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(`DTSTART;TZID=US/Eastern:20220601T123000
|
||||
RRULE:INTERVAL=2;FREQ=HOURLY;UNTIL=20260702T170000Z
|
||||
RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`);
|
||||
});
|
||||
|
||||
test('should build single occurence', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
frequency: [],
|
||||
frequencyOptions: {},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
expect(ruleSet.toString()).toEqual(`DTSTART:20220613T123000Z
|
||||
RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY`);
|
||||
});
|
||||
});
|
||||
235
awx/ui/src/components/Schedule/shared/parseRuleObj.js
Normal file
235
awx/ui/src/components/Schedule/shared/parseRuleObj.js
Normal file
@ -0,0 +1,235 @@
|
||||
import { RRule, rrulestr } from 'rrule';
|
||||
import { dateToInputDateTime } from 'util/dates';
|
||||
import { DateTime } from 'luxon';
|
||||
import sortFrequencies from './sortFrequencies';
|
||||
|
||||
export class UnsupportedRRuleError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'UnsupportedRRuleError';
|
||||
}
|
||||
}
|
||||
|
||||
export default function parseRuleObj(schedule) {
|
||||
let values = {
|
||||
frequency: [],
|
||||
frequencyOptions: {},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
timezone: schedule.timezone,
|
||||
};
|
||||
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
||||
forceset: true,
|
||||
});
|
||||
|
||||
const ruleStrings = ruleset.valueOf();
|
||||
ruleStrings.forEach((ruleString) => {
|
||||
const type = ruleString.match(/^[A-Z]+/)[0];
|
||||
switch (type) {
|
||||
case 'DTSTART':
|
||||
values = parseDtstart(schedule, values);
|
||||
break;
|
||||
case 'RRULE':
|
||||
values = parseRrule(ruleString, schedule, values);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedRRuleError(`Unsupported rrule type: ${type}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (isSingleOccurrence(values)) {
|
||||
values.frequency = [];
|
||||
values.frequencyOptions = {};
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function isSingleOccurrence(values) {
|
||||
if (values.frequency.length > 1) {
|
||||
return false;
|
||||
}
|
||||
if (values.frequency[0] !== 'minute') {
|
||||
return false;
|
||||
}
|
||||
const options = values.frequencyOptions.minute;
|
||||
return options.end === 'after' && options.occurrences === 1;
|
||||
}
|
||||
|
||||
function parseDtstart(schedule, values) {
|
||||
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
|
||||
const [startDate, startTime] = dateToInputDateTime(
|
||||
schedule.dtstart,
|
||||
schedule.timezone
|
||||
);
|
||||
return {
|
||||
...values,
|
||||
startDate,
|
||||
startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const frequencyTypes = {
|
||||
[RRule.MINUTELY]: 'minute',
|
||||
[RRule.HOURLY]: 'hour',
|
||||
[RRule.DAILY]: 'day',
|
||||
[RRule.WEEKLY]: 'week',
|
||||
[RRule.MONTHLY]: 'month',
|
||||
[RRule.YEARLY]: 'year',
|
||||
};
|
||||
|
||||
function parseRrule(rruleString, schedule, values) {
|
||||
const {
|
||||
origOptions: {
|
||||
bymonth,
|
||||
bymonthday,
|
||||
bysetpos,
|
||||
byweekday,
|
||||
count,
|
||||
freq,
|
||||
interval,
|
||||
until,
|
||||
},
|
||||
} = RRule.fromString(rruleString);
|
||||
|
||||
const now = DateTime.now();
|
||||
const closestQuarterHour = DateTime.fromMillis(
|
||||
Math.ceil(now.ts / 900000) * 900000
|
||||
);
|
||||
const tomorrow = closestQuarterHour.plus({ days: 1 });
|
||||
const [, time] = dateToInputDateTime(closestQuarterHour.toISO());
|
||||
const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
|
||||
|
||||
const options = {
|
||||
endDate: tomorrowDate,
|
||||
endTime: time,
|
||||
occurrences: 1,
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
};
|
||||
|
||||
if (until) {
|
||||
options.end = 'onDate';
|
||||
const end = DateTime.fromISO(until.toISOString());
|
||||
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
|
||||
options.endDate = endDate;
|
||||
options.endTime = endTime;
|
||||
} else if (count) {
|
||||
options.end = 'after';
|
||||
options.occurrences = count;
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
options.interval = interval;
|
||||
}
|
||||
|
||||
if (typeof freq !== 'number') {
|
||||
throw new Error(`Unexpected rrule frequency: ${freq}`);
|
||||
}
|
||||
const frequency = frequencyTypes[freq];
|
||||
if (values.frequency.includes(frequency)) {
|
||||
throw new Error(`Duplicate frequency types not supported (${frequency})`);
|
||||
}
|
||||
|
||||
if (freq === RRule.WEEKLY && byweekday) {
|
||||
options.daysOfWeek = byweekday;
|
||||
}
|
||||
|
||||
if (freq === RRule.MONTHLY) {
|
||||
options.runOn = 'day';
|
||||
options.runOnTheOccurrence = 1;
|
||||
options.runOnTheDay = 'sunday';
|
||||
options.runOnDayNumber = 1;
|
||||
|
||||
if (bymonthday) {
|
||||
options.runOnDayNumber = bymonthday;
|
||||
}
|
||||
if (bysetpos) {
|
||||
options.runOn = 'the';
|
||||
options.runOnTheOccurrence = bysetpos;
|
||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
}
|
||||
}
|
||||
|
||||
if (freq === RRule.YEARLY) {
|
||||
options.runOn = 'day';
|
||||
options.runOnTheOccurrence = 1;
|
||||
options.runOnTheDay = 'sunday';
|
||||
options.runOnTheMonth = 1;
|
||||
options.runOnDayMonth = 1;
|
||||
options.runOnDayNumber = 1;
|
||||
|
||||
if (bymonthday) {
|
||||
options.runOnDayNumber = bymonthday;
|
||||
options.runOnDayMonth = bymonth;
|
||||
}
|
||||
if (bysetpos) {
|
||||
options.runOn = 'the';
|
||||
options.runOnTheOccurrence = bysetpos;
|
||||
options.runOnTheDay = generateRunOnTheDay(byweekday);
|
||||
options.runOnTheMonth = bymonth;
|
||||
}
|
||||
}
|
||||
|
||||
if (values.frequencyOptions.frequency) {
|
||||
throw new UnsupportedRRuleError('Duplicate frequency types not supported');
|
||||
}
|
||||
|
||||
return {
|
||||
...values,
|
||||
frequency: [...values.frequency, frequency].sort(sortFrequencies),
|
||||
frequencyOptions: {
|
||||
...values.frequencyOptions,
|
||||
[frequency]: options,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateRunOnTheDay(days = []) {
|
||||
if (
|
||||
[
|
||||
RRule.MO,
|
||||
RRule.TU,
|
||||
RRule.WE,
|
||||
RRule.TH,
|
||||
RRule.FR,
|
||||
RRule.SA,
|
||||
RRule.SU,
|
||||
].every((element) => days.indexOf(element) > -1)
|
||||
) {
|
||||
return 'day';
|
||||
}
|
||||
if (
|
||||
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
|
||||
(element) => days.indexOf(element) > -1
|
||||
)
|
||||
) {
|
||||
return 'weekday';
|
||||
}
|
||||
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
|
||||
return 'weekendDay';
|
||||
}
|
||||
if (days.indexOf(RRule.MO) > -1) {
|
||||
return 'monday';
|
||||
}
|
||||
if (days.indexOf(RRule.TU) > -1) {
|
||||
return 'tuesday';
|
||||
}
|
||||
if (days.indexOf(RRule.WE) > -1) {
|
||||
return 'wednesday';
|
||||
}
|
||||
if (days.indexOf(RRule.TH) > -1) {
|
||||
return 'thursday';
|
||||
}
|
||||
if (days.indexOf(RRule.FR) > -1) {
|
||||
return 'friday';
|
||||
}
|
||||
if (days.indexOf(RRule.SA) > -1) {
|
||||
return 'saturday';
|
||||
}
|
||||
if (days.indexOf(RRule.SU) > -1) {
|
||||
return 'sunday';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
244
awx/ui/src/components/Schedule/shared/parseRuleObj.test.js
Normal file
244
awx/ui/src/components/Schedule/shared/parseRuleObj.test.js
Normal file
@ -0,0 +1,244 @@
|
||||
import { DateTime, Settings } from 'luxon';
|
||||
import { RRule } from 'rrule';
|
||||
import parseRuleObj from './parseRuleObj';
|
||||
import buildRuleSet from './buildRuleSet';
|
||||
|
||||
describe(parseRuleObj, () => {
|
||||
let origNow = Settings.now;
|
||||
beforeEach(() => {
|
||||
const expectedNow = DateTime.local(2022, 6, 1, 13, 0, 0);
|
||||
Settings.now = () => expectedNow.toMillis();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Settings.now = origNow;
|
||||
});
|
||||
|
||||
test('should parse weekly recurring rrule', () => {
|
||||
const schedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=US/Eastern:20220608T123000 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO',
|
||||
dtstart: '2022-06-13T16:30:00Z',
|
||||
timezone: 'US/Eastern',
|
||||
until: '',
|
||||
dtend: null,
|
||||
};
|
||||
|
||||
const parsed = parseRuleObj(schedule);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
startDate: '2022-06-13',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['week'],
|
||||
frequencyOptions: {
|
||||
week: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
occurrences: 1,
|
||||
endDate: '2022-06-02',
|
||||
endTime: '1:00 PM',
|
||||
daysOfWeek: [RRule.MO],
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should parse weekly recurring rrule with end date', () => {
|
||||
const schedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z',
|
||||
dtstart: '2020-04-02T18:45:00Z',
|
||||
timezone: 'America/New_York',
|
||||
};
|
||||
|
||||
const parsed = parseRuleObj(schedule);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
startDate: '2020-04-02',
|
||||
startTime: '2:45 PM',
|
||||
timezone: 'America/New_York',
|
||||
frequency: ['week'],
|
||||
frequencyOptions: {
|
||||
week: {
|
||||
interval: 1,
|
||||
end: 'onDate',
|
||||
occurrences: 1,
|
||||
endDate: '2021-01-01',
|
||||
endTime: '12:00 AM',
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should parse hourly rule with end date', () => {
|
||||
const schedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=US/Eastern:20220608T123000 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20230608T170000Z',
|
||||
dtstart: '2022-06-08T16:30:00Z',
|
||||
timezone: 'US/Eastern',
|
||||
};
|
||||
|
||||
const parsed = parseRuleObj(schedule);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
startDate: '2022-06-08',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['hour'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
interval: 1,
|
||||
end: 'onDate',
|
||||
occurrences: 1,
|
||||
endDate: '2023-06-08',
|
||||
endTime: '1:00 PM',
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: do we need to support this? It's technically invalid RRULE, but the
|
||||
// API has historically supported it as a special case (but cast to UTC?)
|
||||
test.skip('should parse hourly rule with end date in local time', () => {
|
||||
const schedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=US/Eastern:20220608T123000 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20230608T130000',
|
||||
dtstart: '2022-06-08T16:30:00',
|
||||
timezone: 'US/Eastern',
|
||||
};
|
||||
|
||||
const parsed = parseRuleObj(schedule);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
startDate: '2022-06-08',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['hour'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
interval: 1,
|
||||
end: 'onDate',
|
||||
occurrences: 1,
|
||||
endDate: '2023-06-08',
|
||||
endTime: '1:00 PM',
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('should parse non-recurring rrule', () => {
|
||||
const schedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20220610T130000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
dtstart: '2022-06-10T17:00:00Z',
|
||||
dtend: '2022-06-10T17:00:00Z',
|
||||
timezone: 'US/Eastern',
|
||||
until: '',
|
||||
};
|
||||
|
||||
expect(parseRuleObj(schedule)).toEqual({
|
||||
startDate: '2022-06-10',
|
||||
startTime: '1:00 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: [],
|
||||
frequencyOptions: {},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
});
|
||||
});
|
||||
|
||||
// buildRuleSet is well-tested; use it to verify this does the inverse
|
||||
test('should re-parse built complex schedule', () => {
|
||||
const values = {
|
||||
startDate: '2022-06-01',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['minute', 'month'],
|
||||
frequencyOptions: {
|
||||
minute: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
endDate: '2022-06-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'monday',
|
||||
runOnDayNumber: 1,
|
||||
endDate: '2022-06-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
};
|
||||
|
||||
const ruleSet = buildRuleSet(values);
|
||||
const parsed = parseRuleObj({
|
||||
rrule: ruleSet.toString(),
|
||||
dtstart: '2022-06-01T12:30:00',
|
||||
dtend: '2022-06-01T12:30:00',
|
||||
timezone: 'US/Eastern',
|
||||
});
|
||||
|
||||
expect(parsed).toEqual(values);
|
||||
});
|
||||
|
||||
test('should parse built complex schedule with end dates', () => {
|
||||
const rulesetString = `DTSTART;TZID=US/Eastern:20220601T123000
|
||||
RRULE:INTERVAL=2;FREQ=HOURLY;UNTIL=20260702T170000Z
|
||||
RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`;
|
||||
const values = {
|
||||
startDate: '2022-06-01',
|
||||
startTime: '12:30 PM',
|
||||
timezone: 'US/Eastern',
|
||||
frequency: ['hour', 'month'],
|
||||
frequencyOptions: {
|
||||
hour: {
|
||||
interval: 2,
|
||||
end: 'onDate',
|
||||
endDate: '2026-07-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
month: {
|
||||
interval: 1,
|
||||
end: 'onDate',
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'monday',
|
||||
runOnDayNumber: 1,
|
||||
endDate: '2026-06-02',
|
||||
endTime: '1:00 PM',
|
||||
occurrences: 1,
|
||||
},
|
||||
},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
};
|
||||
|
||||
const parsed = parseRuleObj({
|
||||
rrule: rulesetString,
|
||||
dtstart: '2022-06-01T16:30:00Z',
|
||||
dtend: '2026-06-07T16:30:00Z',
|
||||
timezone: 'US/Eastern',
|
||||
});
|
||||
|
||||
expect(parsed).toEqual(values);
|
||||
});
|
||||
});
|
||||
18
awx/ui/src/components/Schedule/shared/sortFrequencies.js
Normal file
18
awx/ui/src/components/Schedule/shared/sortFrequencies.js
Normal file
@ -0,0 +1,18 @@
|
||||
const ORDER = {
|
||||
minute: 1,
|
||||
hour: 2,
|
||||
day: 3,
|
||||
week: 4,
|
||||
month: 5,
|
||||
year: 6,
|
||||
};
|
||||
|
||||
export default function sortFrequencies(a, b) {
|
||||
if (ORDER[a] < ORDER[b]) {
|
||||
return -1;
|
||||
}
|
||||
if (ORDER[a] > ORDER[b]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -186,3 +186,20 @@ export function regExp() {
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function requiredPositiveInteger() {
|
||||
return (value) => {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value)) {
|
||||
return t`This field must be an integer`;
|
||||
}
|
||||
if (value < 1) {
|
||||
return t`This field must be greater than 0`;
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
return t`Select a value for this field`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user