mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 12:20:45 -03:30
Implement schedule add form on JT/WFJT/Proj
This commit is contained in:
parent
827adbce76
commit
d9b613ccb3
@ -1,5 +1,9 @@
|
||||
const SchedulesMixin = parent =>
|
||||
class extends parent {
|
||||
createSchedule(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/schedules/`, data);
|
||||
}
|
||||
|
||||
readSchedules(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/schedules/`, { params });
|
||||
}
|
||||
|
||||
@ -13,6 +13,10 @@ class Schedules extends Base {
|
||||
readCredentials(resourceId, params) {
|
||||
return this.http.get(`${this.baseUrl}${resourceId}/credentials/`, params);
|
||||
}
|
||||
|
||||
readZoneInfo() {
|
||||
return this.http.get(`${this.baseUrl}zoneinfo/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Schedules;
|
||||
|
||||
@ -37,7 +37,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
title="Remove {0} Access"
|
||||
title="Remove Team Access"
|
||||
variant="danger"
|
||||
>
|
||||
<Modal
|
||||
@ -71,7 +71,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-describedby="pf-modal-0"
|
||||
aria-label="Remove {0} Access"
|
||||
aria-label="Remove Team Access"
|
||||
aria-modal="true"
|
||||
class="pf-c-modal-box pf-m-sm"
|
||||
role="dialog"
|
||||
@ -120,7 +120,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
<h1
|
||||
class="pf-c-title pf-m-2xl"
|
||||
>
|
||||
Remove {0} Access
|
||||
Remove Team Access
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@ -128,7 +128,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
class="pf-c-modal-box__body"
|
||||
id="pf-modal-0"
|
||||
>
|
||||
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
|
||||
Are you sure you want to remove Member access from The Team? Doing so affects all members of the team.
|
||||
<br />
|
||||
<br />
|
||||
If you only want to remove access for this particular user, please remove them from the team.
|
||||
@ -166,7 +166,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
<Title
|
||||
size="2xl"
|
||||
>
|
||||
Remove {0} Access
|
||||
Remove Team Access
|
||||
</Title>
|
||||
</AlertModal__Header>
|
||||
}
|
||||
@ -177,7 +177,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
isSmall={true}
|
||||
onClose={[Function]}
|
||||
showClose={true}
|
||||
title="Remove {0} Access"
|
||||
title="Remove Team Access"
|
||||
>
|
||||
<Portal
|
||||
containerInfo={
|
||||
@ -190,7 +190,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-describedby="pf-modal-0"
|
||||
aria-label="Remove {0} Access"
|
||||
aria-label="Remove Team Access"
|
||||
aria-modal="true"
|
||||
class="pf-c-modal-box pf-m-sm"
|
||||
role="dialog"
|
||||
@ -239,7 +239,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
<h1
|
||||
class="pf-c-title pf-m-2xl"
|
||||
>
|
||||
Remove {0} Access
|
||||
Remove Team Access
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@ -247,7 +247,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
class="pf-c-modal-box__body"
|
||||
id="pf-modal-0"
|
||||
>
|
||||
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
|
||||
Are you sure you want to remove Member access from The Team? Doing so affects all members of the team.
|
||||
<br />
|
||||
<br />
|
||||
If you only want to remove access for this particular user, please remove them from the team.
|
||||
@ -303,7 +303,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
<Title
|
||||
size="2xl"
|
||||
>
|
||||
Remove {0} Access
|
||||
Remove Team Access
|
||||
</Title>
|
||||
</AlertModal__Header>
|
||||
}
|
||||
@ -315,7 +315,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
isSmall={true}
|
||||
onClose={[Function]}
|
||||
showClose={true}
|
||||
title="Remove {0} Access"
|
||||
title="Remove Team Access"
|
||||
>
|
||||
<Backdrop>
|
||||
<div
|
||||
@ -342,11 +342,11 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
isLarge={false}
|
||||
isSmall={true}
|
||||
style={Object {}}
|
||||
title="Remove {0} Access"
|
||||
title="Remove Team Access"
|
||||
>
|
||||
<div
|
||||
aria-describedby="pf-modal-0"
|
||||
aria-label="Remove {0} Access"
|
||||
aria-label="Remove Team Access"
|
||||
aria-modal="true"
|
||||
className="pf-c-modal-box pf-m-sm"
|
||||
role="dialog"
|
||||
@ -528,7 +528,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
<h1
|
||||
className="pf-c-title pf-m-2xl"
|
||||
>
|
||||
Remove {0} Access
|
||||
Remove Team Access
|
||||
</h1>
|
||||
</Title>
|
||||
</div>
|
||||
@ -542,7 +542,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
|
||||
className="pf-c-modal-box__body"
|
||||
id="pf-modal-0"
|
||||
>
|
||||
Are you sure you want to remove {0} access from {1}? Doing so affects all members of the team.
|
||||
Are you sure you want to remove Member access from The Team? Doing so affects all members of the team.
|
||||
<br />
|
||||
<br />
|
||||
If you only want to remove access for this particular user, please remove them from the team.
|
||||
|
||||
168
awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx
Normal file
168
awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx
Normal file
@ -0,0 +1,168 @@
|
||||
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 { getWeekNumber } from '@util/dates';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
|
||||
const days = {
|
||||
0: 'SU',
|
||||
1: 'MO',
|
||||
2: 'TU',
|
||||
3: 'WE',
|
||||
4: 'TH',
|
||||
5: 'FR',
|
||||
6: 'SA',
|
||||
};
|
||||
|
||||
function ScheduleAdd({ i18n, createSchedule }) {
|
||||
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 [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 === 'number') {
|
||||
ruleObj.bymonthday = startDay;
|
||||
} else if (values.runOn === 'day') {
|
||||
ruleObj.byweekday =
|
||||
RRule[days[new Date(values.startDateTime).getDay()]];
|
||||
ruleObj.bysetpos = getWeekNumber(values.startDateTime);
|
||||
} else if (values.runOn === 'lastDay') {
|
||||
ruleObj.byweekday =
|
||||
RRule[days[new Date(values.startDateTime).getDay()]];
|
||||
ruleObj.bysetpos = -1;
|
||||
}
|
||||
break;
|
||||
case 'year':
|
||||
ruleObj.freq = RRule.YEARLY;
|
||||
ruleObj.bymonth = new Date(values.startDateTime).getMonth() + 1;
|
||||
if (values.runOn === 'number') {
|
||||
ruleObj.bymonthday = startDay;
|
||||
} else if (values.runOn === 'day') {
|
||||
ruleObj.byweekday =
|
||||
RRule[days[new Date(values.startDateTime).getDay()]];
|
||||
ruleObj.bysetpos = getWeekNumber(values.startDateTime);
|
||||
} else if (values.runOn === 'lastDay') {
|
||||
ruleObj.byweekday =
|
||||
RRule[days[new Date(values.startDateTime).getDay()]];
|
||||
ruleObj.bysetpos = -1;
|
||||
}
|
||||
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`));
|
||||
}
|
||||
|
||||
const rule = new RRule(ruleObj);
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
} = await createSchedule({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
});
|
||||
|
||||
history.push(`${pathRoot}schedules/${scheduleId}`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<ScheduleForm
|
||||
handleCancel={() => history.push(`${pathRoot}schedules`)}
|
||||
handleSubmit={handleSubmit}
|
||||
submitError={formSubmitError}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleAdd.propTypes = {
|
||||
createSchedule: func.isRequired,
|
||||
};
|
||||
|
||||
ScheduleAdd.defaultProps = {};
|
||||
|
||||
export default withI18n()(ScheduleAdd);
|
||||
@ -0,0 +1,227 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import ScheduleAdd from './ScheduleAdd';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
|
||||
const createSchedule = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('<ScheduleAdd />', () => {
|
||||
beforeAll(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleAdd createSchedule={createSchedule} />
|
||||
);
|
||||
});
|
||||
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(createSchedule).toHaveBeenCalledWith({
|
||||
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,
|
||||
runOn: 'number',
|
||||
startDateTime: '2020-03-25T10:30:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
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',
|
||||
runOn: 'number',
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
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',
|
||||
runOn: 'number',
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
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: ['MO', 'WE', 'FR'],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'week',
|
||||
interval: 1,
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
occurrences: 1,
|
||||
runOn: 'number',
|
||||
startDateTime: '2020-03-25T10:45:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
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',
|
||||
});
|
||||
});
|
||||
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: 'number',
|
||||
startDateTime: '2020-04-01T10:45',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
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=01',
|
||||
});
|
||||
});
|
||||
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: 'lastDay',
|
||||
startDateTime: '2020-03-31T11:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
description: 'test description',
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYDAY=TU;BYSETPOS=-1',
|
||||
});
|
||||
});
|
||||
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: 'number',
|
||||
startDateTime: '2020-03-01T00:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
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=01',
|
||||
});
|
||||
});
|
||||
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: 'day',
|
||||
startDateTime: '2020-04-10T11:15',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
description: 'test description',
|
||||
name: 'Yearly on the second Friday in April',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=4;BYDAY=FR;BYSETPOS=2',
|
||||
});
|
||||
});
|
||||
});
|
||||
1
awx/ui_next/src/components/Schedule/ScheduleAdd/index.js
Normal file
1
awx/ui_next/src/components/Schedule/ScheduleAdd/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ScheduleAdd';
|
||||
@ -1,18 +1,23 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
import { Schedule, ScheduleList } from '@components/Schedule';
|
||||
import { Schedule, ScheduleAdd, ScheduleList } from '@components/Schedule';
|
||||
|
||||
function Schedules({
|
||||
createSchedule,
|
||||
loadScheduleOptions,
|
||||
loadSchedules,
|
||||
setBreadcrumb,
|
||||
unifiedJobTemplate,
|
||||
loadSchedules,
|
||||
loadScheduleOptions,
|
||||
}) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${match.path}/add`}
|
||||
render={() => <ScheduleAdd createSchedule={createSchedule} />}
|
||||
/>
|
||||
<Route
|
||||
key="details"
|
||||
path={`${match.path}/:scheduleId`}
|
||||
|
||||
@ -4,3 +4,4 @@ export { default as ScheduleList } from './ScheduleList';
|
||||
export { default as ScheduleOccurrences } from './ScheduleOccurrences';
|
||||
export { default as ScheduleToggle } from './ScheduleToggle';
|
||||
export { default as ScheduleDetail } from './ScheduleDetail';
|
||||
export { default as ScheduleAdd } from './ScheduleAdd';
|
||||
|
||||
@ -0,0 +1,445 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Checkbox as _Checkbox,
|
||||
FormGroup,
|
||||
Radio,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import FormField from '@components/FormField';
|
||||
import {
|
||||
getDaysInMonth,
|
||||
getDayString,
|
||||
getMonthString,
|
||||
getWeekString,
|
||||
getWeekNumber,
|
||||
} from '@util/dates';
|
||||
import { required } from '@util/validators';
|
||||
|
||||
const RunEveryLabel = styled.p`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Checkbox = styled(_Checkbox)`
|
||||
:not(:last-of-type) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function requiredPositiveInteger(i18n) {
|
||||
return value => {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value)) {
|
||||
return i18n._(t`This field must an integer`);
|
||||
}
|
||||
if (value < 1) {
|
||||
return i18n._(t`This field must be greater than 0`);
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
return i18n._(t`Select a value for this field`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const FrequencyDetailSubform = ({ i18n }) => {
|
||||
const [startDateTime] = useField({
|
||||
name: 'startDateTime',
|
||||
});
|
||||
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
|
||||
name: 'daysOfWeek',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
const [end, endMeta] = useField({
|
||||
name: 'end',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
const [interval, intervalMeta] = useField({
|
||||
name: 'interval',
|
||||
validate: requiredPositiveInteger(i18n),
|
||||
});
|
||||
const [runOn, runOnMeta, runOnHelpers] = useField({
|
||||
name: 'runOn',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
const [endDateTime, endDateTimeMeta] = useField({
|
||||
name: 'endDateTime',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
const [frequency] = useField({
|
||||
name: 'frequency',
|
||||
});
|
||||
useField({
|
||||
name: 'occurrences',
|
||||
validate: requiredPositiveInteger(i18n),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// The Last day option disappears if the start date isn't in the
|
||||
// last week of the month. If that value was selected when this
|
||||
// happens then we'll clear out the selection and force the user
|
||||
// to choose between the remaining two.
|
||||
if (
|
||||
(frequency.value === 'month' || frequency.value === 'year') &&
|
||||
runOn.value === 'lastDay' &&
|
||||
getDaysInMonth(startDateTime.value) - 7 >=
|
||||
new Date(startDateTime.value).getDate()
|
||||
) {
|
||||
runOnHelpers.setValue('');
|
||||
}
|
||||
}, [startDateTime.value, frequency.value, runOn.value, runOnHelpers]);
|
||||
|
||||
const updateDaysOfWeek = (day, checked) => {
|
||||
const newDaysOfWeek = [...daysOfWeek.value];
|
||||
if (checked) {
|
||||
newDaysOfWeek.push(day);
|
||||
daysOfWeekHelpers.setValue(newDaysOfWeek);
|
||||
} else {
|
||||
daysOfWeekHelpers.setValue(
|
||||
newDaysOfWeek.filter(selectedDay => selectedDay !== day)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getRunEveryLabel = () => {
|
||||
switch (frequency.value) {
|
||||
case 'minute':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'minute',
|
||||
other: 'minutes',
|
||||
});
|
||||
case 'hour':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'hour',
|
||||
other: 'hours',
|
||||
});
|
||||
case 'day':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'day',
|
||||
other: 'days',
|
||||
});
|
||||
case 'week':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'week',
|
||||
other: 'weeks',
|
||||
});
|
||||
case 'month':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'month',
|
||||
other: 'months',
|
||||
});
|
||||
case 'year':
|
||||
return i18n.plural({
|
||||
value: interval.value,
|
||||
one: 'year',
|
||||
other: 'years',
|
||||
});
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
};
|
||||
|
||||
const generateRunOnNumberLabel = () => {
|
||||
switch (frequency.value) {
|
||||
case 'month':
|
||||
return i18n._(
|
||||
t`Day ${startDateTime.value.split('T')[0].split('-')[2]}`
|
||||
);
|
||||
case 'year': {
|
||||
const monthString = getMonthString(
|
||||
new Date(startDateTime.value).getMonth(),
|
||||
i18n
|
||||
);
|
||||
return `${monthString} ${new Date(startDateTime.value).getDate()}`;
|
||||
}
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
};
|
||||
|
||||
const generateRunOnDayLabel = () => {
|
||||
const dayString = getDayString(
|
||||
new Date(startDateTime.value).getDay(),
|
||||
i18n
|
||||
);
|
||||
const weekNumber = getWeekNumber(startDateTime.value);
|
||||
const weekString = getWeekString(weekNumber, i18n);
|
||||
switch (frequency.value) {
|
||||
case 'month':
|
||||
return i18n._(t`The ${weekString} ${dayString}`);
|
||||
case 'year': {
|
||||
const monthString = getMonthString(
|
||||
new Date(startDateTime.value).getMonth(),
|
||||
i18n
|
||||
);
|
||||
return i18n._(t`The ${weekString} ${dayString} in ${monthString}`);
|
||||
}
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
};
|
||||
|
||||
const generateRunOnLastDayLabel = () => {
|
||||
const dayString = getDayString(
|
||||
new Date(startDateTime.value).getDay(),
|
||||
i18n
|
||||
);
|
||||
switch (frequency.value) {
|
||||
case 'month':
|
||||
return i18n._(t`The last ${dayString}`);
|
||||
case 'year': {
|
||||
const monthString = getMonthString(
|
||||
new Date(startDateTime.value).getMonth(),
|
||||
i18n
|
||||
);
|
||||
return i18n._(t`The last ${dayString} in ${monthString}`);
|
||||
}
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
name="interval"
|
||||
fieldId="schedule-run-every"
|
||||
helperTextInvalid={intervalMeta.error}
|
||||
isRequired
|
||||
isValid={!intervalMeta.touched || !intervalMeta.error}
|
||||
label={i18n._(t`Run every`)}
|
||||
>
|
||||
<div css="display: flex">
|
||||
<TextInput
|
||||
css="margin-right: 10px;"
|
||||
id="schedule-run-every"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
{...interval}
|
||||
onChange={(value, event) => {
|
||||
interval.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{frequency?.value === 'week' && (
|
||||
<FormGroup
|
||||
name="daysOfWeek"
|
||||
fieldId="schedule-days-of-week"
|
||||
helperTextInvalid={daysOfWeekMeta.error}
|
||||
isRequired
|
||||
isValid={!daysOfWeekMeta.touched || !daysOfWeekMeta.error}
|
||||
label={i18n._(t`On days`)}
|
||||
>
|
||||
<div css="display: flex">
|
||||
<Checkbox
|
||||
label={i18n._(t`Sun`)}
|
||||
isChecked={daysOfWeek.value.includes('SU')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('SU', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Sunday`)}
|
||||
id="days-of-week-sun"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Mon`)}
|
||||
isChecked={daysOfWeek.value.includes('MO')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('MO', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Monday`)}
|
||||
id="days-of-week-mon"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Tue`)}
|
||||
isChecked={daysOfWeek.value.includes('TU')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('TU', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Tuesday`)}
|
||||
id="days-of-week-tue"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Wed`)}
|
||||
isChecked={daysOfWeek.value.includes('WE')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('WE', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Wednesday`)}
|
||||
id="days-of-week-wed"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Thu`)}
|
||||
isChecked={daysOfWeek.value.includes('TH')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('TH', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Thursday`)}
|
||||
id="days-of-week-thu"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Fri`)}
|
||||
isChecked={daysOfWeek.value.includes('FR')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('FR', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Friday`)}
|
||||
id="days-of-week-fri"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n._(t`Sat`)}
|
||||
isChecked={daysOfWeek.value.includes('SA')}
|
||||
onChange={checked => {
|
||||
updateDaysOfWeek('SA', checked);
|
||||
}}
|
||||
aria-label={i18n._(t`Saturday`)}
|
||||
id="days-of-week-sat"
|
||||
name="daysOfWeek"
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
)}
|
||||
{(frequency?.value === 'month' || frequency?.value === 'year') &&
|
||||
!isNaN(new Date(startDateTime.value)) && (
|
||||
<FormGroup
|
||||
name="runOn"
|
||||
fieldId="schedule-run-on"
|
||||
helperTextInvalid={runOnMeta.error}
|
||||
isRequired
|
||||
isValid={!runOnMeta.touched || !runOnMeta.error}
|
||||
label={i18n._(t`Run on`)}
|
||||
>
|
||||
<Radio
|
||||
id="run-on-number"
|
||||
name="runOn"
|
||||
label={generateRunOnNumberLabel()}
|
||||
value="number"
|
||||
isChecked={runOn.value === 'number'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'number';
|
||||
runOn.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
id="run-on-day"
|
||||
name="runOn"
|
||||
label={generateRunOnDayLabel()}
|
||||
value="day"
|
||||
isChecked={runOn.value === 'day'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'day';
|
||||
runOn.onChange(event);
|
||||
}}
|
||||
/>
|
||||
{new Date(startDateTime.value).getDate() >
|
||||
getDaysInMonth(startDateTime.value) - 7 && (
|
||||
<Radio
|
||||
id="run-on-last-day"
|
||||
name="runOn"
|
||||
label={generateRunOnLastDayLabel()}
|
||||
value="lastDay"
|
||||
isChecked={runOn.value === 'lastDay'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'lastDay';
|
||||
runOn.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
name="end"
|
||||
fieldId="schedule-end"
|
||||
helperTextInvalid={endMeta.error}
|
||||
isRequired
|
||||
isValid={!endMeta.touched || !endMeta.error}
|
||||
label={i18n._(t`End`)}
|
||||
>
|
||||
<Radio
|
||||
id="end-never"
|
||||
name="end"
|
||||
label={i18n._(t`Never`)}
|
||||
value="never"
|
||||
isChecked={end.value === 'never'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'never';
|
||||
end.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
id="end-after"
|
||||
name="end"
|
||||
label={i18n._(t`After number of occurrences`)}
|
||||
value="after"
|
||||
isChecked={end.value === 'after'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'after';
|
||||
end.onChange(event);
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
id="end-on-date"
|
||||
name="end"
|
||||
label={i18n._(t`On date`)}
|
||||
value="onDate"
|
||||
isChecked={end.value === 'onDate'}
|
||||
onChange={(value, event) => {
|
||||
event.target.value = 'onDate';
|
||||
end.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{end?.value === 'after' && (
|
||||
<FormField
|
||||
id="schedule-occurrences"
|
||||
label={i18n._(t`Occurrences`)}
|
||||
name="occurrences"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
)}
|
||||
{end?.value === 'onDate' && (
|
||||
<FormGroup
|
||||
fieldId="schedule-end-datetime"
|
||||
helperTextInvalid={endDateTimeMeta.error}
|
||||
isRequired
|
||||
isValid={!endDateTimeMeta.touched || !endDateTimeMeta.error}
|
||||
label={i18n._(t`End date/time`)}
|
||||
>
|
||||
<input
|
||||
className="pf-c-form-control"
|
||||
type="datetime-local"
|
||||
id="schedule-end-datetime"
|
||||
step="1"
|
||||
{...endDateTime}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
/* eslint-enable no-restricted-globals */
|
||||
};
|
||||
|
||||
export default withI18n()(FrequencyDetailSubform);
|
||||
230
awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx
Normal file
230
awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx
Normal file
@ -0,0 +1,230 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { shape, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, useField } from 'formik';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { Form, FormGroup, Title } from '@patternfly/react-core';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import ContentError from '@components/ContentError';
|
||||
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 useRequest from '@util/useRequest';
|
||||
import { required } from '@util/validators';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
|
||||
function ScheduleFormFields({ i18n, zoneOptions }) {
|
||||
const [startDateTime, startDateTimeMeta] = useField({
|
||||
name: 'startDateTime',
|
||||
validate: required(
|
||||
i18n._(t`Select a valid date and time for this field`),
|
||||
i18n
|
||||
),
|
||||
});
|
||||
const [timezone, timezoneMeta] = useField({
|
||||
name: 'timezone',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
const [frequency, frequencyMeta] = useField({
|
||||
name: 'frequency',
|
||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
id="schedule-name"
|
||||
label={i18n._(t`Name`)}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="schedule-description"
|
||||
label={i18n._(t`Description`)}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="schedule-start-datetime"
|
||||
helperTextInvalid={startDateTimeMeta.error}
|
||||
isRequired
|
||||
isValid={!startDateTimeMeta.touched || !startDateTimeMeta.error}
|
||||
label={i18n._(t`Start date/time`)}
|
||||
>
|
||||
<input
|
||||
className="pf-c-form-control"
|
||||
type="datetime-local"
|
||||
id="schedule-start-datetime"
|
||||
step="1"
|
||||
{...startDateTime}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name="timezone"
|
||||
fieldId="schedule-timezone"
|
||||
helperTextInvalid={timezoneMeta.error}
|
||||
isRequired
|
||||
isValid={!timezoneMeta.touched || !timezoneMeta.error}
|
||||
label={i18n._(t`Local time zone`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-timezone"
|
||||
data={zoneOptions}
|
||||
{...timezone}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name="frequency"
|
||||
fieldId="schedule-requency"
|
||||
helperTextInvalid={frequencyMeta.error}
|
||||
isRequired
|
||||
isValid={!frequencyMeta.touched || !frequencyMeta.error}
|
||||
label={i18n._(t`Run frequency`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
id="schedule-frequency"
|
||||
data={[
|
||||
{ value: 'none', key: 'none', label: i18n._(t`None (run once)`) },
|
||||
{ value: 'minute', key: 'minute', label: i18n._(t`Minute`) },
|
||||
{ value: 'hour', key: 'hour', label: i18n._(t`Hour`) },
|
||||
{ value: 'day', key: 'day', label: i18n._(t`Day`) },
|
||||
{ value: 'week', key: 'week', label: i18n._(t`Week`) },
|
||||
{ value: 'month', key: 'month', label: i18n._(t`Month`) },
|
||||
{ value: 'year', key: 'year', label: i18n._(t`Year`) },
|
||||
]}
|
||||
{...frequency}
|
||||
/>
|
||||
</FormGroup>
|
||||
{frequency.value !== 'none' && (
|
||||
<SubFormLayout>
|
||||
<Title size="md">{i18n._(t`Frequency Details`)}</Title>
|
||||
<FormColumnLayout>
|
||||
<FrequencyDetailSubform />
|
||||
</FormColumnLayout>
|
||||
</SubFormLayout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduleForm({
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
i18n,
|
||||
schedule,
|
||||
submitError,
|
||||
...rest
|
||||
}) {
|
||||
const {
|
||||
request: loadZoneInfo,
|
||||
error: contentError,
|
||||
contentLoading,
|
||||
result: zoneOptions,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SchedulesAPI.readZoneInfo();
|
||||
return data.map(zone => {
|
||||
return {
|
||||
value: zone.name,
|
||||
key: zone.name,
|
||||
label: zone.name,
|
||||
};
|
||||
});
|
||||
}, [])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadZoneInfo();
|
||||
}, [loadZoneInfo]);
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
if (contentLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Config>
|
||||
{() => {
|
||||
const now = new Date();
|
||||
const closestQuarterHour = new Date(
|
||||
Math.ceil(now.getTime() / 900000) * 900000
|
||||
);
|
||||
const tomorrow = new Date(closestQuarterHour);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
end: 'never',
|
||||
endDateTime: dateToInputDateTime(tomorrow),
|
||||
frequency: 'none',
|
||||
interval: 1,
|
||||
name: schedule.name || '',
|
||||
occurrences: 1,
|
||||
runOn: 'number',
|
||||
startDateTime: dateToInputDateTime(closestQuarterHour),
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
validate={values => {
|
||||
const errors = {};
|
||||
const { end, endDateTime, startDateTime } = values;
|
||||
|
||||
if (
|
||||
end === 'onDate' &&
|
||||
new Date(startDateTime) > new Date(endDateTime)
|
||||
) {
|
||||
errors.endDateTime = i18n._(
|
||||
t`Please select an end date/time that comes after the start date/time.`
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}}
|
||||
>
|
||||
{formik => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<ScheduleFormFields
|
||||
i18n={i18n}
|
||||
zoneOptions={zoneOptions}
|
||||
{...rest}
|
||||
/>
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}}
|
||||
</Config>
|
||||
);
|
||||
}
|
||||
|
||||
ScheduleForm.propTypes = {
|
||||
handleCancel: func.isRequired,
|
||||
handleSubmit: func.isRequired,
|
||||
schedule: shape({}),
|
||||
submitError: shape(),
|
||||
};
|
||||
|
||||
ScheduleForm.defaultProps = {
|
||||
schedule: {},
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
export default withI18n()(ScheduleForm);
|
||||
415
awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx
Normal file
415
awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx
Normal file
@ -0,0 +1,415 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '@api';
|
||||
import ScheduleForm from './ScheduleForm';
|
||||
|
||||
jest.mock('@api/models/Schedules');
|
||||
|
||||
let wrapper;
|
||||
|
||||
const defaultFieldsVisible = () => {
|
||||
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Start date/time"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Local time zone"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Run frequency"]').length).toBe(1);
|
||||
};
|
||||
|
||||
describe('<ScheduleForm />', () => {
|
||||
describe('Error', () => {
|
||||
test('should display error when error occurs while loading', async () => {
|
||||
SchedulesAPI.readZoneInfo.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/schedules/zoneinfo',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 500,
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('Cancel', () => {
|
||||
test('should make the appropriate callback', async () => {
|
||||
const handleCancel = jest.fn();
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={handleCancel} />
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Cancel"]').simulate('click');
|
||||
});
|
||||
expect(handleCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe('Add', () => {
|
||||
beforeAll(async () => {
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
});
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
test('initially renders expected fields and values', () => {
|
||||
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);
|
||||
|
||||
expect(wrapper.find('input#schedule-name').prop('value')).toBe('');
|
||||
expect(wrapper.find('input#schedule-description').prop('value')).toBe('');
|
||||
expect(
|
||||
wrapper.find('input#schedule-start-datetime').prop('value')
|
||||
).toMatch(/\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d/);
|
||||
expect(wrapper.find('select#schedule-timezone').prop('value')).toBe(
|
||||
'America/New_York'
|
||||
);
|
||||
expect(wrapper.find('select#schedule-frequency').prop('value')).toBe(
|
||||
'none'
|
||||
);
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to minute', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to hour', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('hour', {
|
||||
target: { value: 'hour', key: 'hour', label: 'Hour' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to day', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('day', {
|
||||
target: { value: 'day', key: 'day', label: 'Day' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to week', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('week', {
|
||||
target: { value: 'week', key: 'week', label: 'Week' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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(1);
|
||||
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);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to month', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('month', {
|
||||
target: { value: 'month', key: 'month', label: 'Month' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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(1);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('month run on options displayed correctly as date changes', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-23T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 23');
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The fourth Monday'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-27T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 27');
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The fourth Friday'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-last-day + label').text()).toBe(
|
||||
'The last Friday'
|
||||
);
|
||||
});
|
||||
test('month run on cleared when last day selected but date changes from one of the last seven days of the month', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', {
|
||||
target: { name: 'runOn' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-15T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 15');
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The third Sunday'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#run-on-number').invoke('onChange')('number', {
|
||||
target: { name: 'runOn' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
});
|
||||
test('correct frequency details fields and values shown when frequency changed to year', async () => {
|
||||
const runFrequencySelect = wrapper.find(
|
||||
'FormGroup[label="Run frequency"] FormSelect'
|
||||
);
|
||||
await act(async () => {
|
||||
runFrequencySelect.invoke('onChange')('year', {
|
||||
target: { value: 'year', key: 'year', label: 'Year' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
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(1);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(0);
|
||||
expect(wrapper.find('FormGroup[label="End date/time"]').length).toBe(0);
|
||||
|
||||
expect(wrapper.find('input#schedule-run-every').prop('value')).toBe(1);
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
});
|
||||
test('year run on options displayed correctly as date changes', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-23T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe(
|
||||
'March 23'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The fourth Monday in March'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-27T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe(
|
||||
'March 27'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The fourth Friday in March'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-last-day + label').text()).toBe(
|
||||
'The last Friday in March'
|
||||
);
|
||||
});
|
||||
test('occurrences field properly shown when that run on selection is made', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-after').invoke('onChange')('after', {
|
||||
target: { name: 'end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('FormGroup[label="Occurrences"]').length).toBe(1);
|
||||
expect(wrapper.find('input#schedule-occurrences').prop('value')).toBe(1);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-never').invoke('onChange')('never', {
|
||||
target: { name: 'end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
});
|
||||
test('year run on cleared when last day selected but date changes from one of the last seven days of the month', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', {
|
||||
target: { name: 'runOn' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-start-datetime').simulate('change', {
|
||||
target: { value: '2020-03-15T01:45:00', name: 'startDateTime' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-number + label').text()).toBe(
|
||||
'March 15'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#run-on-day + label').text()).toBe(
|
||||
'The third Sunday in March'
|
||||
);
|
||||
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
|
||||
});
|
||||
test('error shown when end date/time comes before start date/time', async () => {
|
||||
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);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-on-date').invoke('onChange')('onDate', {
|
||||
target: { name: 'end' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('#schedule-end-datetime-helper').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('input#schedule-end-datetime').invoke('onChange')(
|
||||
'2020-03-14T01:45:00',
|
||||
{
|
||||
target: { name: 'endDateTime' },
|
||||
}
|
||||
);
|
||||
});
|
||||
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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -26,6 +26,7 @@ class Project extends Component {
|
||||
isInitialized: false,
|
||||
isNotifAdmin: false,
|
||||
};
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
this.loadProject = this.loadProject.bind(this);
|
||||
this.loadProjectAndRoles = this.loadProjectAndRoles.bind(this);
|
||||
this.loadSchedules = this.loadSchedules.bind(this);
|
||||
@ -91,6 +92,11 @@ class Project extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
createSchedule(data) {
|
||||
const { project } = this.state;
|
||||
return ProjectsAPI.createSchedule(project.id, data);
|
||||
}
|
||||
|
||||
loadScheduleOptions() {
|
||||
const { project } = this.state;
|
||||
return ProjectsAPI.readScheduleOptions(project.id);
|
||||
@ -233,6 +239,7 @@ class Project extends Component {
|
||||
<Schedules
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={project}
|
||||
createSchedule={this.createSchedule}
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
|
||||
@ -66,22 +66,7 @@ describe('<ProjectDetail />', () => {
|
||||
});
|
||||
|
||||
test('should render Details', () => {
|
||||
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
|
||||
context: {
|
||||
linguiPublisher: {
|
||||
i18n: {
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||
function assertDetail(label, value) {
|
||||
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||
|
||||
@ -44,6 +44,7 @@ class Projects extends Component {
|
||||
[`/projects/${project.id}/job_templates`]: i18n._(t`Job Templates`),
|
||||
|
||||
[`${projectSchedulesPath}`]: i18n._(t`Schedules`),
|
||||
[`${projectSchedulesPath}/add`]: i18n._(t`Create New Schedule`),
|
||||
[`${projectSchedulesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${projectSchedulesPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Edit Details`
|
||||
|
||||
@ -59,6 +59,10 @@ function Template({ i18n, me, setBreadcrumb }) {
|
||||
loadTemplateAndRoles();
|
||||
}, [loadTemplateAndRoles, location.pathname]);
|
||||
|
||||
const createSchedule = data => {
|
||||
return JobTemplatesAPI.createSchedule(templateId, data);
|
||||
};
|
||||
|
||||
const loadScheduleOptions = () => {
|
||||
return JobTemplatesAPI.readScheduleOptions(templateId);
|
||||
};
|
||||
@ -173,6 +177,7 @@ function Template({ i18n, me, setBreadcrumb }) {
|
||||
path="/templates/:templateType/:id/schedules"
|
||||
>
|
||||
<Schedules
|
||||
createSchedule={createSchedule}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={template}
|
||||
loadSchedules={loadSchedules}
|
||||
|
||||
@ -62,6 +62,9 @@ class Templates extends Component {
|
||||
[`/templates/${template.type}/${template.id}/schedules`]: i18n._(
|
||||
t`Schedules`
|
||||
),
|
||||
[`/templates/${template.type}/${template.id}/schedules/add`]: i18n._(
|
||||
t`Create New Schedule`
|
||||
),
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
schedule.id}`]: `${schedule && schedule.name}`,
|
||||
[`/templates/${template.type}/${template.id}/schedules/${schedule &&
|
||||
|
||||
@ -34,6 +34,7 @@ class WorkflowJobTemplate extends Component {
|
||||
webhook_key: null,
|
||||
isNotifAdmin: false,
|
||||
};
|
||||
this.createSchedule = this.createSchedule.bind(this);
|
||||
this.loadTemplate = this.loadTemplate.bind(this);
|
||||
this.loadSchedules = this.loadSchedules.bind(this);
|
||||
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
|
||||
@ -91,6 +92,11 @@ class WorkflowJobTemplate extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
createSchedule(data) {
|
||||
const { template } = this.state;
|
||||
return WorkflowJobTemplatesAPI.createSchedule(template.id, data);
|
||||
}
|
||||
|
||||
loadScheduleOptions() {
|
||||
const { template } = this.state;
|
||||
return WorkflowJobTemplatesAPI.readScheduleOptions(template.id);
|
||||
@ -271,6 +277,7 @@ class WorkflowJobTemplate extends Component {
|
||||
<Schedules
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
unifiedJobTemplate={template}
|
||||
createSchedule={this.createSchedule}
|
||||
loadSchedules={this.loadSchedules}
|
||||
loadScheduleOptions={this.loadScheduleOptions}
|
||||
/>
|
||||
|
||||
@ -14,22 +14,7 @@ describe('<UserDetail />', () => {
|
||||
});
|
||||
|
||||
test('should render Details', () => {
|
||||
const wrapper = mountWithContexts(<UserDetail user={mockDetails} />, {
|
||||
context: {
|
||||
linguiPublisher: {
|
||||
i18n: {
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithContexts(<UserDetail user={mockDetails} />);
|
||||
function assertDetail(label, value) {
|
||||
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { t } from '@lingui/macro';
|
||||
import { getLanguage } from './language';
|
||||
|
||||
const prependZeros = value => value.toString().padStart(2, 0);
|
||||
|
||||
export function formatDateString(dateString, lang = getLanguage(navigator)) {
|
||||
return new Date(dateString).toLocaleString(lang);
|
||||
}
|
||||
@ -12,3 +15,100 @@ export function formatDateStringUTC(dateString, lang = getLanguage(navigator)) {
|
||||
export function secondsToHHMMSS(seconds) {
|
||||
return new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
}
|
||||
|
||||
export function dateToInputDateTime(dateObj) {
|
||||
// input type="date-time" expects values to be formatted
|
||||
// like: YYYY-MM-DDTHH-MM-SS
|
||||
const year = dateObj.getFullYear();
|
||||
const month = prependZeros(dateObj.getMonth() + 1);
|
||||
const day = prependZeros(dateObj.getDate());
|
||||
const hour = prependZeros(dateObj.getHours());
|
||||
const minute = prependZeros(dateObj.getMinutes());
|
||||
const second = prependZeros(dateObj.getSeconds());
|
||||
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
export function getDaysInMonth(dateString) {
|
||||
const dateObj = new Date(dateString);
|
||||
return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 0).getDate();
|
||||
}
|
||||
|
||||
export function getWeekNumber(dateString) {
|
||||
const dateObj = new Date(dateString);
|
||||
const dayOfMonth = dateObj.getDate();
|
||||
const dayOfWeek = dateObj.getDay();
|
||||
if (dayOfMonth < 8) {
|
||||
return 1;
|
||||
}
|
||||
dateObj.setDate(dayOfMonth - dayOfWeek + 1);
|
||||
return Math.ceil(dayOfMonth / 7);
|
||||
}
|
||||
|
||||
export function getDayString(dayIndex, i18n) {
|
||||
switch (dayIndex) {
|
||||
case 0:
|
||||
return i18n._(t`Sunday`);
|
||||
case 1:
|
||||
return i18n._(t`Monday`);
|
||||
case 2:
|
||||
return i18n._(t`Tuesday`);
|
||||
case 3:
|
||||
return i18n._(t`Wednesday`);
|
||||
case 4:
|
||||
return i18n._(t`Thursday`);
|
||||
case 5:
|
||||
return i18n._(t`Friday`);
|
||||
case 6:
|
||||
return i18n._(t`Saturday`);
|
||||
default:
|
||||
throw new Error(i18n._(t`Unrecognized day index`));
|
||||
}
|
||||
}
|
||||
|
||||
export function getWeekString(weekNumber, i18n) {
|
||||
switch (weekNumber) {
|
||||
case 1:
|
||||
return i18n._(t`first`);
|
||||
case 2:
|
||||
return i18n._(t`second`);
|
||||
case 3:
|
||||
return i18n._(t`third`);
|
||||
case 4:
|
||||
return i18n._(t`fourth`);
|
||||
case 5:
|
||||
return i18n._(t`fifth`);
|
||||
default:
|
||||
throw new Error(i18n._(t`Unrecognized week number`));
|
||||
}
|
||||
}
|
||||
|
||||
export function getMonthString(monthIndex, i18n) {
|
||||
switch (monthIndex) {
|
||||
case 0:
|
||||
return i18n._(t`January`);
|
||||
case 1:
|
||||
return i18n._(t`February`);
|
||||
case 2:
|
||||
return i18n._(t`March`);
|
||||
case 3:
|
||||
return i18n._(t`April`);
|
||||
case 4:
|
||||
return i18n._(t`May`);
|
||||
case 5:
|
||||
return i18n._(t`June`);
|
||||
case 6:
|
||||
return i18n._(t`July`);
|
||||
case 7:
|
||||
return i18n._(t`August`);
|
||||
case 8:
|
||||
return i18n._(t`September`);
|
||||
case 9:
|
||||
return i18n._(t`October`);
|
||||
case 10:
|
||||
return i18n._(t`November`);
|
||||
case 11:
|
||||
return i18n._(t`December`);
|
||||
default:
|
||||
throw new Error(i18n._(t`Unrecognized month index`));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,25 @@
|
||||
import { formatDateString } from './dates';
|
||||
import {
|
||||
dateToInputDateTime,
|
||||
getDaysInMonth,
|
||||
getDayString,
|
||||
getMonthString,
|
||||
getWeekNumber,
|
||||
getWeekString,
|
||||
formatDateString,
|
||||
formatDateStringUTC,
|
||||
secondsToHHMMSS,
|
||||
} from './dates';
|
||||
|
||||
const i18n = {
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
},
|
||||
};
|
||||
|
||||
describe('formatDateString', () => {
|
||||
test('it returns the expected value', () => {
|
||||
@ -11,3 +32,90 @@ describe('formatDateString', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDateStringUTC', () => {
|
||||
test('it returns the expected value', () => {
|
||||
const lang = 'en-US';
|
||||
expect(formatDateStringUTC('', lang)).toEqual('Invalid Date');
|
||||
expect(formatDateStringUTC({}, lang)).toEqual('Invalid Date');
|
||||
expect(formatDateStringUTC(undefined, lang)).toEqual('Invalid Date');
|
||||
expect(formatDateStringUTC('2018-01-31T01:14:52.969227Z', lang)).toEqual(
|
||||
'1/31/2018, 1:14:52 AM'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('secondsToHHMMSS', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(secondsToHHMMSS(50000)).toEqual('13:53:20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateToInputDateTime', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(
|
||||
dateToInputDateTime(new Date('2018-01-31T01:14:52.969227Z'))
|
||||
).toEqual('2018-01-31T01:14:52');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDaysInMonth', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(getDaysInMonth('2020-02-15T00:00:00Z')).toEqual(29);
|
||||
expect(getDaysInMonth('2020-03-15T00:00:00Z')).toEqual(31);
|
||||
expect(getDaysInMonth('2020-04-15T00:00:00Z')).toEqual(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeekNumber', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(getWeekNumber('2020-02-01T00:00:00Z')).toEqual(1);
|
||||
expect(getWeekNumber('2020-02-08T00:00:00Z')).toEqual(2);
|
||||
expect(getWeekNumber('2020-02-15T00:00:00Z')).toEqual(3);
|
||||
expect(getWeekNumber('2020-02-22T00:00:00Z')).toEqual(4);
|
||||
expect(getWeekNumber('2020-02-29T00:00:00Z')).toEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDayString', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(getDayString(0, i18n)).toEqual('Sunday');
|
||||
expect(getDayString(1, i18n)).toEqual('Monday');
|
||||
expect(getDayString(2, i18n)).toEqual('Tuesday');
|
||||
expect(getDayString(3, i18n)).toEqual('Wednesday');
|
||||
expect(getDayString(4, i18n)).toEqual('Thursday');
|
||||
expect(getDayString(5, i18n)).toEqual('Friday');
|
||||
expect(getDayString(6, i18n)).toEqual('Saturday');
|
||||
expect(() => getDayString(7, i18n)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeekString', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(() => getWeekString(0, i18n)).toThrow();
|
||||
expect(getWeekString(1, i18n)).toEqual('first');
|
||||
expect(getWeekString(2, i18n)).toEqual('second');
|
||||
expect(getWeekString(3, i18n)).toEqual('third');
|
||||
expect(getWeekString(4, i18n)).toEqual('fourth');
|
||||
expect(getWeekString(5, i18n)).toEqual('fifth');
|
||||
expect(() => getWeekString(6, i18n)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMonthString', () => {
|
||||
test('it returns the expected value', () => {
|
||||
expect(getMonthString(0, i18n)).toEqual('January');
|
||||
expect(getMonthString(1, i18n)).toEqual('February');
|
||||
expect(getMonthString(2, i18n)).toEqual('March');
|
||||
expect(getMonthString(3, i18n)).toEqual('April');
|
||||
expect(getMonthString(4, i18n)).toEqual('May');
|
||||
expect(getMonthString(5, i18n)).toEqual('June');
|
||||
expect(getMonthString(6, i18n)).toEqual('July');
|
||||
expect(getMonthString(7, i18n)).toEqual('August');
|
||||
expect(getMonthString(8, i18n)).toEqual('September');
|
||||
expect(getMonthString(9, i18n)).toEqual('October');
|
||||
expect(getMonthString(10, i18n)).toEqual('November');
|
||||
expect(getMonthString(11, i18n)).toEqual('December');
|
||||
expect(() => getMonthString(12, i18n)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@ -27,7 +27,14 @@ const defaultContexts = {
|
||||
linguiPublisher: {
|
||||
i18n: {
|
||||
...originalI18n,
|
||||
_: key => key.id, // provide _ macro, for just passing down the key
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
}, // provide _ macro, for just passing down the key
|
||||
toJSON: () => '/i18n/',
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user