mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Adds Prompting for schedule
This commit is contained in:
parent
61c0beccff
commit
c608d761a2
@ -160,6 +160,14 @@ function JobListItem({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{job.job_explanation && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Explanation`)}
|
||||
value={job.job_explanation}
|
||||
/>
|
||||
)}
|
||||
</DetailList>
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
|
||||
@ -11,7 +11,8 @@ export default function usePreviewStep(
|
||||
resource,
|
||||
surveyConfig,
|
||||
hasErrors,
|
||||
showStep
|
||||
showStep,
|
||||
nextButtonText
|
||||
) {
|
||||
return {
|
||||
step: showStep
|
||||
@ -31,7 +32,7 @@ export default function usePreviewStep(
|
||||
/>
|
||||
),
|
||||
enableNext: !hasErrors,
|
||||
nextButtonText: i18n._(t`Launch`),
|
||||
nextButtonText: nextButtonText || i18n._(t`Launch`),
|
||||
}
|
||||
: null,
|
||||
initialValues: {},
|
||||
|
||||
@ -26,12 +26,7 @@ function Schedule({ i18n, setBreadcrumb, resource }) {
|
||||
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
const {
|
||||
isLoading: contentLoading,
|
||||
error: contentError,
|
||||
request: loadData,
|
||||
result: schedule,
|
||||
} = useRequest(
|
||||
const { isLoading, error, request: loadData, result: schedule } = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SchedulesAPI.readDetail(scheduleId);
|
||||
|
||||
@ -68,7 +63,7 @@ function Schedule({ i18n, setBreadcrumb, resource }) {
|
||||
},
|
||||
];
|
||||
|
||||
if (contentLoading) {
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
@ -85,8 +80,8 @@ function Schedule({ i18n, setBreadcrumb, resource }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
let showCardHeader = true;
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { func } from 'prop-types';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import yaml from 'js-yaml';
|
||||
import { CardBody } from '../../Card';
|
||||
import { parseVariableField } from '../../../util/yaml';
|
||||
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
|
||||
import getSurveyValues from '../../../util/prompt/getSurveyValues';
|
||||
import { getAddedAndRemoved } from '../../../util/lists';
|
||||
|
||||
function ScheduleAdd({ i18n, resource, apiModel }) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
@ -15,14 +22,63 @@ function ScheduleAdd({ i18n, resource, apiModel }) {
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const handleSubmit = async (values, launchConfig, surveyConfig) => {
|
||||
const {
|
||||
inventory,
|
||||
extra_vars,
|
||||
originalCredentials,
|
||||
end,
|
||||
frequency,
|
||||
interval,
|
||||
startDateTime,
|
||||
timezone,
|
||||
occurrences,
|
||||
runOn,
|
||||
runOnTheDay,
|
||||
runOnTheMonth,
|
||||
runOnDayMonth,
|
||||
runOnDayNumber,
|
||||
endDateTime,
|
||||
runOnTheOccurrence,
|
||||
credentials,
|
||||
daysOfWeek,
|
||||
...submitValues
|
||||
} = values;
|
||||
const { added } = getAddedAndRemoved(
|
||||
resource?.summary_fields.credentials,
|
||||
credentials
|
||||
);
|
||||
let extraVars;
|
||||
const surveyValues = getSurveyValues(values);
|
||||
const initialExtraVars =
|
||||
launchConfig?.ask_variables_on_launch && (values.extra_vars || '---');
|
||||
if (surveyConfig?.spec) {
|
||||
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues));
|
||||
} else {
|
||||
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
|
||||
}
|
||||
submitValues.extra_data = extraVars && parseVariableField(extraVars);
|
||||
delete values.extra_vars;
|
||||
if (inventory) {
|
||||
submitValues.inventory = inventory.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values, i18n));
|
||||
|
||||
const { id: scheduleId } = await apiModel.createSchedule(resource.id, {
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
} = await apiModel.createSchedule(resource.id, {
|
||||
...submitValues,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
});
|
||||
|
||||
if (credentials?.length > 0) {
|
||||
await Promise.all(
|
||||
added.map(({ id: credentialId }) =>
|
||||
SchedulesAPI.associateCredential(scheduleId, credentialId)
|
||||
)
|
||||
);
|
||||
}
|
||||
history.push(`${pathRoot}schedules/${scheduleId}`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
@ -36,6 +92,7 @@ function ScheduleAdd({ i18n, resource, apiModel }) {
|
||||
handleCancel={() => history.push(`${pathRoot}schedules`)}
|
||||
handleSubmit={handleSubmit}
|
||||
submitError={formSubmitError}
|
||||
resource={resource}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@ -5,10 +5,12 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api';
|
||||
import ScheduleAdd from './ScheduleAdd';
|
||||
|
||||
jest.mock('../../../api/models/Schedules');
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
@ -17,22 +19,62 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
},
|
||||
],
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: true,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
defaults: {
|
||||
extra_vars: '---',
|
||||
diff_mode: false,
|
||||
limit: '',
|
||||
job_tags: '',
|
||||
skip_tags: '',
|
||||
job_type: 'run',
|
||||
verbosity: 0,
|
||||
inventory: {
|
||||
name: null,
|
||||
id: null,
|
||||
},
|
||||
scm_branch: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.createSchedule.mockResolvedValue({ data: { id: 3 } });
|
||||
|
||||
let wrapper;
|
||||
|
||||
const createSchedule = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('<ScheduleAdd />', () => {
|
||||
beforeAll(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleAdd createSchedule={createSchedule} />
|
||||
<ScheduleAdd
|
||||
apiModel={JobTemplatesAPI}
|
||||
resource={{
|
||||
id: 700,
|
||||
type: 'job_template',
|
||||
inventory: 2,
|
||||
summary_fields: { credentials: [] },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -42,7 +84,7 @@ describe('<ScheduleAdd />', () => {
|
||||
});
|
||||
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
@ -52,16 +94,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run once schedule',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'after',
|
||||
frequency: 'minute',
|
||||
@ -72,16 +115,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'onDate',
|
||||
endDateTime: '2020-03-26T10:45:00',
|
||||
@ -92,16 +136,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run every hour until date',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'day',
|
||||
@ -111,16 +156,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run daily',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
@ -132,15 +178,16 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'month',
|
||||
@ -153,16 +200,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run on the first day of the month',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
endDateTime: '2020-03-26T11:00:00',
|
||||
@ -177,16 +225,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -200,16 +249,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first day of March',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -224,16 +274,17 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the second Friday in April',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -248,11 +299,119 @@ describe('<ScheduleAdd />', () => {
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
expect(createSchedule).toHaveBeenCalledWith({
|
||||
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first weekday in October',
|
||||
extra_data: {},
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
|
||||
});
|
||||
});
|
||||
|
||||
test('should submit prompted data properly', async () => {
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
name: 'Foo',
|
||||
id: 1,
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'Bar',
|
||||
id: 2,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
related_search_fields: [],
|
||||
actions: {
|
||||
GET: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Prompt"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem')
|
||||
.at(0)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem')
|
||||
.at(1)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Wizard').length).toBe(0);
|
||||
// console.log(wrapper.debug());
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
name: 'Schedule',
|
||||
end: 'never',
|
||||
endDateTime: '2021-01-29T14:15:00',
|
||||
frequency: 'none',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
skip_tags: '',
|
||||
inventory: { name: 'inventory', id: 45 },
|
||||
credentials: [
|
||||
{ name: 'cred 1', id: 10 },
|
||||
{ name: 'cred 2', id: 20 },
|
||||
],
|
||||
startDateTime: '2021-01-28T14:15:00',
|
||||
timezone: 'America/New_York',
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(JobTemplatesAPI.createSchedule).toBeCalledWith(700, {
|
||||
extra_data: {},
|
||||
inventory: 45,
|
||||
name: 'Schedule',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY',
|
||||
skip_tags: '',
|
||||
});
|
||||
expect(SchedulesAPI.associateCredential).toBeCalledWith(3, 10);
|
||||
expect(SchedulesAPI.associateCredential).toBeCalledWith(3, 20);
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,10 +4,16 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { RRule } from 'rrule';
|
||||
import { shape } from 'prop-types';
|
||||
import { Card } from '@patternfly/react-core';
|
||||
import yaml from 'js-yaml';
|
||||
import { CardBody } from '../../Card';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import buildRuleObj from '../shared/buildRuleObj';
|
||||
import ScheduleForm from '../shared/ScheduleForm';
|
||||
import { getAddedAndRemoved } from '../../../util/lists';
|
||||
|
||||
import { parseVariableField } from '../../../util/yaml';
|
||||
import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
|
||||
import getSurveyValues from '../../../util/prompt/getSurveyValues';
|
||||
|
||||
function ScheduleEdit({ i18n, schedule, resource }) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
@ -16,16 +22,69 @@ function ScheduleEdit({ i18n, schedule, resource }) {
|
||||
const { pathname } = location;
|
||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const handleSubmit = async (
|
||||
values,
|
||||
launchConfig,
|
||||
surveyConfig,
|
||||
scheduleCredentials = []
|
||||
) => {
|
||||
const {
|
||||
inventory,
|
||||
credentials = [],
|
||||
end,
|
||||
frequency,
|
||||
interval,
|
||||
startDateTime,
|
||||
timezone,
|
||||
occurrences,
|
||||
runOn,
|
||||
runOnTheDay,
|
||||
runOnTheMonth,
|
||||
runOnDayMonth,
|
||||
runOnDayNumber,
|
||||
endDateTime,
|
||||
runOnTheOccurrence,
|
||||
daysOfWeek,
|
||||
...submitValues
|
||||
} = values;
|
||||
const { added, removed } = getAddedAndRemoved(
|
||||
[...resource?.summary_fields.credentials, ...scheduleCredentials],
|
||||
credentials
|
||||
);
|
||||
|
||||
let extraVars;
|
||||
const surveyValues = getSurveyValues(values);
|
||||
const initialExtraVars =
|
||||
launchConfig?.ask_variables_on_launch && (values.extra_vars || '---');
|
||||
if (surveyConfig?.spec) {
|
||||
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues));
|
||||
} else {
|
||||
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
|
||||
}
|
||||
submitValues.extra_data = extraVars && parseVariableField(extraVars);
|
||||
delete values.extra_vars;
|
||||
if (inventory) {
|
||||
submitValues.inventory = inventory.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = new RRule(buildRuleObj(values, i18n));
|
||||
const {
|
||||
data: { id: scheduleId },
|
||||
} = await SchedulesAPI.update(schedule.id, {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
...submitValues,
|
||||
rrule: rule.toString().replace(/\n/g, ' '),
|
||||
});
|
||||
if (values.credentials?.length > 0) {
|
||||
await Promise.all([
|
||||
...removed.map(({ id }) =>
|
||||
SchedulesAPI.disassociateCredential(scheduleId, id)
|
||||
),
|
||||
...added.map(({ id }) =>
|
||||
SchedulesAPI.associateCredential(scheduleId, id)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
history.push(`${pathRoot}schedules/${scheduleId}/details`);
|
||||
} catch (err) {
|
||||
@ -43,6 +102,7 @@ function ScheduleEdit({ i18n, schedule, resource }) {
|
||||
}
|
||||
handleSubmit={handleSubmit}
|
||||
submitError={formSubmitError}
|
||||
resource={resource}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@ -5,10 +5,20 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import {
|
||||
SchedulesAPI,
|
||||
JobTemplatesAPI,
|
||||
InventoriesAPI,
|
||||
CredentialsAPI,
|
||||
CredentialTypesAPI,
|
||||
} from '../../../api';
|
||||
import ScheduleEdit from './ScheduleEdit';
|
||||
|
||||
jest.mock('../../../api/models/Schedules');
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
jest.mock('../../../api/models/Credentials');
|
||||
jest.mock('../../../api/models/CredentialTypes');
|
||||
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
@ -18,6 +28,75 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
],
|
||||
});
|
||||
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: true,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: true,
|
||||
inventory_needed_to_start: true,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
defaults: {
|
||||
extra_vars: '---',
|
||||
diff_mode: false,
|
||||
limit: '',
|
||||
job_tags: '',
|
||||
skip_tags: '',
|
||||
job_type: 'run',
|
||||
verbosity: 0,
|
||||
inventory: {
|
||||
name: null,
|
||||
id: null,
|
||||
},
|
||||
scm_branch: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
SchedulesAPI.readCredentials.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ name: 'schedule credential 1', id: 1, kind: 'vault' },
|
||||
{ name: 'schedule credential 2', id: 2, kind: 'aws' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
|
||||
CredentialTypesAPI.loadAllTypes.mockResolvedValue([
|
||||
{ id: 1, name: 'ssh', kind: 'ssh' },
|
||||
]);
|
||||
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 3,
|
||||
results: [
|
||||
{ id: 1, name: 'Credential 1', kind: 'ssh', url: '' },
|
||||
{ id: 2, name: 'Credential 2', kind: 'ssh', url: '' },
|
||||
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
CredentialsAPI.readOptions.mockResolvedValue({
|
||||
data: { related_search_fields: [], actions: { GET: { filterabled: true } } },
|
||||
});
|
||||
|
||||
SchedulesAPI.update.mockResolvedValue({
|
||||
data: {
|
||||
id: 27,
|
||||
@ -37,13 +116,14 @@ const mockSchedule = {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
inventory: { id: 702, name: 'Inventory' },
|
||||
},
|
||||
created: '2020-04-02T18:43:12.664142Z',
|
||||
modified: '2020-04-02T18:43:12.664185Z',
|
||||
name: 'mock schedule',
|
||||
description: '',
|
||||
extra_data: {},
|
||||
inventory: null,
|
||||
inventory: 1,
|
||||
scm_branch: null,
|
||||
job_type: null,
|
||||
job_tags: null,
|
||||
@ -61,18 +141,33 @@ const mockSchedule = {
|
||||
};
|
||||
|
||||
describe('<ScheduleEdit />', () => {
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ScheduleEdit schedule={mockSchedule} />);
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleEdit
|
||||
schedule={mockSchedule}
|
||||
resource={{
|
||||
id: 700,
|
||||
type: 'job_template',
|
||||
iventory: 1,
|
||||
summary_fields: {
|
||||
credentials: [
|
||||
{ name: 'job template credential', id: 75, kind: 'ssh' },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'none',
|
||||
@ -85,13 +180,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run once schedule',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'after',
|
||||
frequency: 'minute',
|
||||
@ -105,13 +201,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run every 10 minutes 10 times',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'onDate',
|
||||
endDateTime: '2020-03-26T10:45:00',
|
||||
@ -125,13 +222,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run every hour until date',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'day',
|
||||
@ -144,13 +242,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run daily',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
@ -165,12 +264,13 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run weekly on mon/wed/fri',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'month',
|
||||
@ -186,13 +286,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run on the first day of the month',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
endDateTime: '2020-03-26T11:00:00',
|
||||
@ -210,13 +311,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Run monthly on the last Tuesday',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -233,13 +335,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first day of March',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -257,13 +360,14 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the second Friday in April',
|
||||
extra_data: {},
|
||||
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')({
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
description: 'test description',
|
||||
end: 'never',
|
||||
frequency: 'year',
|
||||
@ -281,8 +385,215 @@ describe('<ScheduleEdit />', () => {
|
||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||
description: 'test description',
|
||||
name: 'Yearly on the first weekday in October',
|
||||
extra_data: {},
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
|
||||
});
|
||||
});
|
||||
|
||||
test('should open with correct values and navigate through the Promptable fields properly', async () => {
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
name: 'Foo',
|
||||
id: 1,
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'Bar',
|
||||
id: 2,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
related_search_fields: [],
|
||||
actions: {
|
||||
GET: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Prompt"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('WizardNavItem').length).toBe(3);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem')
|
||||
.at(0)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem')
|
||||
.at(1)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
|
||||
expect(wrapper.find('CredentialChip').length).toBe(3);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-3"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-3"]')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
wrapper.update();
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Wizard').length).toBe(0);
|
||||
await act(async () => {
|
||||
wrapper.find('Formik').invoke('onSubmit')({
|
||||
name: mockSchedule.name,
|
||||
end: 'never',
|
||||
endDateTime: '2021-01-29T14:15:00',
|
||||
frequency: 'none',
|
||||
occurrences: 1,
|
||||
runOn: 'day',
|
||||
runOnDayMonth: 1,
|
||||
runOnDayNumber: 1,
|
||||
runOnTheDay: 'sunday',
|
||||
runOnTheMonth: 1,
|
||||
runOnTheOccurrence: 1,
|
||||
skip_tags: '',
|
||||
startDateTime: '2021-01-28T14:15:00',
|
||||
timezone: 'America/New_York',
|
||||
credentials: [
|
||||
{ id: 3, name: 'Credential 3', kind: 'ssh', url: '' },
|
||||
{ name: 'schedule credential 1', id: 1, kind: 'vault' },
|
||||
{ name: 'schedule credential 2', id: 2, kind: 'aws' },
|
||||
],
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(SchedulesAPI.update).toBeCalledWith(27, {
|
||||
extra_data: {},
|
||||
name: 'mock schedule',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY',
|
||||
skip_tags: '',
|
||||
});
|
||||
expect(SchedulesAPI.disassociateCredential).toBeCalledWith(27, 75);
|
||||
|
||||
expect(SchedulesAPI.associateCredential).toBeCalledWith(27, 3);
|
||||
});
|
||||
|
||||
test('should submit updated static form values, but original prompt form values', async () => {
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
name: 'Foo',
|
||||
id: 1,
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'Bar',
|
||||
id: 2,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
related_search_fields: [],
|
||||
actions: {
|
||||
GET: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () =>
|
||||
wrapper.find('input#schedule-name').simulate('change', {
|
||||
target: { value: 'foo', name: 'name' },
|
||||
})
|
||||
);
|
||||
wrapper.update();
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Prompt"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-2"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-2"]')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('WizardFooterInternal').prop('onClose')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Wizard').length).toBe(0);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[aria-label="Save"]').prop('onClick')()
|
||||
);
|
||||
expect(SchedulesAPI.update).toBeCalledWith(27, {
|
||||
description: '',
|
||||
extra_data: {},
|
||||
inventory: 702,
|
||||
name: 'foo',
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,22 +1,36 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
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 { Form, FormGroup, Title } from '@patternfly/react-core';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Title,
|
||||
ActionGroup,
|
||||
} from '@patternfly/react-core';
|
||||
import { Config } from '../../../contexts/Config';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import {
|
||||
SchedulesAPI,
|
||||
JobTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '../../../api';
|
||||
import AnsibleSelect from '../../AnsibleSelect';
|
||||
import ContentError from '../../ContentError';
|
||||
import ContentLoading from '../../ContentLoading';
|
||||
import FormActionGroup from '../../FormActionGroup/FormActionGroup';
|
||||
import FormField, { FormSubmitError } from '../../FormField';
|
||||
import { FormColumnLayout, SubFormLayout } from '../../FormLayout';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
SubFormLayout,
|
||||
FormFullWidthLayout,
|
||||
} from '../../FormLayout';
|
||||
import { dateToInputDateTime, formatDateStringUTC } from '../../../util/dates';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { required } from '../../../util/validators';
|
||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||
import SchedulePromptableFields from './SchedulePromptableFields';
|
||||
|
||||
const generateRunOnTheDay = (days = []) => {
|
||||
if (
|
||||
@ -179,8 +193,12 @@ function ScheduleForm({
|
||||
i18n,
|
||||
schedule,
|
||||
submitError,
|
||||
resource,
|
||||
...rest
|
||||
}) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
|
||||
let rruleError;
|
||||
const now = new Date();
|
||||
const closestQuarterHour = new Date(
|
||||
@ -189,6 +207,123 @@ function ScheduleForm({
|
||||
const tomorrow = new Date(closestQuarterHour);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
const isTemplate =
|
||||
resource.type === 'workflow_job_template' ||
|
||||
resource.type === 'job_template';
|
||||
const isWorkflowJobTemplate =
|
||||
isTemplate && resource.type === 'workflow_job_template';
|
||||
|
||||
const {
|
||||
request: loadScheduleData,
|
||||
error: contentError,
|
||||
contentLoading,
|
||||
result: { zoneOptions, surveyConfig, launchConfig, credentials },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const readLaunch =
|
||||
isTemplate &&
|
||||
(isWorkflowJobTemplate
|
||||
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||
: JobTemplatesAPI.readLaunch(resource.id));
|
||||
const [{ data }, { data: launchConfiguration }] = await Promise.all([
|
||||
SchedulesAPI.readZoneInfo(),
|
||||
readLaunch,
|
||||
]);
|
||||
|
||||
const readSurvey = isWorkflowJobTemplate
|
||||
? WorkflowJobTemplatesAPI.readSurvey(resource.id)
|
||||
: JobTemplatesAPI.readSurvey(resource.id);
|
||||
|
||||
let surveyConfiguration = null;
|
||||
|
||||
if (isTemplate && launchConfiguration.survey_enabled) {
|
||||
const { data: survey } = await readSurvey;
|
||||
|
||||
surveyConfiguration = survey;
|
||||
}
|
||||
let creds;
|
||||
if (schedule.id) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await SchedulesAPI.readCredentials(schedule.id);
|
||||
creds = results;
|
||||
}
|
||||
|
||||
const missingRequiredInventory = Boolean(
|
||||
!resource.inventory && !schedule?.summary_fields?.inventory.id
|
||||
);
|
||||
let missingRequiredSurvey = false;
|
||||
|
||||
if (
|
||||
schedule.id &&
|
||||
isTemplate &&
|
||||
!launchConfiguration?.can_start_without_user_input
|
||||
) {
|
||||
missingRequiredSurvey = surveyConfiguration?.spec?.every(question => {
|
||||
let hasValue;
|
||||
if (Object.keys(schedule)?.length === 0) {
|
||||
hasValue = true;
|
||||
}
|
||||
Object.entries(schedule?.extra_data).forEach(([key, value]) => {
|
||||
if (
|
||||
question.required &&
|
||||
question.variable === key &&
|
||||
value.length > 0
|
||||
) {
|
||||
hasValue = false;
|
||||
}
|
||||
});
|
||||
return hasValue;
|
||||
});
|
||||
}
|
||||
if (missingRequiredInventory || missingRequiredSurvey) {
|
||||
setIsSaveDisabled(true);
|
||||
}
|
||||
|
||||
const zones = data.map(zone => {
|
||||
return {
|
||||
value: zone.name,
|
||||
key: zone.name,
|
||||
label: zone.name,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
zoneOptions: zones,
|
||||
surveyConfig: surveyConfiguration || {},
|
||||
launchConfig: launchConfiguration,
|
||||
credentials: creds || [],
|
||||
};
|
||||
}, [isTemplate, isWorkflowJobTemplate, resource, schedule]),
|
||||
{
|
||||
zonesOptions: [],
|
||||
surveyConfig: {},
|
||||
launchConfig: {},
|
||||
credentials: [],
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadScheduleData();
|
||||
}, [loadScheduleData]);
|
||||
|
||||
let showPromptButton = false;
|
||||
|
||||
if (
|
||||
launchConfig &&
|
||||
(launchConfig.ask_inventory_on_launch ||
|
||||
launchConfig.ask_variables_on_launch ||
|
||||
launchConfig.ask_job_type_on_launch ||
|
||||
launchConfig.ask_limit_on_launch ||
|
||||
launchConfig.ask_credential_on_launch ||
|
||||
launchConfig.ask_scm_branch_on_launch ||
|
||||
launchConfig.survey_enabled ||
|
||||
launchConfig.inventory_needed_to_start ||
|
||||
launchConfig.variables_needed_to_start?.length > 0)
|
||||
) {
|
||||
showPromptButton = true;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
daysOfWeek: [],
|
||||
description: schedule.description || '',
|
||||
@ -207,6 +342,19 @@ function ScheduleForm({
|
||||
startDateTime: dateToInputDateTime(closestQuarterHour),
|
||||
timezone: schedule.timezone || 'America/New_York',
|
||||
};
|
||||
const submitSchedule = (
|
||||
values,
|
||||
launchConfiguration,
|
||||
surveyConfiguration,
|
||||
scheduleCredentials
|
||||
) => {
|
||||
handleSubmit(
|
||||
values,
|
||||
launchConfiguration,
|
||||
surveyConfiguration,
|
||||
scheduleCredentials
|
||||
);
|
||||
};
|
||||
|
||||
const overriddenValues = {};
|
||||
|
||||
@ -297,28 +445,6 @@ function ScheduleForm({
|
||||
}
|
||||
}
|
||||
|
||||
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 || rruleError) {
|
||||
return <ContentError error={contentError || rruleError} />;
|
||||
}
|
||||
@ -333,7 +459,9 @@ function ScheduleForm({
|
||||
return (
|
||||
<Formik
|
||||
initialValues={Object.assign(initialValues, overriddenValues)}
|
||||
onSubmit={handleSubmit}
|
||||
onSubmit={values => {
|
||||
submitSchedule(values, launchConfig, surveyConfig, credentials);
|
||||
}}
|
||||
validate={values => {
|
||||
const errors = {};
|
||||
const {
|
||||
@ -375,11 +503,55 @@ function ScheduleForm({
|
||||
zoneOptions={zoneOptions}
|
||||
{...rest}
|
||||
/>
|
||||
{isWizardOpen && (
|
||||
<SchedulePromptableFields
|
||||
schedule={schedule}
|
||||
credentials={credentials}
|
||||
surveyConfig={surveyConfig}
|
||||
launchConfig={launchConfig}
|
||||
resource={resource}
|
||||
onCloseWizard={hasErrors => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(hasErrors);
|
||||
}}
|
||||
onSave={() => {
|
||||
setIsWizardOpen(false);
|
||||
setIsSaveDisabled(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FormSubmitError error={submitError} />
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
<FormFullWidthLayout>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
aria-label={i18n._(t`Save`)}
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={formik.handleSubmit}
|
||||
isDisabled={isSaveDisabled}
|
||||
>
|
||||
{i18n._(t`Save`)}
|
||||
</Button>
|
||||
{isTemplate && showPromptButton && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
aria-label={i18n._(t`Prompt`)}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{i18n._(t`Prompt`)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
aria-label={i18n._(t`Cancel`)}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormFullWidthLayout>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@ -1,11 +1,71 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api';
|
||||
import ScheduleForm from './ScheduleForm';
|
||||
|
||||
jest.mock('../../../api/models/Schedules');
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/Inventories');
|
||||
|
||||
const survey = {
|
||||
name: '',
|
||||
description: '',
|
||||
spec: [
|
||||
{
|
||||
question_name: 'new survey',
|
||||
question_description: '',
|
||||
required: true,
|
||||
type: 'text',
|
||||
variable: 'newsurveyquestion',
|
||||
min: 0,
|
||||
max: 1024,
|
||||
default: '',
|
||||
choices: '',
|
||||
new_question: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
const credentials = {
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||
{ id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' },
|
||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const launchData = {
|
||||
data: {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: true,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockSchedule = {
|
||||
rrule:
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||
@ -23,7 +83,7 @@ const mockSchedule = {
|
||||
name: 'mock schedule',
|
||||
description: 'test description',
|
||||
extra_data: {},
|
||||
inventory: null,
|
||||
inventory: 1,
|
||||
scm_branch: null,
|
||||
job_type: null,
|
||||
job_tags: null,
|
||||
@ -82,7 +142,11 @@ describe('<ScheduleForm />', () => {
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -92,6 +156,10 @@ describe('<ScheduleForm />', () => {
|
||||
describe('Cancel', () => {
|
||||
test('should make the appropriate callback', async () => {
|
||||
const handleCancel = jest.fn();
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue(launchData);
|
||||
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue(survey);
|
||||
SchedulesAPI.readCredentials.mockResolvedValue(credentials);
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
@ -101,7 +169,11 @@ describe('<ScheduleForm />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={handleCancel} />
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={handleCancel}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
@ -111,6 +183,173 @@ describe('<ScheduleForm />', () => {
|
||||
expect(handleCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe('Prompted Schedule', () => {
|
||||
let promptWrapper;
|
||||
beforeEach(async () => {
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: false,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_credential_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: true,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
name: 'America/New_York',
|
||||
},
|
||||
],
|
||||
});
|
||||
await act(async () => {
|
||||
promptWrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{
|
||||
id: 23,
|
||||
type: 'job_template',
|
||||
inventory: 1,
|
||||
summary_fields: {
|
||||
credentials: [],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(
|
||||
promptWrapper,
|
||||
'Button[aria-label="Prompt"]',
|
||||
el => el.length > 0
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
promptWrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should open prompt modal with proper steps and default values', async () => {
|
||||
await act(async () =>
|
||||
promptWrapper.find('Button[aria-label="Prompt"]').prop('onClick')()
|
||||
);
|
||||
promptWrapper.update();
|
||||
waitForElement(promptWrapper, 'Wizard', el => el.length > 0);
|
||||
expect(promptWrapper.find('Wizard').length).toBe(1);
|
||||
expect(promptWrapper.find('StepName#inventory-step').length).toBe(2);
|
||||
expect(promptWrapper.find('StepName#preview-step').length).toBe(1);
|
||||
expect(promptWrapper.find('WizardNavItem').length).toBe(2);
|
||||
});
|
||||
|
||||
test('should update prompt modal data', async () => {
|
||||
InventoriesAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
count: 2,
|
||||
results: [
|
||||
{
|
||||
name: 'Foo',
|
||||
id: 1,
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
name: 'Bar',
|
||||
id: 2,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readOptions.mockResolvedValue({
|
||||
data: {
|
||||
related_search_fields: [],
|
||||
actions: {
|
||||
GET: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
promptWrapper.find('Button[aria-label="Prompt"]').prop('onClick')()
|
||||
);
|
||||
promptWrapper.update();
|
||||
expect(
|
||||
promptWrapper
|
||||
.find('WizardNavItem')
|
||||
.at(0)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
await act(async () => {
|
||||
promptWrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
promptWrapper.update();
|
||||
expect(
|
||||
promptWrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.prop('checked')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
promptWrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
promptWrapper.update();
|
||||
expect(
|
||||
promptWrapper
|
||||
.find('WizardNavItem')
|
||||
.at(1)
|
||||
.prop('isCurrent')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
promptWrapper.find('WizardFooterInternal').prop('onNext')()
|
||||
);
|
||||
promptWrapper.update();
|
||||
expect(promptWrapper.find('Wizard').length).toBe(0);
|
||||
});
|
||||
test('should render prompt button with disabled save button', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{
|
||||
id: 23,
|
||||
type: 'job_template',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(
|
||||
wrapper,
|
||||
'Button[aria-label="Prompt"]',
|
||||
el => el.length > 0
|
||||
);
|
||||
expect(wrapper.find('Button[aria-label="Save"]').prop('isDisabled')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('Add', () => {
|
||||
beforeAll(async () => {
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
@ -120,9 +359,39 @@ describe('<ScheduleForm />', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: true,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: false,
|
||||
ask_credential_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: false,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{ id: 23, type: 'job_template', inventory: 1 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -312,6 +581,14 @@ describe('<ScheduleForm />', () => {
|
||||
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1);
|
||||
});
|
||||
test('occurrences field properly shown when end after selection is made', async () => {
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('FormGroup[label="Run frequency"] FormSelect')
|
||||
.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('Radio#end-after').invoke('onChange')('after', {
|
||||
target: { name: 'end' },
|
||||
@ -331,6 +608,14 @@ describe('<ScheduleForm />', () => {
|
||||
wrapper.update();
|
||||
});
|
||||
test('error shown when end date/time comes before start date/time', async () => {
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('FormGroup[label="Run frequency"] FormSelect')
|
||||
.invoke('onChange')('minute', {
|
||||
target: { value: 'minute', key: 'minute', label: 'Minute' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
|
||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||
@ -361,13 +646,28 @@ describe('<ScheduleForm />', () => {
|
||||
);
|
||||
});
|
||||
test('error shown when on day number is not between 1 and 31', async () => {
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
wrapper.find('select[id="schedule-frequency"]').invoke('onChange')(
|
||||
{
|
||||
currentTarget: { value: 'month', type: 'change' },
|
||||
target: { name: 'frequency', value: 'month' },
|
||||
},
|
||||
'month'
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper.find('input#schedule-run-on-day-number').simulate('change', {
|
||||
target: { value: 32, name: 'runOnDayNumber' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||
).toBe(32);
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
@ -379,7 +679,7 @@ describe('<ScheduleForm />', () => {
|
||||
});
|
||||
});
|
||||
describe('Edit', () => {
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
@ -387,10 +687,94 @@ describe('<ScheduleForm />', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: true,
|
||||
passwords_needed_to_start: [],
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
ask_inventory_on_launch: false,
|
||||
ask_credential_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
credential_needed_to_start: false,
|
||||
inventory_needed_to_start: false,
|
||||
job_template_data: {
|
||||
name: 'Demo Job Template',
|
||||
id: 7,
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readSurvey.mockResolvedValue({});
|
||||
SchedulesAPI.readCredentials.mockResolvedValue(credentials);
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should make API calls to fetch credentials, launch configuration, and survey configuration', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={{ inventory: null, ...mockSchedule }}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(JobTemplatesAPI.readLaunch).toBeCalledWith(23);
|
||||
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(23);
|
||||
expect(SchedulesAPI.readCredentials).toBeCalledWith(27);
|
||||
});
|
||||
|
||||
test('should not call API to get credentials ', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(SchedulesAPI.readCredentials).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('should render prompt button with enabled save button for project', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ScheduleForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
resource={{
|
||||
id: 23,
|
||||
type: 'project',
|
||||
inventory: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(
|
||||
wrapper,
|
||||
'Button[aria-label="Prompt"]',
|
||||
el => el.length > 0
|
||||
);
|
||||
|
||||
expect(wrapper.find('Button[aria-label="Save"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('initially renders expected fields and values with existing schedule that runs once', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@ -398,6 +782,7 @@ describe('<ScheduleForm />', () => {
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
schedule={mockSchedule}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -426,6 +811,7 @@ describe('<ScheduleForm />', () => {
|
||||
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=10;FREQ=MINUTELY',
|
||||
dtend: null,
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -459,6 +845,7 @@ describe('<ScheduleForm />', () => {
|
||||
dtend: '2020-04-03T03:45:00Z',
|
||||
until: '',
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -493,6 +880,7 @@ describe('<ScheduleForm />', () => {
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
@ -526,6 +914,7 @@ describe('<ScheduleForm />', () => {
|
||||
dtend: '2020-10-30T18:45:00Z',
|
||||
until: '2021-01-01T00:00:00',
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -583,6 +972,7 @@ describe('<ScheduleForm />', () => {
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
@ -628,6 +1018,7 @@ describe('<ScheduleForm />', () => {
|
||||
dtend: null,
|
||||
until: '',
|
||||
})}
|
||||
resource={{ id: 23, type: 'job_template' }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
import AlertModal from '../../AlertModal';
|
||||
import { useDismissableError } from '../../../util/useRequest';
|
||||
import ContentError from '../../ContentError';
|
||||
import ContentLoading from '../../ContentLoading';
|
||||
import useSchedulePromptSteps from './useSchedulePromptSteps';
|
||||
|
||||
function SchedulePromptableFields({
|
||||
schedule,
|
||||
surveyConfig,
|
||||
launchConfig,
|
||||
onCloseWizard,
|
||||
onSave,
|
||||
credentials,
|
||||
resource,
|
||||
i18n,
|
||||
}) {
|
||||
const {
|
||||
validateForm,
|
||||
setFieldTouched,
|
||||
values,
|
||||
initialValues,
|
||||
resetForm,
|
||||
} = useFormikContext();
|
||||
const {
|
||||
steps,
|
||||
visitStep,
|
||||
visitAllSteps,
|
||||
validateStep,
|
||||
contentError,
|
||||
isReady,
|
||||
} = useSchedulePromptSteps(
|
||||
surveyConfig,
|
||||
launchConfig,
|
||||
schedule,
|
||||
resource,
|
||||
i18n,
|
||||
credentials
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(contentError);
|
||||
const cancelPromptableValues = async () => {
|
||||
const hasErrors = await validateForm();
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
daysOfWeek: values.daysOfWeek,
|
||||
description: values.description,
|
||||
end: values.end,
|
||||
endDateTime: values.endDateTime,
|
||||
frequency: values.frequency,
|
||||
interval: values.interval,
|
||||
name: values.name,
|
||||
occurences: values.occurances,
|
||||
runOn: values.runOn,
|
||||
runOnDayMonth: values.runOnDayMonth,
|
||||
runOnDayNumber: values.runOnDayNumber,
|
||||
runOnTheDay: values.runOnTheDay,
|
||||
runOnTheMonth: values.runOnTheMonth,
|
||||
runOnTheOccurence: values.runOnTheOccurance,
|
||||
startDateTime: values.startDateTime,
|
||||
timezone: values.timezone,
|
||||
},
|
||||
});
|
||||
onCloseWizard(Object.keys(hasErrors).length > 0);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
onCloseWizard();
|
||||
}}
|
||||
>
|
||||
<ContentError error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Wizard
|
||||
isOpen
|
||||
onClose={cancelPromptableValues}
|
||||
onSave={onSave}
|
||||
onNext={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
onGoToStep={async (nextStep, prevStep) => {
|
||||
if (nextStep.id === 'preview') {
|
||||
visitAllSteps(setFieldTouched);
|
||||
} else {
|
||||
visitStep(prevStep.prevId, setFieldTouched);
|
||||
validateStep(nextStep.id);
|
||||
}
|
||||
await validateForm();
|
||||
}}
|
||||
title={i18n._(t`Prompts`)}
|
||||
steps={
|
||||
isReady
|
||||
? steps
|
||||
: [
|
||||
{
|
||||
name: i18n._(t`Content Loading`),
|
||||
component: <ContentLoading />,
|
||||
},
|
||||
]
|
||||
}
|
||||
backButtonText={i18n._(t`Back`)}
|
||||
cancelButtonText={i18n._(t`Cancel`)}
|
||||
nextButtonText={i18n._(t`Next`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(SchedulePromptableFields);
|
||||
@ -0,0 +1,102 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { t } from '@lingui/macro';
|
||||
import useInventoryStep from '../../LaunchPrompt/steps/useInventoryStep';
|
||||
import useCredentialsStep from '../../LaunchPrompt/steps/useCredentialsStep';
|
||||
import useOtherPromptsStep from '../../LaunchPrompt/steps/useOtherPromptsStep';
|
||||
import useSurveyStep from '../../LaunchPrompt/steps/useSurveyStep';
|
||||
import usePreviewStep from '../../LaunchPrompt/steps/usePreviewStep';
|
||||
|
||||
export default function useSchedulePromptSteps(
|
||||
surveyConfig,
|
||||
launchConfig,
|
||||
schedule,
|
||||
resource,
|
||||
i18n,
|
||||
scheduleCredentials
|
||||
) {
|
||||
const {
|
||||
summary_fields: { credentials: resourceCredentials },
|
||||
} = resource;
|
||||
const sourceOfValues =
|
||||
(Object.keys(schedule).length > 0 && schedule) || resource;
|
||||
|
||||
sourceOfValues.summary_fields = {
|
||||
credentials: [...resourceCredentials, ...scheduleCredentials],
|
||||
...sourceOfValues.summary_fields,
|
||||
};
|
||||
const { resetForm, values } = useFormikContext();
|
||||
const [visited, setVisited] = useState({});
|
||||
|
||||
const steps = [
|
||||
useInventoryStep(launchConfig, sourceOfValues, i18n, visited),
|
||||
useCredentialsStep(launchConfig, sourceOfValues, i18n),
|
||||
useOtherPromptsStep(launchConfig, sourceOfValues, i18n),
|
||||
useSurveyStep(launchConfig, surveyConfig, sourceOfValues, i18n, visited),
|
||||
];
|
||||
|
||||
const hasErrors = steps.some(step => step.hasError);
|
||||
steps.push(
|
||||
usePreviewStep(
|
||||
launchConfig,
|
||||
i18n,
|
||||
resource,
|
||||
surveyConfig,
|
||||
hasErrors,
|
||||
true,
|
||||
i18n._(t`Save`)
|
||||
)
|
||||
);
|
||||
|
||||
const pfSteps = steps.map(s => s.step).filter(s => s != null);
|
||||
const isReady = !steps.some(s => !s.isReady);
|
||||
|
||||
useEffect(() => {
|
||||
let initialValues = {};
|
||||
if (launchConfig && surveyConfig && isReady) {
|
||||
initialValues = steps.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
...cur.initialValues,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
resetForm({
|
||||
values: {
|
||||
...initialValues,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [launchConfig, surveyConfig, isReady]);
|
||||
|
||||
const stepWithError = steps.find(s => s.contentError);
|
||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||
|
||||
return {
|
||||
isReady,
|
||||
validateStep: stepId => {
|
||||
steps.find(s => s?.step?.id === stepId).validate();
|
||||
},
|
||||
steps: pfSteps,
|
||||
visitStep: (prevStepId, setFieldTouched) => {
|
||||
setVisited({
|
||||
...visited,
|
||||
[prevStepId]: true,
|
||||
});
|
||||
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||
},
|
||||
visitAllSteps: setFieldTouched => {
|
||||
setVisited({
|
||||
inventory: true,
|
||||
credentials: true,
|
||||
other: true,
|
||||
survey: true,
|
||||
preview: true,
|
||||
});
|
||||
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||
},
|
||||
contentError,
|
||||
};
|
||||
}
|
||||
@ -326,6 +326,11 @@ function JobDetail({ job, i18n }) {
|
||||
user={created_by}
|
||||
/>
|
||||
<UserDateDetail label={i18n._(t`Last Modified`)} date={job.modified} />
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Explanation`)}
|
||||
value={job.job_explanation}
|
||||
/>
|
||||
</DetailList>
|
||||
{job.extra_vars && (
|
||||
<VariablesInput
|
||||
|
||||
@ -40,6 +40,7 @@ describe('<JobDetail />', () => {
|
||||
name: 'Test Source Workflow',
|
||||
},
|
||||
},
|
||||
job_explanation: 'It failed, bummer!',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -69,6 +70,7 @@ describe('<JobDetail />', () => {
|
||||
assertDetail('Job Slice', '0/1');
|
||||
assertDetail('Credentials', 'SSH: Demo Credential');
|
||||
assertDetail('Machine Credential', 'SSH: Machine cred');
|
||||
assertDetail('Explanation', 'It failed, bummer!');
|
||||
|
||||
const credentialChip = wrapper.find(
|
||||
`Detail[label="Credentials"] CredentialChip`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user