{
- 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({
diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx
index 8a3376e504..0438a01497 100644
--- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx
+++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx
@@ -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('', () => {
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('', () => {
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 () => {
diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx
new file mode 100644
index 0000000000..3e12bd9a58
--- /dev/null
+++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx
@@ -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 { shape } 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 (
+
+
+
+ history.push(`${pathRoot}schedules/${schedule.id}/details`)
+ }
+ handleSubmit={handleSubmit}
+ submitError={formSubmitError}
+ />
+
+
+ );
+}
+
+ScheduleEdit.propTypes = {
+ schedule: shape({}).isRequired,
+};
+
+ScheduleEdit.defaultProps = {};
+
+export default withI18n()(ScheduleEdit);
diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx
new file mode 100644
index 0000000000..21c7b700b6
--- /dev/null
+++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx
@@ -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('', () => {
+ beforeAll(async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ 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',
+ });
+ });
+});
diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/index.js b/awx/ui_next/src/components/Schedule/ScheduleEdit/index.js
new file mode 100644
index 0000000000..2e2e554f1f
--- /dev/null
+++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/index.js
@@ -0,0 +1 @@
+export { default } from './ScheduleEdit';
diff --git a/awx/ui_next/src/components/Schedule/index.js b/awx/ui_next/src/components/Schedule/index.js
index f70d8ef3fd..5342b7ae20 100644
--- a/awx/ui_next/src/components/Schedule/index.js
+++ b/awx/ui_next/src/components/Schedule/index.js
@@ -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';
diff --git a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx
index 2ca554b8dd..c062258354 100644
--- a/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx
+++ b/awx/ui_next/src/components/Schedule/shared/FrequencyDetailSubform.jsx
@@ -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 }) => {
{
- 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 }) => {
/>
{
- 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 }) => {
/>
{
- 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 }) => {
/>
{
- 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 }) => {
/>
{
- 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 }) => {
/>
{
- 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 }) => {
/>
{
- updateDaysOfWeek('SA', checked);
+ updateDaysOfWeek(RRule.SA, checked);
}}
aria-label={i18n._(t`Saturday`)}
id="schedule-days-of-week-sat"
diff --git a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx
index 41f43da561..ddb00ccbbf 100644
--- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx
+++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx
@@ -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 ;
+ if (contentError || rruleError) {
+ return ;
}
if (contentLoading) {
@@ -163,24 +322,7 @@ function ScheduleForm({
{() => {
return (
{
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.`
);
}
diff --git a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx
index 2487f9beee..45b802dd0f 100644
--- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx
+++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.test.jsx
@@ -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('', () => {
describe('Error', () => {
test('should display error when error occurs while loading', async () => {
@@ -296,20 +345,321 @@ describe('', () => {
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(
+
);
});
+ 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(
+
+ );
+ });
+ 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(
+
+ );
+ });
+ 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(
+
+ );
+ 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(
+
+ );
+ });
+ 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(
+
+ );
+ 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(
+
+ );
+ 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);
+ });
});
});
});
diff --git a/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js b/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js
new file mode 100644
index 0000000000..38828b60ab
--- /dev/null
+++ b/awx/ui_next/src/components/Schedule/shared/buildRuleObj.js
@@ -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;
+}
diff --git a/awx/ui_next/src/screens/Project/Projects.jsx b/awx/ui_next/src/screens/Project/Projects.jsx
index 6dba9ce98e..a5bcddd4f0 100644
--- a/awx/ui_next/src/screens/Project/Projects.jsx
+++ b/awx/ui_next/src/screens/Project/Projects.jsx
@@ -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 });
diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx
index 3ade3ef294..2f2cf5497f 100644
--- a/awx/ui_next/src/screens/Template/Templates.jsx
+++ b/awx/ui_next/src/screens/Template/Templates.jsx
@@ -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 });
};