mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Adds support for editing proj/jt/wfjt schedule
This commit is contained in:
parent
7311ddf722
commit
017064aecf
@ -8,6 +8,8 @@ function($filter, $state, $stateParams, Wait, $scope, moment,
|
||||
WorkflowJobTemplate, SchedulerStrings, scheduleResolve, timezonesResolve, Alert
|
||||
) {
|
||||
|
||||
console.log(scheduleResolve);
|
||||
|
||||
let schedule, scheduler, scheduleCredentials = [];
|
||||
|
||||
/*
|
||||
@ -161,6 +163,7 @@ function($filter, $state, $stateParams, Wait, $scope, moment,
|
||||
function setUntil (scheduler) {
|
||||
let { until } = scheduleResolve;
|
||||
if(until !== ''){
|
||||
console.log(until);
|
||||
const date = moment(until);
|
||||
const endDt = moment.parseZone(date).format("MM/DD/YYYY");
|
||||
const endHour = date.format('HH');
|
||||
|
||||
@ -10,18 +10,21 @@ function FormSubmitError({ error }) {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
if (error?.response?.data && typeof error.response.data === 'object') {
|
||||
if (
|
||||
error?.response?.data &&
|
||||
typeof error.response.data === 'object' &&
|
||||
Object.keys(error.response.data).length > 0
|
||||
) {
|
||||
const errorMessages = error.response.data;
|
||||
setErrors(errorMessages);
|
||||
if (errorMessages.__all__) {
|
||||
setErrorMessage(errorMessages.__all__);
|
||||
} else if (errorMessages.detail) {
|
||||
setErrorMessage(errorMessages.detail);
|
||||
} else if (errorMessages.resources_needed_to_start) {
|
||||
setErrorMessage(errorMessages.resources_needed_to_start);
|
||||
} else {
|
||||
setErrorMessage(null);
|
||||
}
|
||||
|
||||
let messages = [];
|
||||
Object.values(error.response.data).forEach(value => {
|
||||
if (Array.isArray(value)) {
|
||||
messages = messages.concat(value);
|
||||
}
|
||||
});
|
||||
setErrorMessage(messages.length > 0 ? messages : null);
|
||||
} else {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(error);
|
||||
|
||||
@ -17,7 +17,7 @@ import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import { ScheduleDetail } from '@components/Schedule';
|
||||
import { ScheduleDetail, ScheduleEdit } from '@components/Schedule';
|
||||
import { SchedulesAPI } from '@api';
|
||||
|
||||
function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
||||
@ -108,6 +108,11 @@ function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
||||
exact
|
||||
/>
|
||||
{schedule && [
|
||||
<Route
|
||||
key="edit"
|
||||
path={`${pathRoot}schedules/:id/edit`}
|
||||
render={() => <ScheduleEdit schedule={schedule} />}
|
||||
/>,
|
||||
<Route
|
||||
key="details"
|
||||
path={`${pathRoot}schedules/:scheduleId/details`}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { func } from 'prop-types';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { getRRuleDayConstants } from '@util/dates';
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
|
||||
function ScheduleAdd({ i18n, createSchedule }) {
|
||||
@ -16,105 +15,9 @@ function ScheduleAdd({ i18n, createSchedule }) {
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
const buildRuleObj = values => {
|
||||
const [startDate, startTime] = values.startDateTime.split('T');
|
||||
// Dates are formatted like "YYYY-MM-DD"
|
||||
const [startYear, startMonth, startDay] = startDate.split('-');
|
||||
// Times are formatted like "HH:MM:SS" or "HH:MM" if no seconds
|
||||
// have been specified
|
||||
const [startHour = 0, startMinute = 0, startSecond = 0] = startTime.split(
|
||||
':'
|
||||
);
|
||||
|
||||
const ruleObj = {
|
||||
interval: values.interval,
|
||||
dtstart: new Date(
|
||||
Date.UTC(
|
||||
startYear,
|
||||
parseInt(startMonth, 10) - 1,
|
||||
startDay,
|
||||
startHour,
|
||||
startMinute,
|
||||
startSecond
|
||||
)
|
||||
),
|
||||
tzid: values.timezone,
|
||||
};
|
||||
|
||||
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.map(day => RRule[day]);
|
||||
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, i18n);
|
||||
}
|
||||
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, i18n);
|
||||
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
|
||||
switch (values.end) {
|
||||
case 'never':
|
||||
break;
|
||||
case 'after':
|
||||
ruleObj.count = values.occurrences;
|
||||
break;
|
||||
case 'onDate': {
|
||||
const [endDate, endTime] = values.endDateTime.split('T');
|
||||
const [endYear, endMonth, endDay] = endDate.split('-');
|
||||
const [endHour = 0, endMinute = 0, endSecond = 0] = endTime.split(':');
|
||||
ruleObj.until = new Date(
|
||||
Date.UTC(
|
||||
endYear,
|
||||
parseInt(endMonth, 10) - 1,
|
||||
endDay,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(i18n._(t`End did not match an expected value`));
|
||||
}
|
||||
|
||||
return ruleObj;
|
||||
};
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values));
|
||||
const rule = new RRule(buildRuleObj(values, i18n));
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
} = await createSchedule({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { RRule } from 'rrule';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import ScheduleAdd from './ScheduleAdd';
|
||||
|
||||
@ -117,7 +118,7 @@ describe('<ScheduleAdd />', () => {
|
||||
test('Successfully creates a schedule with weekly repeat frequency on mon/wed/fri', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
daysOfWeek: ['MO', 'WE', 'FR'],
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'week',
|
||||
@ -131,8 +132,7 @@ describe('<ScheduleAdd />', () => {
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
description: 'test description',
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR',
|
||||
rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`,
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with monthly repeat frequency on the first day of the month', async () => {
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { object } from 'prop-types';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
|
||||
function ScheduleEdit({ i18n, schedule }) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
const handleSubmit = async values => {
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values, i18n));
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
} = await SchedulesAPI.update(schedule.id, {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
});
|
||||
|
||||
history.push(`${pathRoot}schedules/${scheduleId}/details`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<ScheduleForm
|
||||
schedule={schedule}
|
||||
handleCancel={() =>
|
||||
history.push(`${pathRoot}schedules/${schedule.id}/details`)
|
||||
}
|
||||
handleSubmit={handleSubmit}
|
||||
submitError={formSubmitError}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleEdit.propTypes = {
|
||||
schedule: object.isRequired,
|
||||
};
|
||||
|
||||
ScheduleEdit.defaultProps = {};
|
||||
|
||||
export default withI18n()(ScheduleEdit);
|
||||
@ -0,0 +1,285 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { RRule } from 'rrule';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import ScheduleEdit from './ScheduleEdit';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
SchedulesAPI.update.mockResolvedValue({
|
||||
data: {
|
||||
id: 27,
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
|
||||
const mockSchedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
id: 27,
|
||||
type: 'schedule',
|
||||
url: '/api/v2/schedules/27/',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
},
|
||||
created: '2020-04-02T18:43:12.664142Z',
|
||||
modified: '2020-04-02T18:43:12.664185Z',
|
||||
name: 'mock schedule',
|
||||
description: '',
|
||||
extra_data: {},
|
||||
inventory: null,
|
||||
scm_branch: null,
|
||||
job_type: null,
|
||||
job_tags: null,
|
||||
skip_tags: null,
|
||||
limit: null,
|
||||
diff_mode: null,
|
||||
verbosity: null,
|
||||
unified_job_template: 11,
|
||||
enabled: true,
|
||||
dtstart: '2020-04-02T18:45:00Z',
|
||||
dtend: '2020-04-02T18:45:00Z',
|
||||
next_run: '2020-04-02T18:45:00Z',
|
||||
timezone: 'America/New_York',
|
||||
until: '',
|
||||
};
|
||||
|
||||
describe('<ScheduleEdit />', () => {
|
||||
beforeAll(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ScheduleEdit schedule={mockSchedule} />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
name: 'Run once schedule',
|
||||
startDateTime: '2020-03-25T10:00:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run once schedule',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T100000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with 10 minute repeat frequency after 10 occurrences', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'after',
|
||||
frequency: 'minute',
|
||||
interval: 10,
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
occurrences: 10,
|
||||
startDateTime: '2020-03-25T10:30:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with hourly repeat frequency ending on a specific date/time', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'onDate',
|
||||
endDateTime: '2020-03-26T10:45:00',
|
||||
frequency: 'hour',
|
||||
interval: 1,
|
||||
name: 'Run every hour until date',
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run every hour until date',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with daily repeat frequency', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'day',
|
||||
interval: 1,
|
||||
name: 'Run daily',
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run daily',
|
||||
rrule:
|
||||
'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('ScheduleForm').invoke('handleSubmit')({
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'week',
|
||||
interval: 1,
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
occurrences: 1,
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`,
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with monthly repeat frequency on the first day of the month', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
name: 'Run on the first day of the month',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayNumber: 1,
|
||||
startDateTime: '2020-04-01T10:45',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run on the first day of the month',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
endDateTime: '2020-03-26T11:00:00',
|
||||
frequency: 'month',
|
||||
interval: 1,
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheDay: 'tuesday',
|
||||
runOnTheOccurrence: -1,
|
||||
startDateTime: '2020-03-31T11:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
rrule:
|
||||
'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('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
name: 'Yearly on the first day of March',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 3,
|
||||
runOnDayNumber: 1,
|
||||
startDateTime: '2020-03-01T00:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first day of March',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
name: 'Yearly on the second Friday in April',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 2,
|
||||
runOnTheDay: 'friday',
|
||||
runOnTheMonth: 4,
|
||||
startDateTime: '2020-04-10T11:15',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the second Friday in April',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4',
|
||||
});
|
||||
});
|
||||
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
interval: 1,
|
||||
name: 'Yearly on the first weekday in October',
|
||||
occurrences: 1,
|
||||
runOn: 'the',
|
||||
runOnTheOccurrence: 1,
|
||||
runOnTheDay: 'weekday',
|
||||
runOnTheMonth: 10,
|
||||
startDateTime: '2020-04-10T11:15',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first weekday in October',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './ScheduleEdit';
|
||||
@ -5,3 +5,4 @@ export { default as ScheduleOccurrences } from './ScheduleOccurrences';
|
||||
export { default as ScheduleToggle } from './ScheduleToggle';
|
||||
export { default as ScheduleDetail } from './ScheduleDetail';
|
||||
export { default as ScheduleAdd } from './ScheduleAdd';
|
||||
export { default as ScheduleEdit } from './ScheduleEdit';
|
||||
|
||||
@ -3,6 +3,7 @@ import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { RRule } from 'rrule';
|
||||
import {
|
||||
Checkbox as _Checkbox,
|
||||
FormGroup,
|
||||
@ -255,9 +256,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
<div css="display: flex">
|
||||
<Checkbox
|
||||
label={i18n._(t`Sun`)}
|
||||
isChecked={daysOfWeek.value.includes('SU')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.SU)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('SU', checked);
|
||||
updateDaysOfWeek(RRule.SU, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Sunday`)}
|
||||
id="schedule-days-of-week-sun"
|
||||
@ -265,9 +266,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Mon`)}
|
||||
isChecked={daysOfWeek.value.includes('MO')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.MO)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('MO', checked);
|
||||
updateDaysOfWeek(RRule.MO, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Monday`)}
|
||||
id="schedule-days-of-week-mon"
|
||||
@ -275,9 +276,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Tue`)}
|
||||
isChecked={daysOfWeek.value.includes('TU')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.TU)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('TU', checked);
|
||||
updateDaysOfWeek(RRule.TU, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Tuesday`)}
|
||||
id="schedule-days-of-week-tue"
|
||||
@ -285,9 +286,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Wed`)}
|
||||
isChecked={daysOfWeek.value.includes('WE')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.WE)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('WE', checked);
|
||||
updateDaysOfWeek(RRule.WE, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Wednesday`)}
|
||||
id="schedule-days-of-week-wed"
|
||||
@ -295,9 +296,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Thu`)}
|
||||
isChecked={daysOfWeek.value.includes('TH')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.TH)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('TH', checked);
|
||||
updateDaysOfWeek(RRule.TH, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Thursday`)}
|
||||
id="schedule-days-of-week-thu"
|
||||
@ -305,9 +306,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Fri`)}
|
||||
isChecked={daysOfWeek.value.includes('FR')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.FR)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('FR', checked);
|
||||
updateDaysOfWeek(RRule.FR, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Friday`)}
|
||||
id="schedule-days-of-week-fri"
|
||||
@ -315,9 +316,9 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Sat`)}
|
||||
isChecked={daysOfWeek.value.includes('SA')}
|
||||
isChecked={daysOfWeek.value.includes(RRule.SA)}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('SA', checked);
|
||||
updateDaysOfWeek(RRule.SA, checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Saturday`)}
|
||||
id="schedule-days-of-week-sat"
|
||||
|
||||
@ -3,6 +3,7 @@ import { shape, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { RRule } from 'rrule';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
||||
import { SchedulesAPI } from '@api';
|
||||
@ -12,11 +13,60 @@ import ContentLoading from '@components/ContentLoading';
|
||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||
import FormField, { FormSubmitError } from '@components/FormField';
|
||||
import { FormColumnLayout, SubFormLayout } from '@components/FormLayout';
|
||||
import { dateToInputDateTime } from '@util/dates';
|
||||
import { dateToInputDateTime, formatDateStringUTC } from '@util/dates';
|
||||
import useRequest from '@util/useRequest';
|
||||
import { required } from '@util/validators';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
|
||||
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({ i18n, zoneOptions }) {
|
||||
const [startDateTime, startDateTimeMeta] = useField({
|
||||
name: 'startDateTime',
|
||||
@ -121,6 +171,7 @@ function ScheduleForm({
|
||||
submitError,
|
||||
...rest
|
||||
}) {
|
||||
let rruleError;
|
||||
const now = new Date();
|
||||
const closestQuarterHour = new Date(
|
||||
Math.ceil(now.getTime() / 900000) * 900000
|
||||
@ -128,6 +179,114 @@ function ScheduleForm({
|
||||
const tomorrow = new Date(closestQuarterHour);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const initialValues = {
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
end: 'never',
|
||||
endDateTime: dateToInputDateTime(tomorrow),
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
name: schedule.name || '',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
startDateTime: dateToInputDateTime(closestQuarterHour),
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
};
|
||||
|
||||
const overriddenValues = {};
|
||||
|
||||
if (Object.keys(schedule).length > 0) {
|
||||
if (schedule.rrule) {
|
||||
try {
|
||||
const {
|
||||
origOptions: {
|
||||
bymonth,
|
||||
bymonthday,
|
||||
bysetpos,
|
||||
byweekday,
|
||||
count,
|
||||
dtstart,
|
||||
freq,
|
||||
interval,
|
||||
},
|
||||
} = RRule.fromString(schedule.rrule.replace(' ', '\n'));
|
||||
|
||||
if (dtstart) {
|
||||
overriddenValues.startDateTime = dateToInputDateTime(
|
||||
new Date(formatDateStringUTC(dtstart))
|
||||
);
|
||||
}
|
||||
|
||||
if (schedule.until) {
|
||||
overriddenValues.end = 'onDate';
|
||||
overriddenValues.endDateTime = schedule.until;
|
||||
} 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(i18n._(t`Schedule is missing rrule`));
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
request: loadZoneInfo,
|
||||
error: contentError,
|
||||
@ -150,8 +309,8 @@ function ScheduleForm({
|
||||
loadZoneInfo();
|
||||
}, [loadZoneInfo]);
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
if (contentError || rruleError) {
|
||||
return <ContentError error={contentError || rruleError} />;
|
||||
}
|
||||
|
||||
if (contentLoading) {
|
||||
@ -163,24 +322,7 @@ function ScheduleForm({
|
||||
{() => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
end: 'never',
|
||||
endDateTime: dateToInputDateTime(tomorrow),
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
name: schedule.name || '',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
startDateTime: dateToInputDateTime(closestQuarterHour),
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
}}
|
||||
initialValues={Object.assign(initialValues, overriddenValues)}
|
||||
onSubmit={handleSubmit}
|
||||
validate={values => {
|
||||
const errors = {};
|
||||
@ -208,7 +350,7 @@ function ScheduleForm({
|
||||
(runOnDayNumber < 1 || runOnDayNumber > 31)
|
||||
) {
|
||||
errors.runOn = i18n._(
|
||||
t`Please select a day number between 1 and 31`
|
||||
t`Please select a day number between 1 and 31.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,40 @@ import ScheduleForm from './ScheduleForm';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
const mockSchedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
id: 27,
|
||||
type: 'schedule',
|
||||
url: '/api/v2/schedules/27/',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
},
|
||||
created: '2020-04-02T18:43:12.664142Z',
|
||||
modified: '2020-04-02T18:43:12.664185Z',
|
||||
name: 'mock schedule',
|
||||
description: 'test description',
|
||||
extra_data: {},
|
||||
inventory: null,
|
||||
scm_branch: null,
|
||||
job_type: null,
|
||||
job_tags: null,
|
||||
skip_tags: null,
|
||||
limit: null,
|
||||
diff_mode: null,
|
||||
verbosity: null,
|
||||
unified_job_template: 11,
|
||||
enabled: true,
|
||||
dtstart: '2020-04-02T18:45:00Z',
|
||||
dtend: '2020-04-02T18:45:00Z',
|
||||
next_run: '2020-04-02T18:45:00Z',
|
||||
timezone: 'America/New_York',
|
||||
until: '',
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
const defaultFieldsVisible = () => {
|
||||
@ -16,6 +50,21 @@ const defaultFieldsVisible = () => {
|
||||
expect(wrapper.find('FormGroup[label="Run frequency"]').length).toBe(1);
|
||||
};
|
||||
|
||||
const nonRRuleValuesMatch = () => {
|
||||
expect(wrapper.find('input#schedule-name').prop('value')).toBe(
|
||||
'mock schedule'
|
||||
);
|
||||
expect(wrapper.find('input#schedule-description').prop('value')).toBe(
|
||||
'test description'
|
||||
);
|
||||
expect(wrapper.find('input#schedule-start-datetime').prop('value')).toBe(
|
||||
'2020-04-02T14:45:00'
|
||||
);
|
||||
expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
|
||||
'America/New_York'
|
||||
);
|
||||
};
|
||||
|
||||
describe('<ScheduleForm />', () => {
|
||||
describe('Error', () => {
|
||||
test('should display error when error occurs while loading', async () => {
|
||||
@ -296,20 +345,321 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-end-datetime').invoke('onChange')(
|
||||
'2020-03-14T01:45:00',
|
||||
{
|
||||
target: { name: 'endDateTime' },
|
||||
}
|
||||
);
|
||||
wrapper.find('input#schedule-end-datetime').simulate('change', {
|
||||
target: { name: 'endDateTime', value: '2020-03-14T01:45:00' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('#schedule-end-datetime-helper').text()).toBe(
|
||||
'Please select an end date/time that comes after the start date/time.'
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-end-datetime').simulate('blur');
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('#schedule-end-datetime-helper').text()).toBe(
|
||||
'Please select an end date/time that comes after the start date/time.'
|
||||
);
|
||||
});
|
||||
test('error shown when on day number is not between 1 and 31', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-run-on-day-number').simulate('change', {
|
||||
target: { value: 32, name: 'runOnDayNumber' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('#schedule-run-on-helper').text()).toBe(
|
||||
'Please select a day number between 1 and 31.'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Edit', () => {
|
||||
beforeAll(async () => {
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs once', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={mockSchedule}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
nonRRuleValuesMatch();
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'none'
|
||||
);
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every 10 minutes', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=10;FREQ=MINUTELY',
|
||||
dtend: null,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(0);
|
||||
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);
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every hour 10 times', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=HOURLY;COUNT=10',
|
||||
dtend: '2020-04-03T03:45:00Z',
|
||||
until: '',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(0);
|
||||
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);
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every day', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=DAILY',
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
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);
|
||||
});
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every week on m/w/f until Jan 1, 2020', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z',
|
||||
dtend: '2020-10-30T18:45:00Z',
|
||||
until: '2021-01-01T00:00:00',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
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')
|
||||
).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')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-wed').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-thu').prop('checked')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-fri').prop('checked')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('input#schedule-days-of-week-sat').prop('checked')
|
||||
).toBe(false);
|
||||
expect(wrapper.find('input#schedule-end-datetime').prop('value')).toBe(
|
||||
'2021-01-01T00:00:00'
|
||||
);
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every month on the last weekday', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR',
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
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')
|
||||
).toBe(-1);
|
||||
expect(
|
||||
wrapper.find('select#schedule-run-on-the-day').prop('value')
|
||||
).toBe('weekday');
|
||||
});
|
||||
});
|
||||
test('initially renders expected fields and values with existing schedule that runs every year on the May 6', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={Object.assign(mockSchedule, {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=5;BYMONTHDAY=6',
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
defaultFieldsVisible();
|
||||
expect(wrapper.find('FormGroup[label="End"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run every"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run on"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="On days"]').length).toBe(0);
|
||||
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')
|
||||
).toBe(5);
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
).toBe(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
101
awx/ui_next/src/components/Schedule/shared/buildRuleObj.js
Normal file
101
awx/ui_next/src/components/Schedule/shared/buildRuleObj.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { RRule } from 'rrule';
|
||||
import { getRRuleDayConstants } from '@util/dates';
|
||||
|
||||
export default function buildRuleObj(values, i18n) {
|
||||
const [startDate, startTime] = values.startDateTime.split('T');
|
||||
// Dates are formatted like "YYYY-MM-DD"
|
||||
const [startYear, startMonth, startDay] = startDate.split('-');
|
||||
// Times are formatted like "HH:MM:SS" or "HH:MM" if no seconds
|
||||
// have been specified
|
||||
const [startHour = 0, startMinute = 0, startSecond = 0] = startTime.split(
|
||||
':'
|
||||
);
|
||||
|
||||
const ruleObj = {
|
||||
interval: values.interval,
|
||||
dtstart: new Date(
|
||||
Date.UTC(
|
||||
startYear,
|
||||
parseInt(startMonth, 10) - 1,
|
||||
startDay,
|
||||
startHour,
|
||||
startMinute,
|
||||
startSecond
|
||||
)
|
||||
),
|
||||
tzid: values.timezone,
|
||||
};
|
||||
|
||||
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, i18n);
|
||||
}
|
||||
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, i18n);
|
||||
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(i18n._(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': {
|
||||
const [endDate, endTime] = values.endDateTime.split('T');
|
||||
const [endYear, endMonth, endDay] = endDate.split('-');
|
||||
const [endHour = 0, endMinute = 0, endSecond = 0] = endTime.split(':');
|
||||
ruleObj.until = new Date(
|
||||
Date.UTC(
|
||||
endYear,
|
||||
parseInt(endMonth, 10) - 1,
|
||||
endDay,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(i18n._(t`End did not match an expected value`));
|
||||
}
|
||||
}
|
||||
|
||||
return ruleObj;
|
||||
}
|
||||
@ -47,8 +47,9 @@ class Projects extends Component {
|
||||
[`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`),
|
||||
[`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${projectSchedulesPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Edit Details`
|
||||
t`Schedule Details`
|
||||
),
|
||||
[`${projectSchedulesPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
|
||||
};
|
||||
|
||||
this.setState({ breadcrumbConfig });
|
||||
|
||||
@ -69,6 +69,8 @@ class Templates extends Component {
|
||||
schedule.id}`]: `${schedule && schedule.name}`,
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
schedule.id}/details`]: i18n._(t`Schedule Details`),
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
schedule.id}/edit`]: i18n._(t`Edit Details`),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user