mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
Adds functionality to add multiple rrules to a schedule and save the form
This commit is contained in:
parent
b99a434dee
commit
26a947ed31
@ -55,7 +55,6 @@ function DateTimePicker({ dateFieldName, timeFieldName, label }) {
|
||||
onChange={onDateChange}
|
||||
/>
|
||||
<TimePicker
|
||||
placeholder="hh:mm AM/PM"
|
||||
stepMinutes={15}
|
||||
aria-label={
|
||||
timeFieldName.startsWith('start') ? t`Start time` : t`End time`
|
||||
|
||||
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
93
awx/ui/src/components/Schedule/shared/FrequenciesList.js
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Switch,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Thead,
|
||||
Th,
|
||||
Tr,
|
||||
Td,
|
||||
} from '@patternfly/react-table';
|
||||
|
||||
import { useField } from 'formik';
|
||||
import ContentEmpty from 'components/ContentEmpty';
|
||||
|
||||
function FrequenciesList({ openWizard }) {
|
||||
const [isShowingRules, setIsShowingRules] = useState(true);
|
||||
const [frequencies] = useField('frequencies');
|
||||
const list = (freq) => (
|
||||
<Tr key={freq.rrule}>
|
||||
<Td>{freq.frequency}</Td>
|
||||
<Td>{freq.rrule}</Td>
|
||||
<Td>{t`End`}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={t`Click to toggle default value`}
|
||||
ouiaId={freq ? `${freq}-button` : 'new-freq-button'}
|
||||
onClick={() => {
|
||||
openWizard(true);
|
||||
}}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
onClick={() => {
|
||||
openWizard(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
{isShowingRules ? t`Add RRules` : t`Add Exception`}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Switch
|
||||
label={t`Occurances`}
|
||||
labelOff={t`Exceptions`}
|
||||
isChecked={isShowingRules}
|
||||
onChange={(isChecked) => {
|
||||
setIsShowingRules(isChecked);
|
||||
}}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<div css="overflow: auto">
|
||||
{frequencies.value[0].frequency === '' &&
|
||||
frequencies.value.length < 2 ? (
|
||||
<ContentEmpty title={t`RRules`} message={t`Add RRules`} />
|
||||
) : (
|
||||
<TableComposable aria-label={t`RRules`} ouiaId="rrules-list">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t`Frequency`}</Th>
|
||||
<Th>{t`RRule`}</Th>
|
||||
<Th>{t`Ending`}</Th>
|
||||
<Th>{t`Actions`}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>{frequencies.value.map((freq, i) => list(freq, i))}</Tbody>
|
||||
</TableComposable>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FrequenciesList;
|
||||
@ -1,568 +0,0 @@
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
|
||||
import { t, Trans, Plural } from '@lingui/macro';
|
||||
import { RRule } from 'rrule';
|
||||
import {
|
||||
Checkbox as _Checkbox,
|
||||
FormGroup,
|
||||
Radio,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import { required, requiredPositiveInteger } from 'util/validators';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import FormField from '../../FormField';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
|
||||
const RunOnRadio = styled(Radio)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:not(:last-of-type) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
select:not(:first-of-type) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const RunEveryLabel = styled.p`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Checkbox = styled(_Checkbox)`
|
||||
:not(:last-of-type) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FrequencyDetailSubform = ({ frequency, prefix, isException }) => {
|
||||
const id = prefix.replace('.', '-');
|
||||
const [runOnDayMonth] = useField({
|
||||
name: `${prefix}.runOnDayMonth`,
|
||||
});
|
||||
const [runOnDayNumber] = useField({
|
||||
name: `${prefix}.runOnDayNumber`,
|
||||
});
|
||||
const [runOnTheOccurrence] = useField({
|
||||
name: `${prefix}.runOnTheOccurrence`,
|
||||
});
|
||||
const [runOnTheDay] = useField({
|
||||
name: `${prefix}.runOnTheDay`,
|
||||
});
|
||||
const [runOnTheMonth] = useField({
|
||||
name: `${prefix}.runOnTheMonth`,
|
||||
});
|
||||
const [startDate] = useField(`${prefix}.startDate`);
|
||||
|
||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
|
||||
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: `${prefix}.end`,
|
||||
validate: required(t`Select a value for this field`),
|
||||
});
|
||||
const [interval, intervalMeta] = useField({
|
||||
name: `${prefix}.interval`,
|
||||
validate: requiredPositiveInteger(),
|
||||
});
|
||||
const [runOn, runOnMeta] = useField({
|
||||
name: `${prefix}.runOn`,
|
||||
validate: (val) => {
|
||||
if (frequency === 'month' || frequency === 'year') {
|
||||
return required(t`Select a value for this field`)(val);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const monthOptions = [
|
||||
{
|
||||
key: 'january',
|
||||
value: 1,
|
||||
label: t`January`,
|
||||
},
|
||||
{
|
||||
key: 'february',
|
||||
value: 2,
|
||||
label: t`February`,
|
||||
},
|
||||
{
|
||||
key: 'march',
|
||||
value: 3,
|
||||
label: t`March`,
|
||||
},
|
||||
{
|
||||
key: 'april',
|
||||
value: 4,
|
||||
label: t`April`,
|
||||
},
|
||||
{
|
||||
key: 'may',
|
||||
value: 5,
|
||||
label: t`May`,
|
||||
},
|
||||
{
|
||||
key: 'june',
|
||||
value: 6,
|
||||
label: t`June`,
|
||||
},
|
||||
{
|
||||
key: 'july',
|
||||
value: 7,
|
||||
label: t`July`,
|
||||
},
|
||||
{
|
||||
key: 'august',
|
||||
value: 8,
|
||||
label: t`August`,
|
||||
},
|
||||
{
|
||||
key: 'september',
|
||||
value: 9,
|
||||
label: t`September`,
|
||||
},
|
||||
{
|
||||
key: 'october',
|
||||
value: 10,
|
||||
label: t`October`,
|
||||
},
|
||||
{
|
||||
key: 'november',
|
||||
value: 11,
|
||||
label: t`November`,
|
||||
},
|
||||
{
|
||||
key: 'december',
|
||||
value: 12,
|
||||
label: t`December`,
|
||||
},
|
||||
];
|
||||
|
||||
const updateDaysOfWeek = (day, checked) => {
|
||||
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
||||
daysOfWeekHelpers.setTouched(true);
|
||||
if (checked) {
|
||||
newDaysOfWeek.push(day);
|
||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||
} else {
|
||||
daysOfWeekHelpers.setValue(
|
||||
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
case 'minute':
|
||||
return <Plural value={intervalValue} one="minute" other="minutes" />;
|
||||
case 'hour':
|
||||
return <Plural value={intervalValue} one="hour" other="hours" />;
|
||||
case 'day':
|
||||
return <Plural value={intervalValue} one="day" other="days" />;
|
||||
case 'week':
|
||||
return <Plural value={intervalValue} one="week" other="weeks" />;
|
||||
case 'month':
|
||||
return <Plural value={intervalValue} one="month" other="months" />;
|
||||
case 'year':
|
||||
return <Plural value={intervalValue} one="year" other="years" />;
|
||||
default:
|
||||
throw new Error(t`Frequency did not match an expected value`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p css="grid-column: 1/-1">
|
||||
<b>{getPeriodLabel()}</b>
|
||||
</p>
|
||||
<FormGroup
|
||||
name={`${prefix}.interval`}
|
||||
fieldId={`schedule-run-every-${id}`}
|
||||
helperTextInvalid={intervalMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!intervalMeta.touched || !intervalMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={isException ? t`Skip every` : t`Run every`}
|
||||
>
|
||||
<div css="display: flex">
|
||||
<TextInput
|
||||
css="margin-right: 10px;"
|
||||
id={`schedule-run-every-${id}`}
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
{...interval}
|
||||
onChange={(value, event) => {
|
||||
interval.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{frequency === 'week' && (
|
||||
<FormGroup
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
fieldId={`schedule-days-of-week-${id}`}
|
||||
helperTextInvalid={daysOfWeekMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!daysOfWeekMeta.touched || !daysOfWeekMeta.error
|
||||
? 'default'
|
||||
: 'error'
|
||||
}
|
||||
label={t`On days`}
|
||||
>
|
||||
<div css="display: flex">
|
||||
<Checkbox
|
||||
label={t`Sun`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SU, checked);
|
||||
}}
|
||||
aria-label={t`Sunday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.MO, checked);
|
||||
}}
|
||||
aria-label={t`Monday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TU, checked);
|
||||
}}
|
||||
aria-label={t`Tuesday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.WE, checked);
|
||||
}}
|
||||
aria-label={t`Wednesday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TH, checked);
|
||||
}}
|
||||
aria-label={t`Thursday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.FR, checked);
|
||||
}}
|
||||
aria-label={t`Friday`}
|
||||
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)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SA, checked);
|
||||
}}
|
||||
aria-label={t`Saturday`}
|
||||
id={`schedule-days-of-week-sat-${id}`}
|
||||
ouiaId={`schedule-days-of-week-sat-${id}`}
|
||||
name={`${prefix}.daysOfWeek`}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
)}
|
||||
{(frequency === 'month' || frequency === 'year') &&
|
||||
!Number.isNaN(new Date(startDate.value)) && (
|
||||
<FormGroup
|
||||
name={`${prefix}.runOn`}
|
||||
fieldId={`schedule-run-on-${id}`}
|
||||
helperTextInvalid={runOnMeta.error}
|
||||
isRequired
|
||||
validated={
|
||||
!runOnMeta.touched || !runOnMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={t`Run on`}
|
||||
>
|
||||
<RunOnRadio
|
||||
id={`schedule-run-on-day-${id}`}
|
||||
name={`${prefix}.runOn`}
|
||||
label={
|
||||
<div css="display: flex;align-items: center;">
|
||||
{frequency === 'month' && (
|
||||
<span
|
||||
id="radio-schedule-run-on-day"
|
||||
css="margin-right: 10px;"
|
||||
>
|
||||
<Trans>Day</Trans>
|
||||
</span>
|
||||
)}
|
||||
{frequency === 'year' && (
|
||||
<AnsibleSelect
|
||||
id={`schedule-run-on-day-month-${id}`}
|
||||
css="margin-right: 10px"
|
||||
isDisabled={runOn.value !== 'day'}
|
||||
data={monthOptions}
|
||||
{...runOnDayMonth}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
id={`schedule-run-on-day-number-${id}`}
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
step="1"
|
||||
isDisabled={runOn.value !== 'day'}
|
||||
{...runOnDayNumber}
|
||||
onChange={(value, event) => {
|
||||
runOnDayNumber.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
value="day"
|
||||
isChecked={runOn.value === 'day'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'day';
|
||||
runOn.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<RunOnRadio
|
||||
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}`}
|
||||
css="margin-right: 10px;"
|
||||
>
|
||||
<Trans>The</Trans>
|
||||
</span>
|
||||
<AnsibleSelect
|
||||
id={`schedule-run-on-the-occurrence-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={[
|
||||
{ value: 1, key: 'first', label: t`First` },
|
||||
{
|
||||
value: 2,
|
||||
key: 'second',
|
||||
label: t`Second`,
|
||||
},
|
||||
{ value: 3, key: 'third', label: t`Third` },
|
||||
{
|
||||
value: 4,
|
||||
key: 'fourth',
|
||||
label: t`Fourth`,
|
||||
},
|
||||
{ value: 5, key: 'fifth', label: t`Fifth` },
|
||||
{ value: -1, key: 'last', label: t`Last` },
|
||||
]}
|
||||
{...runOnTheOccurrence}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id={`schedule-run-on-the-day-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={[
|
||||
{
|
||||
value: 'sunday',
|
||||
key: 'sunday',
|
||||
label: t`Sunday`,
|
||||
},
|
||||
{
|
||||
value: 'monday',
|
||||
key: 'monday',
|
||||
label: t`Monday`,
|
||||
},
|
||||
{
|
||||
value: 'tuesday',
|
||||
key: 'tuesday',
|
||||
label: t`Tuesday`,
|
||||
},
|
||||
{
|
||||
value: 'wednesday',
|
||||
key: 'wednesday',
|
||||
label: t`Wednesday`,
|
||||
},
|
||||
{
|
||||
value: 'thursday',
|
||||
key: 'thursday',
|
||||
label: t`Thursday`,
|
||||
},
|
||||
{
|
||||
value: 'friday',
|
||||
key: 'friday',
|
||||
label: t`Friday`,
|
||||
},
|
||||
{
|
||||
value: 'saturday',
|
||||
key: 'saturday',
|
||||
label: t`Saturday`,
|
||||
},
|
||||
{ value: 'day', key: 'day', label: t`Day` },
|
||||
{
|
||||
value: 'weekday',
|
||||
key: 'weekday',
|
||||
label: t`Weekday`,
|
||||
},
|
||||
{
|
||||
value: 'weekendDay',
|
||||
key: 'weekendDay',
|
||||
label: t`Weekend day`,
|
||||
},
|
||||
]}
|
||||
{...runOnTheDay}
|
||||
/>
|
||||
{frequency === 'year' && (
|
||||
<>
|
||||
<span
|
||||
id={`of-schedule-run-on-the-month-${id}`}
|
||||
css="margin-left: 10px;"
|
||||
>
|
||||
<Trans>of</Trans>
|
||||
</span>
|
||||
<AnsibleSelect
|
||||
id={`schedule-run-on-the-month-${id}`}
|
||||
isDisabled={runOn.value !== 'the'}
|
||||
data={monthOptions}
|
||||
{...runOnTheMonth}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
value="the"
|
||||
isChecked={runOn.value === 'the'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'the';
|
||||
runOn.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
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-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`Never`}
|
||||
value="never"
|
||||
isChecked={end.value === 'never'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'never';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId={`end-never-radio-button-${id}`}
|
||||
/>
|
||||
<Radio
|
||||
id={`end-after-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`After number of occurrences`}
|
||||
value="after"
|
||||
isChecked={end.value === 'after'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'after';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId={`end-after-radio-button-${id}`}
|
||||
/>
|
||||
<Radio
|
||||
id={`end-on-date-${id}`}
|
||||
name={`${prefix}.end`}
|
||||
label={t`On date`}
|
||||
value="onDate"
|
||||
isChecked={end.value === 'onDate'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'onDate';
|
||||
end.onChange(event);
|
||||
}}
|
||||
ouiaId={`end-on-radio-button-${id}`}
|
||||
/>
|
||||
</FormGroup>
|
||||
{end?.value === 'after' && (
|
||||
<FormField
|
||||
id={`schedule-occurrences-${id}`}
|
||||
label={t`Occurrences`}
|
||||
name={`${prefix}.occurrences`}
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
isRequired
|
||||
/>
|
||||
)}
|
||||
{end?.value === 'onDate' && (
|
||||
<DateTimePicker
|
||||
dateFieldName={`${prefix}.endDate`}
|
||||
timeFieldName={`${prefix}.endTime`}
|
||||
label={t`End date/time`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrequencyDetailSubform;
|
||||
@ -1,30 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { arrayOf, string } from 'prop-types';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { RRule } from 'rrule';
|
||||
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
|
||||
|
||||
export default function FrequencySelect({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
placeholderText,
|
||||
children,
|
||||
}) {
|
||||
export default function FrequencySelect({ id, onBlur, placeholderText }) {
|
||||
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 [frequency, , frequencyHelpers] = useField('freq');
|
||||
|
||||
const onToggle = (val) => {
|
||||
if (!val) {
|
||||
@ -35,21 +17,26 @@ export default function FrequencySelect({
|
||||
|
||||
return (
|
||||
<Select
|
||||
variant={SelectVariant.checkbox}
|
||||
onSelect={onSelect}
|
||||
selections={value}
|
||||
onSelect={(e, v) => {
|
||||
frequencyHelpers.setValue(v);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
selections={frequency.value}
|
||||
placeholderText={placeholderText}
|
||||
onToggle={onToggle}
|
||||
value={frequency.value}
|
||||
isOpen={isOpen}
|
||||
ouiaId={`frequency-select-${id}`}
|
||||
onBlur={() => frequencyHelpers.setTouched(true)}
|
||||
>
|
||||
{children}
|
||||
<SelectOption value={RRule.MINUTELY}>{t`Minute`}</SelectOption>
|
||||
<SelectOption value={RRule.HOURLY}>{t`Hour`}</SelectOption>
|
||||
<SelectOption value={RRule.DAILY}>{t`Day`}</SelectOption>
|
||||
<SelectOption value={RRule.WEEKLY}>{t`Week`}</SelectOption>
|
||||
<SelectOption value={RRule.MONTHLY}>{t`Month`}</SelectOption>
|
||||
<SelectOption value={RRule.YEARLY}>{t`Year`}</SelectOption>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
FrequencySelect.propTypes = {
|
||||
value: arrayOf(string).isRequired,
|
||||
};
|
||||
|
||||
export { SelectOption, SelectVariant };
|
||||
|
||||
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
77
awx/ui/src/components/Schedule/shared/MonthandYearForm.js
Normal file
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import AnsibleSelect from 'components/AnsibleSelect';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
FormGroup,
|
||||
Checkbox as _Checkbox,
|
||||
Grid,
|
||||
GridItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { useField } from 'formik';
|
||||
import { bysetposOptions, monthOptions } from './scheduleFormHelpers';
|
||||
|
||||
const GroupWrapper = styled(FormGroup)`
|
||||
&& .pf-c-form__group-control {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&& .pf-c-form__group-label {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`;
|
||||
const Checkbox = styled(_Checkbox)`
|
||||
:not(:last-of-type) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
function MonthandYearForm({ id }) {
|
||||
const [bySetPos, , bySetPosHelpers] = useField('bysetpos');
|
||||
const [byMonth, , byMonthHelpers] = useField('bymonth');
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupWrapper
|
||||
fieldId={`schedule-run-on-${id}`}
|
||||
label={<b>{t`Run on a specific month`}</b>}
|
||||
>
|
||||
<Grid hasGutter>
|
||||
{monthOptions.map((month) => (
|
||||
<GridItem key={month.label} span={2} rowSpan={2}>
|
||||
<Checkbox
|
||||
label={month.label}
|
||||
isChecked={byMonth.value?.includes(month.value)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
byMonthHelpers.setValue([...byMonth.value, month.value]);
|
||||
} else {
|
||||
const removed = byMonth.value.filter(
|
||||
(i) => i !== month.value
|
||||
);
|
||||
byMonthHelpers.setValue(removed);
|
||||
}
|
||||
}}
|
||||
id={`bymonth-${month.label}`}
|
||||
ouiaId={`bymonth-${month.label}`}
|
||||
name="bymonth"
|
||||
/>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
</GroupWrapper>
|
||||
<GroupWrapper
|
||||
label={<b>{t`Run on a specific week day at monthly intervals`}</b>}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id={`schedule-run-on-the-occurrence-${id}`}
|
||||
data={bysetposOptions}
|
||||
{...bySetPos}
|
||||
onChange={(e, v) => {
|
||||
bySetPosHelpers.setValue(v);
|
||||
}}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default MonthandYearForm;
|
||||
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
45
awx/ui/src/components/Schedule/shared/OrdinalDayForm.js
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
import { FormGroup, TextInput } from '@patternfly/react-core';
|
||||
|
||||
const GroupWrapper = styled(FormGroup)`
|
||||
&& .pf-c-form__group-control {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&& .pf-c-form__group-label {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
function OrdinalDayForm() {
|
||||
const [byMonthDay] = useField('bymonthday');
|
||||
const [byYearDay] = useField('byyearday');
|
||||
return (
|
||||
<GroupWrapper
|
||||
label={<b>{t`On a specific number day`}</b>}
|
||||
name="ordinalDay"
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t`Run on a day of month`}
|
||||
aria-label={t`Type a numbered day`}
|
||||
type="number"
|
||||
onChange={(value, event) => {
|
||||
byMonthDay.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={t`Run on a day of year`}
|
||||
aria-label={t`Type a numbered day`}
|
||||
type="number"
|
||||
onChange={(value, event) => {
|
||||
byYearDay.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrdinalDayForm;
|
||||
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
67
awx/ui/src/components/Schedule/shared/ScheduleEndForm.js
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup, Radio } from '@patternfly/react-core';
|
||||
import FormField from 'components/FormField';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
|
||||
function ScheduleEndForm() {
|
||||
const [endType, , { setValue }] = useField('endingType');
|
||||
const [count] = useField('count');
|
||||
return (
|
||||
<>
|
||||
<FormGroup name="end" label={t`End`}>
|
||||
<Radio
|
||||
id="endNever"
|
||||
name={t`Never End`}
|
||||
label={t`Never`}
|
||||
value="never"
|
||||
isChecked={endType.value === 'never'}
|
||||
onChange={() => {
|
||||
setValue('never');
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
name="Count"
|
||||
id="after"
|
||||
label={t`After number of occurrences`}
|
||||
value="after"
|
||||
isChecked={endType.value === 'after'}
|
||||
onChange={() => {
|
||||
setValue('after');
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
name="End Date"
|
||||
label={t`On date`}
|
||||
value="onDate"
|
||||
id="endDate"
|
||||
isChecked={endType.value === 'onDate'}
|
||||
onChange={() => {
|
||||
setValue('onDate');
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{endType.value === 'after' && (
|
||||
<FormField
|
||||
label={t`Occurrences`}
|
||||
name="count"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
isRequired
|
||||
{...count}
|
||||
/>
|
||||
)}
|
||||
{endType.value === 'onDate' && (
|
||||
<DateTimePicker
|
||||
dateFieldName="endDate"
|
||||
timeFieldName="endTime"
|
||||
label={t`End date/time`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScheduleEndForm;
|
||||
@ -18,14 +18,9 @@ import SchedulePromptableFields from './SchedulePromptableFields';
|
||||
import ScheduleFormFields from './ScheduleFormFields';
|
||||
import UnsupportedScheduleForm from './UnsupportedScheduleForm';
|
||||
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
|
||||
import buildRuleObj from './buildRuleObj';
|
||||
import buildRuleSet from './buildRuleSet';
|
||||
|
||||
const NUM_DAYS_PER_FREQUENCY = {
|
||||
week: 7,
|
||||
month: 31,
|
||||
year: 365,
|
||||
};
|
||||
import ScheduleFormWizard from './ScheduleFormWizard';
|
||||
import FrequenciesList from './FrequenciesList';
|
||||
// import { validateSchedule } from './scheduleFormHelpers';
|
||||
|
||||
function ScheduleForm({
|
||||
hasDaysToKeepField,
|
||||
@ -40,15 +35,16 @@ function ScheduleForm({
|
||||
}) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
const [isScheduleWizardOpen, setIsScheduleWizardOpen] = useState(false);
|
||||
const originalLabels = useRef([]);
|
||||
const originalInstanceGroups = useRef([]);
|
||||
|
||||
let rruleError;
|
||||
const now = DateTime.now();
|
||||
|
||||
const closestQuarterHour = DateTime.fromMillis(
|
||||
Math.ceil(now.ts / 900000) * 900000
|
||||
);
|
||||
const tomorrow = closestQuarterHour.plus({ days: 1 });
|
||||
const isTemplate =
|
||||
resource.type === 'workflow_job_template' ||
|
||||
resource.type === 'job_template';
|
||||
@ -283,69 +279,10 @@ 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 = {
|
||||
description: schedule.description || '',
|
||||
frequency: [],
|
||||
frequencies: [],
|
||||
exceptionFrequency: [],
|
||||
frequencyOptions: initialFrequencyOptions,
|
||||
exceptionOptions: initialFrequencyOptions,
|
||||
name: schedule.name || '',
|
||||
startDate: currentDate,
|
||||
startTime: time,
|
||||
@ -367,11 +304,9 @@ function ScheduleForm({
|
||||
}
|
||||
initialValues.daysToKeep = initialDaysToKeep;
|
||||
}
|
||||
|
||||
let overriddenValues = {};
|
||||
if (schedule.rrule) {
|
||||
try {
|
||||
overriddenValues = parseRuleObj(schedule);
|
||||
parseRuleObj(schedule);
|
||||
} catch (error) {
|
||||
if (error instanceof UnsupportedRRuleError) {
|
||||
return (
|
||||
@ -394,89 +329,33 @@ function ScheduleForm({
|
||||
if (contentLoading) {
|
||||
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.fromFormat(
|
||||
`${values.startDate} ${values.startTime}`,
|
||||
'yyyy-LL-dd h:mm a'
|
||||
).toMillis() >=
|
||||
DateTime.fromFormat(
|
||||
`${options.endDate} ${options.endTime}`,
|
||||
'yyyy-LL-dd h:mm a'
|
||||
).toMillis()
|
||||
) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
||||
errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const frequencies = [];
|
||||
frequencies.push(parseRuleObj(schedule));
|
||||
return (
|
||||
<Config>
|
||||
{() => (
|
||||
<Formik
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
...overriddenValues,
|
||||
frequencyOptions: {
|
||||
...initialValues.frequencyOptions,
|
||||
...overriddenValues.frequencyOptions,
|
||||
},
|
||||
exceptionOptions: {
|
||||
...initialValues.exceptionOptions,
|
||||
...overriddenValues.exceptionOptions,
|
||||
},
|
||||
name: schedule.name || '',
|
||||
description: schedule.description || '',
|
||||
frequencies: frequencies || [],
|
||||
freq: RRule.DAILY,
|
||||
interval: 1,
|
||||
wkst: RRule.SU,
|
||||
byweekday: [],
|
||||
byweekno: [],
|
||||
bymonth: [],
|
||||
bymonthday: '',
|
||||
byyearday: '',
|
||||
bysetpos: '',
|
||||
until: schedule.until || null,
|
||||
endDate: currentDate,
|
||||
endTime: time,
|
||||
count: 1,
|
||||
endingType: 'never',
|
||||
timezone: schedule.timezone || now.zoneName,
|
||||
startDate: currentDate,
|
||||
startTime: time,
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
submitSchedule(
|
||||
@ -488,73 +367,90 @@ function ScheduleForm({
|
||||
credentials
|
||||
);
|
||||
}}
|
||||
validate={validate}
|
||||
validate={() => {}}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<ScheduleFormFields
|
||||
hasDaysToKeepField={hasDaysToKeepField}
|
||||
zoneOptions={zoneOptions}
|
||||
zoneLinks={zoneLinks}
|
||||
/>
|
||||
{isWizardOpen && (
|
||||
<SchedulePromptableFields
|
||||
schedule={schedule}
|
||||
credentials={credentials}
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
onCloseWizard={() => {
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(false);
|
||||
}}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
labels={originalLabels.current}
|
||||
instanceGroups={originalInstanceGroups.current}
|
||||
<>
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<ScheduleFormFields
|
||||
hasDaysToKeepField={hasDaysToKeepField}
|
||||
zoneOptions={zoneOptions}
|
||||
zoneLinks={zoneLinks}
|
||||
/>
|
||||
)}
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormFullWidthLayout>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
ouiaId="schedule-form-save-button"
|
||||
aria-label={t`Save`}
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={formik.handleSubmit}
|
||||
isDisabled={isSaveDisabled}
|
||||
>
|
||||
{t`Save`}
|
||||
</Button>
|
||||
|
||||
{isTemplate && showPromptButton && (
|
||||
{isWizardOpen && (
|
||||
<SchedulePromptableFields
|
||||
schedule={schedule}
|
||||
credentials={credentials}
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
onCloseWizard={() => {
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(false);
|
||||
}}
|
||||
resourceDefaultCredentials={resourceDefaultCredentials}
|
||||
labels={originalLabels.current}
|
||||
instanceGroups={originalInstanceGroups.current}
|
||||
/>
|
||||
)}
|
||||
<FormFullWidthLayout>
|
||||
<FrequenciesList openWizard={setIsScheduleWizardOpen} />
|
||||
</FormFullWidthLayout>
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormFullWidthLayout>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
ouiaId="schedule-form-prompt-button"
|
||||
ouiaId="schedule-form-save-button"
|
||||
aria-label={t`Save`}
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={formik.handleSubmit}
|
||||
isDisabled={isSaveDisabled}
|
||||
>
|
||||
{t`Save`}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
>{t`Preview occurances`}</Button>
|
||||
|
||||
{isTemplate && showPromptButton && (
|
||||
<Button
|
||||
ouiaId="schedule-form-prompt-button"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
aria-label={t`Prompt`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{t`Prompt`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
ouiaId="schedule-form-cancel-button"
|
||||
aria-label={t`Cancel`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
aria-label={t`Prompt`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t`Prompt`}
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
ouiaId="schedule-form-cancel-button"
|
||||
aria-label={t`Cancel`}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormFullWidthLayout>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
</ActionGroup>
|
||||
</FormFullWidthLayout>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
{isScheduleWizardOpen && (
|
||||
<ScheduleFormWizard
|
||||
staticFormFormkik={formik}
|
||||
isOpen={isScheduleWizardOpen}
|
||||
handleSave={() => {}}
|
||||
setIsOpen={setIsScheduleWizardOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
@ -575,24 +471,3 @@ ScheduleForm.defaultProps = {
|
||||
};
|
||||
|
||||
export default ScheduleForm;
|
||||
|
||||
function scheduleHasInstances(values) {
|
||||
let rangeToCheck = 1;
|
||||
values.frequency.forEach((freq) => {
|
||||
if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
|
||||
rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
|
||||
}
|
||||
});
|
||||
|
||||
const ruleSet = buildRuleSet(values, true);
|
||||
const startDate = DateTime.fromISO(values.startDate);
|
||||
const endDate = startDate.plus({ days: rangeToCheck });
|
||||
const instances = ruleSet.between(
|
||||
startDate.toJSDate(),
|
||||
endDate.toJSDate(),
|
||||
true,
|
||||
(date, i) => i === 0
|
||||
);
|
||||
|
||||
return instances.length > 0;
|
||||
}
|
||||
|
||||
@ -1,41 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useField } from 'formik';
|
||||
import { FormGroup, Title } from '@patternfly/react-core';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import 'styled-components/macro';
|
||||
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 getHelpText 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,
|
||||
setTimeZone,
|
||||
}) {
|
||||
const helpText = getHelpText();
|
||||
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]) {
|
||||
@ -46,6 +32,7 @@ export default function ScheduleFormFields({
|
||||
setTimezoneMessage('');
|
||||
}
|
||||
timezone.onChange(event, selectedValue);
|
||||
setTimeZone(zoneLinks(selectedValue));
|
||||
};
|
||||
let timezoneValidatedStatus = 'default';
|
||||
if (timezoneMeta.touched && timezoneMeta.error) {
|
||||
@ -55,16 +42,6 @@ export default function ScheduleFormFields({
|
||||
}
|
||||
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
|
||||
@ -103,33 +80,7 @@ export default function ScheduleFormFields({
|
||||
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"
|
||||
@ -140,68 +91,6 @@ export default function ScheduleFormFields({
|
||||
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"
|
||||
css="margin-top: var(--pf-c-card--child--PaddingRight)"
|
||||
>{t`Exceptions`}</Title>
|
||||
<FormColumnLayout stacked>
|
||||
<FormGroup
|
||||
name="exceptions"
|
||||
fieldId="exception-frequency"
|
||||
helperTextInvalid={exceptionFrequencyMeta.error}
|
||||
validated={
|
||||
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
|
||||
? 'default'
|
||||
: 'error'
|
||||
}
|
||||
label={t`Add exceptions`}
|
||||
>
|
||||
<FrequencySelect
|
||||
id="exception-frequency"
|
||||
onChange={updateFrequency(exceptionFrequencyHelper.setValue)}
|
||||
value={exceptionFrequency.value}
|
||||
placeholderText={
|
||||
exceptionFrequency.value.length
|
||||
? t`Select frequency`
|
||||
: 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>
|
||||
</FormColumnLayout>
|
||||
{exceptionFrequency.value.map((val) => (
|
||||
<FormColumnLayout key={val} stacked>
|
||||
<FrequencyDetailSubform
|
||||
frequency={val}
|
||||
prefix={`exceptionOptions.${val}`}
|
||||
isException
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
))}
|
||||
</SubFormLayout>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
199
awx/ui/src/components/Schedule/shared/ScheduleFormWizard.js
Normal file
@ -0,0 +1,199 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
Title,
|
||||
Wizard,
|
||||
WizardContextConsumer,
|
||||
WizardFooter,
|
||||
} from '@patternfly/react-core';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { RRule } from 'rrule';
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
import { DateTime } from 'luxon';
|
||||
import { formatDateString } from 'util/dates';
|
||||
import FrequencySelect from './FrequencySelect';
|
||||
import MonthandYearForm from './MonthandYearForm';
|
||||
import OrdinalDayForm from './OrdinalDayForm';
|
||||
import WeekdayForm from './WeekdayForm';
|
||||
import ScheduleEndForm from './ScheduleEndForm';
|
||||
import parseRuleObj from './parseRuleObj';
|
||||
import { buildDtStartObj } from './buildRuleObj';
|
||||
|
||||
const GroupWrapper = styled(FormGroup)`
|
||||
&& .pf-c-form__group-control {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&& .pf-c-form__group-label {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
function ScheduleFormWizard({ isOpen, setIsOpen }) {
|
||||
const { values, resetForm, initialValues } = useFormikContext();
|
||||
const [freq, freqMeta] = useField('freq');
|
||||
const [{ value: frequenciesValue }] = useField('frequencies');
|
||||
const [interval, , intervalHelpers] = useField('interval');
|
||||
|
||||
const handleSubmit = (goToStepById) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
endingType,
|
||||
endTime,
|
||||
endDate,
|
||||
timezone,
|
||||
startDate,
|
||||
startTime,
|
||||
frequencies,
|
||||
...rest
|
||||
} = values;
|
||||
if (endingType === 'onDate') {
|
||||
const dt = DateTime.fromFormat(
|
||||
`${endDate} ${endTime}`,
|
||||
'yyyy-MM-dd h:mm a',
|
||||
{
|
||||
zone: timezone,
|
||||
}
|
||||
);
|
||||
rest.until = formatDateString(dt, timezone);
|
||||
|
||||
delete rest.count;
|
||||
}
|
||||
if (endingType === 'never') delete rest.count;
|
||||
|
||||
const rule = new RRule(rest);
|
||||
|
||||
const start = buildDtStartObj({
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency: values.freq,
|
||||
});
|
||||
const newFrequency = parseRuleObj({
|
||||
timezone,
|
||||
frequency: freq.value,
|
||||
rrule: rule.toString(),
|
||||
dtstart: start,
|
||||
});
|
||||
if (goToStepById) {
|
||||
goToStepById(1);
|
||||
}
|
||||
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
description: values.description,
|
||||
name: values.name,
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequencies: frequenciesValue[0].frequency.length
|
||||
? [...frequenciesValue, newFrequency]
|
||||
: [newFrequency],
|
||||
},
|
||||
});
|
||||
};
|
||||
const CustomFooter = (
|
||||
<WizardFooter>
|
||||
<WizardContextConsumer>
|
||||
{({ activeStep, onNext, onBack, goToStepById }) => (
|
||||
<>
|
||||
{activeStep.id === 2 ? (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
handleSubmit(true, goToStepById);
|
||||
}}
|
||||
>{t`Finish and create new`}</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
handleSubmit(false);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>{t`Finish and close`}</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="primary" onClick={onNext}>{t`Next`}</Button>
|
||||
)}
|
||||
|
||||
<Button variant="secondary" onClick={onBack}>{t`Back`}</Button>
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
description: values.description,
|
||||
name: values.name,
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequencies: values.frequencies,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>{t`Cancel`}</Button>
|
||||
</>
|
||||
)}
|
||||
</WizardContextConsumer>
|
||||
</WizardFooter>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
onClose={() => setIsOpen(false)}
|
||||
isOpen={isOpen}
|
||||
footer={CustomFooter}
|
||||
steps={[
|
||||
{
|
||||
key: 'frequency',
|
||||
name: 'Frequency',
|
||||
id: 1,
|
||||
component: (
|
||||
<>
|
||||
<Title size="md" headingLevel="h4">{t`Repeat frequency`}</Title>
|
||||
<GroupWrapper
|
||||
name="freq"
|
||||
fieldId="schedule-frequency"
|
||||
isRequired
|
||||
helperTextInvalid={freqMeta.error}
|
||||
validated={
|
||||
!freqMeta.touched || !freqMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={<b>{t`Frequency`}</b>}
|
||||
>
|
||||
<FrequencySelect />
|
||||
</GroupWrapper>
|
||||
<GroupWrapper isRequired label={<b>{t`Interval`}</b>}>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={interval.value}
|
||||
placeholder={t`Choose an interval for the schedule`}
|
||||
aria-label={t`Choose an interval for the schedule`}
|
||||
onChange={(v) => intervalHelpers.setValue(v)}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
<WeekdayForm />
|
||||
<MonthandYearForm />
|
||||
<OrdinalDayForm />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'End',
|
||||
key: 'end',
|
||||
id: 2,
|
||||
component: <ScheduleEndForm />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default ScheduleFormWizard;
|
||||
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
164
awx/ui/src/components/Schedule/shared/WeekdayForm.js
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Checkbox as _Checkbox,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from '@patternfly/react-core';
|
||||
import { useField } from 'formik';
|
||||
import { RRule } from 'rrule';
|
||||
import styled from 'styled-components';
|
||||
import { weekdayOptions } from './scheduleFormHelpers';
|
||||
|
||||
const Checkbox = styled(_Checkbox)`
|
||||
:not(:last-of-type) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
const GroupWrapper = styled(FormGroup)`
|
||||
&& .pf-c-form__group-control {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&& .pf-c-form__group-label {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
function WeekdayForm({ id }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField('byweekday');
|
||||
const [weekStartDay, , weekStartDayHelpers] = useField('wkst');
|
||||
const updateDaysOfWeek = (day, checked) => {
|
||||
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
|
||||
daysOfWeekHelpers.setTouched(true);
|
||||
|
||||
if (checked) {
|
||||
newDaysOfWeek.push(day);
|
||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||
} else {
|
||||
daysOfWeekHelpers.setValue(
|
||||
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<GroupWrapper
|
||||
name="wkst"
|
||||
label={<b>{t`Select the first day of the week`}</b>}
|
||||
>
|
||||
<Select
|
||||
onSelect={(e, value) => {
|
||||
weekStartDayHelpers.setValue(value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onBlur={() => setIsOpen(false)}
|
||||
selections={weekStartDay.value}
|
||||
onToggle={(isopen) => setIsOpen(isopen)}
|
||||
isOpen={isOpen}
|
||||
id={`schedule-run-on-the-day-${id}`}
|
||||
onChange={(e, v) => {
|
||||
weekStartDayHelpers.setValue(v);
|
||||
}}
|
||||
{...weekStartDay}
|
||||
>
|
||||
{weekdayOptions.map(({ key, value, label }) => (
|
||||
<SelectOption key={key} value={value}>
|
||||
{label}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
</GroupWrapper>
|
||||
<GroupWrapper
|
||||
name="byweekday"
|
||||
fieldId={`schedule-days-of-week-${id}`}
|
||||
helperTextInvalid={daysOfWeekMeta.error}
|
||||
validated={
|
||||
!daysOfWeekMeta.touched || !daysOfWeekMeta.error ? 'default' : 'error'
|
||||
}
|
||||
label={<b>{t`On selected day(s) of the week`}</b>}
|
||||
>
|
||||
<Checkbox
|
||||
label={t`Sun`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.SU)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SU, checked);
|
||||
}}
|
||||
aria-label={t`Sunday`}
|
||||
id={`schedule-days-of-week-sun-${id}`}
|
||||
ouiaId={`schedule-days-of-week-sun-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Mon`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.MO)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.MO, checked);
|
||||
}}
|
||||
aria-label={t`Monday`}
|
||||
id={`schedule-days-of-week-mon-${id}`}
|
||||
ouiaId={`schedule-days-of-week-mon-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Tue`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.TU)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TU, checked);
|
||||
}}
|
||||
aria-label={t`Tuesday`}
|
||||
id={`schedule-days-of-week-tue-${id}`}
|
||||
ouiaId={`schedule-days-of-week-tue-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Wed`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.WE)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.WE, checked);
|
||||
}}
|
||||
aria-label={t`Wednesday`}
|
||||
id={`schedule-days-of-week-wed-${id}`}
|
||||
ouiaId={`schedule-days-of-week-wed-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Thu`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.TH)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.TH, checked);
|
||||
}}
|
||||
aria-label={t`Thursday`}
|
||||
id={`schedule-days-of-week-thu-${id}`}
|
||||
ouiaId={`schedule-days-of-week-thu-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Fri`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.FR)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.FR, checked);
|
||||
}}
|
||||
aria-label={t`Friday`}
|
||||
id={`schedule-days-of-week-fri-${id}`}
|
||||
ouiaId={`schedule-days-of-week-fri-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={t`Sat`}
|
||||
isChecked={daysOfWeek.value?.includes(RRule.SA)}
|
||||
onChange={(checked) => {
|
||||
updateDaysOfWeek(RRule.SA, checked);
|
||||
}}
|
||||
aria-label={t`Saturday`}
|
||||
id={`schedule-days-of-week-sat-${id}`}
|
||||
ouiaId={`schedule-days-of-week-sat-${id}`}
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
</GroupWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default WeekdayForm;
|
||||
@ -1,7 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { RRule } from 'rrule';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getRRuleDayConstants } from 'util/dates';
|
||||
|
||||
window.RRule = RRule;
|
||||
window.DateTime = DateTime;
|
||||
@ -22,7 +20,7 @@ export function buildDtStartObj(values) {
|
||||
startHour
|
||||
)}${pad(startMinute)}00`;
|
||||
const rruleString = values.timezone
|
||||
? `DTSTART;TZID=${values.timezone}:${dateString}`
|
||||
? `DTSTART;TZID=${values.timezone}${dateString}`
|
||||
: `DTSTART:${dateString}Z`;
|
||||
const rule = RRule.fromString(rruleString);
|
||||
|
||||
@ -38,7 +36,8 @@ function pad(num) {
|
||||
|
||||
export default function buildRuleObj(values, includeStart) {
|
||||
const ruleObj = {
|
||||
interval: values.interval,
|
||||
interval: values.interval || 1,
|
||||
freq: values.freq,
|
||||
};
|
||||
|
||||
if (includeStart) {
|
||||
@ -49,68 +48,6 @@ export default function buildRuleObj(values, includeStart) {
|
||||
);
|
||||
}
|
||||
|
||||
switch (values.frequency) {
|
||||
case 'none':
|
||||
ruleObj.count = 1;
|
||||
ruleObj.freq = RRule.MINUTELY;
|
||||
break;
|
||||
case 'minute':
|
||||
ruleObj.freq = RRule.MINUTELY;
|
||||
break;
|
||||
case 'hour':
|
||||
ruleObj.freq = RRule.HOURLY;
|
||||
break;
|
||||
case 'day':
|
||||
ruleObj.freq = RRule.DAILY;
|
||||
break;
|
||||
case 'week':
|
||||
ruleObj.freq = RRule.WEEKLY;
|
||||
ruleObj.byweekday = values.daysOfWeek;
|
||||
break;
|
||||
case 'month':
|
||||
ruleObj.freq = RRule.MONTHLY;
|
||||
if (values.runOn === 'day') {
|
||||
ruleObj.bymonthday = values.runOnDayNumber;
|
||||
} else if (values.runOn === 'the') {
|
||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
||||
}
|
||||
break;
|
||||
case 'year':
|
||||
ruleObj.freq = RRule.YEARLY;
|
||||
if (values.runOn === 'day') {
|
||||
ruleObj.bymonth = parseInt(values.runOnDayMonth, 10);
|
||||
ruleObj.bymonthday = values.runOnDayNumber;
|
||||
} else if (values.runOn === 'the') {
|
||||
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
|
||||
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
|
||||
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(t`Frequency did not match an expected value`);
|
||||
}
|
||||
|
||||
if (values.frequency !== 'none') {
|
||||
switch (values.end) {
|
||||
case 'never':
|
||||
break;
|
||||
case 'after':
|
||||
ruleObj.count = values.occurrences;
|
||||
break;
|
||||
case 'onDate': {
|
||||
ruleObj.until = buildDateTime(
|
||||
values.endDate,
|
||||
values.endTime,
|
||||
values.timezone
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(t`End did not match an expected value (${values.end})`);
|
||||
}
|
||||
}
|
||||
|
||||
return ruleObj;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { RRule, RRuleSet } from 'rrule';
|
||||
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
|
||||
import { FREQUENCIESCONSTANTS } from './scheduleFormHelpers';
|
||||
|
||||
window.RRuleSet = RRuleSet;
|
||||
|
||||
@ -12,42 +13,31 @@ export default function buildRuleSet(values, useUTCStart) {
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency: values.freq,
|
||||
});
|
||||
set.rrule(startRule);
|
||||
}
|
||||
|
||||
if (values.frequency.length === 0) {
|
||||
const rule = buildRuleObj(
|
||||
{
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
},
|
||||
useUTCStart
|
||||
);
|
||||
set.rrule(new RRule(rule));
|
||||
}
|
||||
|
||||
frequencies.forEach((frequency) => {
|
||||
if (!values.frequency.includes(frequency)) {
|
||||
values.frequencies.forEach(({ frequency, rrule }) => {
|
||||
if (!frequencies.includes(frequency)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = buildRuleObj(
|
||||
{
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency,
|
||||
...values.frequencyOptions[frequency],
|
||||
freq: FREQUENCIESCONSTANTS[frequency],
|
||||
rrule,
|
||||
},
|
||||
useUTCStart
|
||||
true
|
||||
);
|
||||
|
||||
set.rrule(new RRule(rule));
|
||||
});
|
||||
|
||||
frequencies.forEach((frequency) => {
|
||||
values.exceptions?.forEach(({ frequency, rrule }) => {
|
||||
if (!values.exceptionFrequency?.includes(frequency)) {
|
||||
return;
|
||||
}
|
||||
@ -56,8 +46,8 @@ export default function buildRuleSet(values, useUTCStart) {
|
||||
startDate: values.startDate,
|
||||
startTime: values.startTime,
|
||||
timezone: values.timezone,
|
||||
frequency,
|
||||
...values.exceptionOptions[frequency],
|
||||
freq: FREQUENCIESCONSTANTS[frequency],
|
||||
rrule,
|
||||
},
|
||||
useUTCStart
|
||||
);
|
||||
|
||||
@ -12,12 +12,14 @@ export class UnsupportedRRuleError extends Error {
|
||||
|
||||
export default function parseRuleObj(schedule) {
|
||||
let values = {
|
||||
frequency: [],
|
||||
frequencyOptions: {},
|
||||
exceptionFrequency: [],
|
||||
exceptionOptions: {},
|
||||
frequency: '',
|
||||
rrules: '',
|
||||
timezone: schedule.timezone,
|
||||
};
|
||||
if (Object.values(schedule).length === 0) {
|
||||
return values;
|
||||
}
|
||||
|
||||
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
|
||||
forceset: true,
|
||||
});
|
||||
@ -40,25 +42,9 @@ export default function parseRuleObj(schedule) {
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
@ -81,27 +67,12 @@ const frequencyTypes = {
|
||||
[RRule.YEARLY]: 'year',
|
||||
};
|
||||
|
||||
function parseRrule(rruleString, schedule, values) {
|
||||
const { frequency, options } = parseRule(
|
||||
rruleString,
|
||||
schedule,
|
||||
values.exceptionFrequency
|
||||
);
|
||||
function parseRrule(rruleString, schedule) {
|
||||
const { frequency } = parseRule(rruleString, schedule);
|
||||
|
||||
if (values.frequencyOptions[frequency]) {
|
||||
throw new UnsupportedRRuleError(
|
||||
'Duplicate exception frequency types not supported'
|
||||
);
|
||||
}
|
||||
const freq = { frequency, rrule: rruleString };
|
||||
|
||||
return {
|
||||
...values,
|
||||
frequency: [...values.frequency, frequency].sort(sortFrequencies),
|
||||
frequencyOptions: {
|
||||
...values.frequencyOptions,
|
||||
[frequency]: options,
|
||||
},
|
||||
};
|
||||
return freq;
|
||||
}
|
||||
|
||||
function parseExRule(exruleString, schedule, values) {
|
||||
@ -129,20 +100,10 @@ function parseExRule(exruleString, schedule, values) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseRule(ruleString, schedule, frequencies) {
|
||||
function parseRule(ruleString, schedule) {
|
||||
const {
|
||||
origOptions: {
|
||||
bymonth,
|
||||
bymonthday,
|
||||
bysetpos,
|
||||
byweekday,
|
||||
count,
|
||||
freq,
|
||||
interval,
|
||||
until,
|
||||
},
|
||||
origOptions: { count, freq, interval, until, ...rest },
|
||||
} = RRule.fromString(ruleString);
|
||||
|
||||
const now = DateTime.now();
|
||||
const closestQuarterHour = DateTime.fromMillis(
|
||||
Math.ceil(now.ts / 900000) * 900000
|
||||
@ -156,17 +117,17 @@ function parseRule(ruleString, schedule, frequencies) {
|
||||
endTime: time,
|
||||
occurrences: 1,
|
||||
interval: 1,
|
||||
end: 'never',
|
||||
endingType: 'never',
|
||||
};
|
||||
|
||||
if (until) {
|
||||
options.end = 'onDate';
|
||||
if (until?.length) {
|
||||
options.endingType = '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.endingType = 'after';
|
||||
options.occurrences = count;
|
||||
}
|
||||
|
||||
@ -178,101 +139,10 @@ function parseRule(ruleString, schedule, frequencies) {
|
||||
throw new Error(`Unexpected rrule frequency: ${freq}`);
|
||||
}
|
||||
const frequency = frequencyTypes[freq];
|
||||
if (frequencies.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;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
frequency,
|
||||
options,
|
||||
...options,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
232
awx/ui/src/components/Schedule/shared/scheduleFormHelpers.js
Normal file
@ -0,0 +1,232 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { RRule } from 'rrule';
|
||||
import buildRuleObj from './buildRuleObj';
|
||||
import buildRuleSet from './buildRuleSet';
|
||||
|
||||
// const NUM_DAYS_PER_FREQUENCY = {
|
||||
// week: 7,
|
||||
// month: 31,
|
||||
// year: 365,
|
||||
// };
|
||||
// const validateSchedule = () =>
|
||||
// const errors = {};
|
||||
|
||||
// values.frequencies.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.fromFormat(
|
||||
// `${values.startDate} ${values.startTime}`,
|
||||
// 'yyyy-LL-dd h:mm a'
|
||||
// ).toMillis() >=
|
||||
// DateTime.fromFormat(
|
||||
// `${options.endDate} ${options.endTime}`,
|
||||
// 'yyyy-LL-dd h:mm a'
|
||||
// ).toMillis()
|
||||
// ) {
|
||||
// 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,
|
||||
// frequencies: 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;
|
||||
// }
|
||||
// });
|
||||
|
||||
// if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
|
||||
// errors.exceptionFrequency = t`This schedule has no occurrences due to the
|
||||
// selected exceptions.`;
|
||||
// }
|
||||
|
||||
// ({});
|
||||
// function scheduleHasInstances(values) {
|
||||
// let rangeToCheck = 1;
|
||||
// values.frequencies.forEach((freq) => {
|
||||
// if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
|
||||
// rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
|
||||
// }
|
||||
// });
|
||||
|
||||
// const ruleSet = buildRuleSet(values, true);
|
||||
// const startDate = DateTime.fromISO(values.startDate);
|
||||
// const endDate = startDate.plus({ days: rangeToCheck });
|
||||
// const instances = ruleSet.between(
|
||||
// startDate.toJSDate(),
|
||||
// endDate.toJSDate(),
|
||||
// true,
|
||||
// (date, i) => i === 0
|
||||
// );
|
||||
|
||||
// return instances.length > 0;
|
||||
// }
|
||||
|
||||
const bysetposOptions = [
|
||||
{ value: '', key: 'none', label: 'None' },
|
||||
{ value: 1, key: 'first', label: t`First` },
|
||||
{
|
||||
value: 2,
|
||||
key: 'second',
|
||||
label: t`Second`,
|
||||
},
|
||||
{ value: 3, key: 'third', label: t`Third` },
|
||||
{
|
||||
value: 4,
|
||||
key: 'fourth',
|
||||
label: t`Fourth`,
|
||||
},
|
||||
{ value: 5, key: 'fifth', label: t`Fifth` },
|
||||
{ value: -1, key: 'last', label: t`Last` },
|
||||
];
|
||||
|
||||
const monthOptions = [
|
||||
{
|
||||
key: 'january',
|
||||
value: 1,
|
||||
label: t`January`,
|
||||
},
|
||||
{
|
||||
key: 'february',
|
||||
value: 2,
|
||||
label: t`February`,
|
||||
},
|
||||
{
|
||||
key: 'march',
|
||||
value: 3,
|
||||
label: t`March`,
|
||||
},
|
||||
{
|
||||
key: 'april',
|
||||
value: 4,
|
||||
label: t`April`,
|
||||
},
|
||||
{
|
||||
key: 'may',
|
||||
value: 5,
|
||||
label: t`May`,
|
||||
},
|
||||
{
|
||||
key: 'june',
|
||||
value: 6,
|
||||
label: t`June`,
|
||||
},
|
||||
{
|
||||
key: 'july',
|
||||
value: 7,
|
||||
label: t`July`,
|
||||
},
|
||||
{
|
||||
key: 'august',
|
||||
value: 8,
|
||||
label: t`August`,
|
||||
},
|
||||
{
|
||||
key: 'september',
|
||||
value: 9,
|
||||
label: t`September`,
|
||||
},
|
||||
{
|
||||
key: 'october',
|
||||
value: 10,
|
||||
label: t`October`,
|
||||
},
|
||||
{
|
||||
key: 'november',
|
||||
value: 11,
|
||||
label: t`November`,
|
||||
},
|
||||
{
|
||||
key: 'december',
|
||||
value: 12,
|
||||
label: t`December`,
|
||||
},
|
||||
];
|
||||
|
||||
const weekdayOptions = [
|
||||
{
|
||||
value: RRule.SU,
|
||||
key: 'sunday',
|
||||
label: t`Sunday`,
|
||||
},
|
||||
{
|
||||
value: RRule.MO,
|
||||
key: 'monday',
|
||||
label: t`Monday`,
|
||||
},
|
||||
{
|
||||
value: RRule.TU,
|
||||
key: 'tuesday',
|
||||
label: t`Tuesday`,
|
||||
},
|
||||
{
|
||||
value: RRule.WE,
|
||||
key: 'wednesday',
|
||||
label: t`Wednesday`,
|
||||
},
|
||||
{
|
||||
value: RRule.TH,
|
||||
key: 'thursday',
|
||||
label: t`Thursday`,
|
||||
},
|
||||
{
|
||||
value: RRule.FR,
|
||||
key: 'friday',
|
||||
label: t`Friday`,
|
||||
},
|
||||
{
|
||||
value: RRule.SA,
|
||||
key: 'saturday',
|
||||
label: t`Saturday`,
|
||||
},
|
||||
];
|
||||
|
||||
const FREQUENCIESCONSTANTS = {
|
||||
minute: RRule.MINUTELY,
|
||||
hour: RRule.HOURLY,
|
||||
day: RRule.DAILY,
|
||||
week: RRule.WEEKLY,
|
||||
month: RRule.MONTHLY,
|
||||
year: RRule.YEARLY,
|
||||
};
|
||||
export {
|
||||
monthOptions,
|
||||
weekdayOptions,
|
||||
bysetposOptions,
|
||||
// validateSchedule,
|
||||
FREQUENCIESCONSTANTS,
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user