Refactors to add warning icon and disable save if schedule has missing values

This commit is contained in:
Alex Corey
2021-02-15 13:44:15 -05:00
parent c608d761a2
commit 561390d405
23 changed files with 704 additions and 310 deletions

View File

@@ -19,7 +19,13 @@ import ScheduleEdit from './ScheduleEdit';
import { SchedulesAPI } from '../../api'; import { SchedulesAPI } from '../../api';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
function Schedule({ i18n, setBreadcrumb, resource }) { function Schedule({
i18n,
setBreadcrumb,
resource,
launchConfig,
surveyConfig,
}) {
const { scheduleId } = useParams(); const { scheduleId } = useParams();
const { pathname } = useLocation(); const { pathname } = useLocation();
@@ -100,13 +106,18 @@ function Schedule({ i18n, setBreadcrumb, resource }) {
/> />
{schedule && [ {schedule && [
<Route key="edit" path={`${pathRoot}schedules/:id/edit`}> <Route key="edit" path={`${pathRoot}schedules/:id/edit`}>
<ScheduleEdit schedule={schedule} resource={resource} /> <ScheduleEdit
schedule={schedule}
resource={resource}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
/>
</Route>, </Route>,
<Route <Route
key="details" key="details"
path={`${pathRoot}schedules/:scheduleId/details`} path={`${pathRoot}schedules/:scheduleId/details`}
> >
<ScheduleDetail schedule={schedule} /> <ScheduleDetail schedule={schedule} surveyConfig={surveyConfig} />
</Route>, </Route>,
]} ]}
<Route key="not-found" path="*"> <Route key="not-found" path="*">

View File

@@ -15,14 +15,18 @@ import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
import getSurveyValues from '../../../util/prompt/getSurveyValues'; import getSurveyValues from '../../../util/prompt/getSurveyValues';
import { getAddedAndRemoved } from '../../../util/lists'; import { getAddedAndRemoved } from '../../../util/lists';
function ScheduleAdd({ i18n, resource, apiModel }) { function ScheduleAdd({ i18n, resource, apiModel, launchConfig, surveyConfig }) {
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { pathname } = location; const { pathname } = location;
const pathRoot = pathname.substr(0, pathname.indexOf('schedules')); const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
const handleSubmit = async (values, launchConfig, surveyConfig) => { const handleSubmit = async (
values,
launchConfiguration,
surveyConfiguration
) => {
const { const {
inventory, inventory,
extra_vars, extra_vars,
@@ -51,8 +55,9 @@ function ScheduleAdd({ i18n, resource, apiModel }) {
let extraVars; let extraVars;
const surveyValues = getSurveyValues(values); const surveyValues = getSurveyValues(values);
const initialExtraVars = const initialExtraVars =
launchConfig?.ask_variables_on_launch && (values.extra_vars || '---'); launchConfiguration?.ask_variables_on_launch &&
if (surveyConfig?.spec) { (values.extra_vars || '---');
if (surveyConfiguration?.spec) {
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues)); extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues));
} else { } else {
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {})); extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
@@ -92,6 +97,8 @@ function ScheduleAdd({ i18n, resource, apiModel }) {
handleCancel={() => history.push(`${pathRoot}schedules`)} handleCancel={() => history.push(`${pathRoot}schedules`)}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
submitError={formSubmitError} submitError={formSubmitError}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
resource={resource} resource={resource}
/> />
</CardBody> </CardBody>

View File

@@ -19,45 +19,45 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
}, },
], ],
}); });
JobTemplatesAPI.readLaunch.mockResolvedValue({
data: { const launchConfig = {
can_start_without_user_input: false, can_start_without_user_input: false,
passwords_needed_to_start: [], passwords_needed_to_start: [],
ask_scm_branch_on_launch: false, ask_scm_branch_on_launch: false,
ask_variables_on_launch: false, ask_variables_on_launch: false,
ask_tags_on_launch: false, ask_tags_on_launch: false,
ask_diff_mode_on_launch: false, ask_diff_mode_on_launch: false,
ask_skip_tags_on_launch: false, ask_skip_tags_on_launch: false,
ask_job_type_on_launch: false, ask_job_type_on_launch: false,
ask_limit_on_launch: false, ask_limit_on_launch: false,
ask_verbosity_on_launch: false, ask_verbosity_on_launch: false,
ask_inventory_on_launch: true, ask_inventory_on_launch: true,
ask_credential_on_launch: false, ask_credential_on_launch: false,
survey_enabled: false, survey_enabled: false,
variables_needed_to_start: [], variables_needed_to_start: [],
credential_needed_to_start: false, credential_needed_to_start: false,
inventory_needed_to_start: true, inventory_needed_to_start: true,
job_template_data: { job_template_data: {
name: 'Demo Job Template', name: 'Demo Job Template',
id: 7, id: 7,
description: '', description: '',
},
defaults: {
extra_vars: '---',
diff_mode: false,
limit: '',
job_tags: '',
skip_tags: '',
job_type: 'run',
verbosity: 0,
inventory: {
name: null,
id: null,
},
scm_branch: '',
},
}, },
}); 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 } }); JobTemplatesAPI.createSchedule.mockResolvedValue({ data: { id: 3 } });
let wrapper; let wrapper;
@@ -74,6 +74,7 @@ describe('<ScheduleAdd />', () => {
inventory: 2, inventory: 2,
summary_fields: { credentials: [] }, summary_fields: { credentials: [] },
}} }}
launchConfig={launchConfig}
/> />
); );
}); });
@@ -377,7 +378,6 @@ describe('<ScheduleAdd />', () => {
); );
wrapper.update(); wrapper.update();
expect(wrapper.find('Wizard').length).toBe(0); expect(wrapper.find('Wizard').length).toBe(0);
// console.log(wrapper.debug());
await act(async () => { await act(async () => {
wrapper.find('Formik').invoke('onSubmit')({ wrapper.find('Formik').invoke('onSubmit')({
name: 'Schedule', name: 'Schedule',

View File

@@ -42,7 +42,7 @@ const PromptDetailList = styled(DetailList)`
padding: 0px 20px; padding: 0px 20px;
`; `;
function ScheduleDetail({ schedule, i18n }) { function ScheduleDetail({ schedule, i18n, surveyConfig }) {
const { const {
id, id,
created, created,
@@ -148,6 +148,7 @@ function ScheduleDetail({ schedule, i18n }) {
const { const {
ask_credential_on_launch, ask_credential_on_launch,
inventory_needed_to_start,
ask_diff_mode_on_launch, ask_diff_mode_on_launch,
ask_inventory_on_launch, ask_inventory_on_launch,
ask_job_type_on_launch, ask_job_type_on_launch,
@@ -160,6 +161,41 @@ function ScheduleDetail({ schedule, i18n }) {
survey_enabled, survey_enabled,
} = launchData || {}; } = launchData || {};
const missingRequiredInventory = () => {
if (!inventory_needed_to_start || schedule?.summary_fields?.inventory?.id) {
return false;
}
return true;
};
const hasMissingSurveyValue = () => {
let missingValues = false;
if (survey_enabled) {
surveyConfig.spec.forEach(question => {
const hasDefaultValue = Boolean(question.default);
if (question.required && !hasDefaultValue) {
const extraDataKeys = Object.keys(schedule?.extra_data);
const hasMatchingKey = extraDataKeys.includes(question.variable);
Object.values(schedule?.extra_data).forEach(value => {
if (!value || !hasMatchingKey) {
missingValues = true;
} else {
missingValues = false;
}
});
if (!Object.values(schedule.extra_data).length) {
missingValues = true;
}
}
});
}
return missingValues;
};
const isDisabled = Boolean(
missingRequiredInventory() || hasMissingSurveyValue()
);
const showCredentialsDetail = const showCredentialsDetail =
ask_credential_on_launch && credentials.length > 0; ask_credential_on_launch && credentials.length > 0;
const showInventoryDetail = ask_inventory_on_launch && inventory; const showInventoryDetail = ask_inventory_on_launch && inventory;
@@ -189,14 +225,6 @@ function ScheduleDetail({ schedule, i18n }) {
showVerbosityDetail || showVerbosityDetail ||
showVariablesDetail; showVariablesDetail;
const VERBOSITY = {
0: i18n._(t`0 (Normal)`),
1: i18n._(t`1 (Verbose)`),
2: i18n._(t`2 (More Verbose)`),
3: i18n._(t`3 (Debug)`),
4: i18n._(t`4 (Connection Debug)`),
};
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
} }
@@ -207,7 +235,11 @@ function ScheduleDetail({ schedule, i18n }) {
return ( return (
<CardBody> <CardBody>
<ScheduleToggle schedule={schedule} css="padding-bottom: 40px" /> <ScheduleToggle
schedule={schedule}
css="padding-bottom: 40px"
isDisabled={isDisabled}
/>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail label={i18n._(t`Name`)} value={name} /> <Detail label={i18n._(t`Name`)} value={name} />
<Detail label={i18n._(t`Description`)} value={description} /> <Detail label={i18n._(t`Description`)} value={description} />
@@ -279,12 +311,6 @@ function ScheduleDetail({ schedule, i18n }) {
{ask_limit_on_launch && ( {ask_limit_on_launch && (
<Detail label={i18n._(t`Limit`)} value={limit} /> <Detail label={i18n._(t`Limit`)} value={limit} />
)} )}
{ask_verbosity_on_launch && (
<Detail
label={i18n._(t`Verbosity`)}
value={VERBOSITY[verbosity]}
/>
)}
{showDiffModeDetail && ( {showDiffModeDetail && (
<Detail <Detail
label={i18n._(t`Show Changes`)} label={i18n._(t`Show Changes`)}

View File

@@ -26,6 +26,7 @@ const allPrompts = {
ask_variables_on_launch: true, ask_variables_on_launch: true,
ask_verbosity_on_launch: true, ask_verbosity_on_launch: true,
survey_enabled: true, survey_enabled: true,
inventory_needed_to_start: true,
}, },
}; };
@@ -489,4 +490,39 @@ describe('<ScheduleDetail />', () => {
); );
expect(SchedulesAPI.destroy).toHaveBeenCalledTimes(1); expect(SchedulesAPI.destroy).toHaveBeenCalledTimes(1);
}); });
test('should have disabled toggle', async () => {
SchedulesAPI.readCredentials.mockResolvedValueOnce({
data: {
count: 0,
results: [],
},
});
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts);
await act(async () => {
wrapper = mountWithContexts(
<Route
path="/templates/job_template/:id/schedules/:scheduleId"
component={() => (
<ScheduleDetail schedule={schedule} surveyConfig={{ spec: [] }} />
)}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match: { params: { id: 1 } },
},
},
},
}
);
});
await waitForElement(
wrapper,
'ScheduleToggle',
el => el.prop('isDisabled') === true
);
});
}); });

View File

@@ -15,7 +15,13 @@ import { parseVariableField } from '../../../util/yaml';
import mergeExtraVars from '../../../util/prompt/mergeExtraVars'; import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
import getSurveyValues from '../../../util/prompt/getSurveyValues'; import getSurveyValues from '../../../util/prompt/getSurveyValues';
function ScheduleEdit({ i18n, schedule, resource }) { function ScheduleEdit({
i18n,
schedule,
resource,
launchConfig,
surveyConfig,
}) {
const [formSubmitError, setFormSubmitError] = useState(null); const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
@@ -24,8 +30,8 @@ function ScheduleEdit({ i18n, schedule, resource }) {
const handleSubmit = async ( const handleSubmit = async (
values, values,
launchConfig, launchConfiguration,
surveyConfig, surveyConfiguration,
scheduleCredentials = [] scheduleCredentials = []
) => { ) => {
const { const {
@@ -36,32 +42,40 @@ function ScheduleEdit({ i18n, schedule, resource }) {
interval, interval,
startDateTime, startDateTime,
timezone, timezone,
occurrences, occurences,
runOn, runOn,
runOnTheDay, runOnTheDay,
runOnTheMonth, runOnTheMonth,
runOnDayMonth, runOnDayMonth,
runOnDayNumber, runOnDayNumber,
endDateTime, endDateTime,
runOnTheOccurrence, runOnTheOccurence,
daysOfWeek, daysOfWeek,
...submitValues ...submitValues
} = values; } = values;
const { added, removed } = getAddedAndRemoved( const { added, removed } = getAddedAndRemoved(
[...resource?.summary_fields.credentials, ...scheduleCredentials], [...(resource?.summary_fields.credentials || []), ...scheduleCredentials],
credentials credentials
); );
let extraVars; let extraVars;
const surveyValues = getSurveyValues(values); const surveyValues = getSurveyValues(values);
const initialExtraVars = const initialExtraVars =
launchConfig?.ask_variables_on_launch && (values.extra_vars || '---'); launchConfiguration?.ask_variables_on_launch &&
if (surveyConfig?.spec) { (values.extra_vars || '---');
if (surveyConfiguration?.spec) {
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues)); extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues));
} else { } else {
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {})); extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
} }
submitValues.extra_data = extraVars && parseVariableField(extraVars); submitValues.extra_data = extraVars && parseVariableField(extraVars);
if (
Object.keys(submitValues.extra_data).length === 0 &&
Object.keys(schedule.extra_data).length > 0
) {
submitValues.extra_data = schedule.extra_data;
}
delete values.extra_vars; delete values.extra_vars;
if (inventory) { if (inventory) {
submitValues.inventory = inventory.id; submitValues.inventory = inventory.id;
@@ -103,6 +117,8 @@ function ScheduleEdit({ i18n, schedule, resource }) {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
submitError={formSubmitError} submitError={formSubmitError}
resource={resource} resource={resource}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
/> />
</CardBody> </CardBody>
</Card> </Card>

View File

@@ -7,7 +7,6 @@ import {
} from '../../../../testUtils/enzymeHelpers'; } from '../../../../testUtils/enzymeHelpers';
import { import {
SchedulesAPI, SchedulesAPI,
JobTemplatesAPI,
InventoriesAPI, InventoriesAPI,
CredentialsAPI, CredentialsAPI,
CredentialTypesAPI, CredentialTypesAPI,
@@ -28,46 +27,6 @@ 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({ SchedulesAPI.readCredentials.mockResolvedValue({
data: { data: {
results: [ results: [
@@ -156,6 +115,44 @@ describe('<ScheduleEdit />', () => {
], ],
}, },
}} }}
launchConfig={{
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: '',
},
}}
surveyConfig={{}}
/> />
); );
}); });
@@ -202,6 +199,7 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Run every 10 minutes 10 times', name: 'Run every 10 minutes 10 times',
extra_data: {}, extra_data: {},
occurrences: 10,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10', 'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10',
}); });
@@ -265,6 +263,7 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Run weekly on mon/wed/fri', name: 'Run weekly on mon/wed/fri',
extra_data: {}, extra_data: {},
occurrences: 1,
rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`, rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`,
}); });
}); });
@@ -287,6 +286,7 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Run on the first day of the month', name: 'Run on the first day of the month',
extra_data: {}, extra_data: {},
occurrences: 1,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1', 'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1',
}); });
@@ -312,6 +312,8 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Run monthly on the last Tuesday', name: 'Run monthly on the last Tuesday',
extra_data: {}, extra_data: {},
occurrences: 1,
runOnTheOccurrence: -1,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU', 'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU',
}); });
@@ -336,6 +338,7 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Yearly on the first day of March', name: 'Yearly on the first day of March',
extra_data: {}, extra_data: {},
occurrences: 1,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1', 'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1',
}); });
@@ -361,6 +364,8 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Yearly on the second Friday in April', name: 'Yearly on the second Friday in April',
extra_data: {}, extra_data: {},
occurrences: 1,
runOnTheOccurrence: 2,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4', 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4',
}); });
@@ -386,6 +391,8 @@ describe('<ScheduleEdit />', () => {
description: 'test description', description: 'test description',
name: 'Yearly on the first weekday in October', name: 'Yearly on the first weekday in October',
extra_data: {}, extra_data: {},
occurrences: 1,
runOnTheOccurrence: 1,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10', 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
}); });
@@ -515,6 +522,8 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toBeCalledWith(27, { expect(SchedulesAPI.update).toBeCalledWith(27, {
extra_data: {}, extra_data: {},
name: 'mock schedule', name: 'mock schedule',
occurrences: 1,
runOnTheOccurrence: 1,
rrule: rrule:
'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY', 'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY',
skip_tags: '', skip_tags: '',
@@ -590,8 +599,10 @@ describe('<ScheduleEdit />', () => {
expect(SchedulesAPI.update).toBeCalledWith(27, { expect(SchedulesAPI.update).toBeCalledWith(27, {
description: '', description: '',
extra_data: {}, extra_data: {},
inventory: 702, occurrences: 1,
runOnTheOccurrence: 1,
name: 'foo', name: 'foo',
inventory: 702,
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
}); });

View File

@@ -24,6 +24,9 @@ function ScheduleList({
loadSchedules, loadSchedules,
loadScheduleOptions, loadScheduleOptions,
hideAddButton, hideAddButton,
resource,
launchConfig,
surveyConfig,
}) { }) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
@@ -114,6 +117,47 @@ function ScheduleList({
actions && actions &&
Object.prototype.hasOwnProperty.call(actions, 'POST') && Object.prototype.hasOwnProperty.call(actions, 'POST') &&
!hideAddButton; !hideAddButton;
const isTemplate =
resource?.type === 'workflow_job_template' ||
resource?.type === 'job_template';
const missingRequiredInventory = schedule => {
if (
!launchConfig.inventory_needed_to_start ||
schedule?.summary_fields?.inventory?.id
) {
return null;
}
return i18n._(t`This schedule is missing an Inventory`);
};
const hasMissingSurveyValue = schedule => {
let missingValues;
if (launchConfig.survey_enabled) {
surveyConfig.spec.forEach(question => {
const hasDefaultValue = Boolean(question.default);
if (question.required && !hasDefaultValue) {
const extraDataKeys = Object.keys(schedule?.extra_data);
const hasMatchingKey = extraDataKeys.includes(question.variable);
Object.values(schedule?.extra_data).forEach(value => {
if (!value || !hasMatchingKey) {
missingValues = true;
} else {
missingValues = false;
}
});
if (!Object.values(schedule.extra_data).length) {
missingValues = true;
}
}
});
}
return (
missingValues &&
i18n._(t`This schedule is missing required survey values`)
);
};
return ( return (
<> <>
@@ -139,6 +183,8 @@ function ScheduleList({
onSelect={() => handleSelect(item)} onSelect={() => handleSelect(item)}
schedule={item} schedule={item}
rowIndex={index} rowIndex={index}
isMissingInventory={isTemplate && missingRequiredInventory(item)}
isMissingSurvey={isTemplate && hasMissingSurveyValue(item)}
/> />
)} )}
toolbarSearchColumns={[ toolbarSearchColumns={[

View File

@@ -32,19 +32,22 @@ describe('ScheduleList', () => {
}); });
describe('read call successful', () => { describe('read call successful', () => {
beforeAll(async () => { beforeEach(async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ScheduleList <ScheduleList
loadSchedules={loadSchedules} loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions} loadScheduleOptions={loadScheduleOptions}
resource={{ type: 'job_template', inventory: 1 }}
launchConfig={{ survey_enabled: false }}
surveyConfig={{}}
/> />
); );
}); });
wrapper.update(); wrapper.update();
}); });
afterAll(() => { afterEach(() => {
wrapper.unmount(); wrapper.unmount();
}); });
@@ -203,6 +206,60 @@ describe('ScheduleList', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('ToolbarAddButton').length).toBe(0); expect(wrapper.find('ToolbarAddButton').length).toBe(0);
}); });
test('should show missing resource icon and disabled toggle', async () => {
await act(async () => {
wrapper = mountWithContexts(
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
hideAddButton
resource={{ type: 'job_template', inventory: 1 }}
launchConfig={{ survey_enabled: true }}
surveyConfig={{ spec: [{ required: true, default: null }] }}
/>
);
});
wrapper.update();
expect(
wrapper
.find('ScheduleListItem')
.at(4)
.prop('isMissingSurvey')
).toBe('This schedule is missing required survey values');
expect(wrapper.find('ExclamationTriangleIcon').length).toBe(5);
expect(wrapper.find('Switch#schedule-5-toggle').prop('isDisabled')).toBe(
true
);
});
test('should show missing resource icon and disabled toggle', async () => {
await act(async () => {
wrapper = mountWithContexts(
<ScheduleList
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
hideAddButton
resource={{ type: 'job_template' }}
launchConfig={{
survey_enabled: true,
inventory_needed_to_start: true,
}}
surveyConfig={{ spec: [] }}
/>
);
});
wrapper.update();
expect(
wrapper
.find('ScheduleListItem')
.at(3)
.prop('isMissingInventory')
).toBe('This schedule is missing an Inventory');
expect(wrapper.find('ExclamationTriangleIcon').length).toBe(4);
expect(wrapper.find('Switch#schedule-3-toggle').prop('isDisabled')).toBe(
true
);
});
}); });
describe('read call unsuccessful', () => { describe('read call unsuccessful', () => {

View File

@@ -4,16 +4,33 @@ import { bool, func } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@patternfly/react-core'; import { Button, Tooltip } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table'; import { Tr, Td } from '@patternfly/react-table';
import { PencilAltIcon } from '@patternfly/react-icons'; import {
PencilAltIcon,
ExclamationTriangleIcon as PFExclamationTriangleIcon,
} from '@patternfly/react-icons';
import styled from 'styled-components';
import { DetailList, Detail } from '../../DetailList'; import { DetailList, Detail } from '../../DetailList';
import { ActionsTd, ActionItem } from '../../PaginatedTable'; import { ActionsTd, ActionItem } from '../../PaginatedTable';
import { ScheduleToggle } from '..'; import { ScheduleToggle } from '..';
import { Schedule } from '../../../types'; import { Schedule } from '../../../types';
import { formatDateString } from '../../../util/dates'; import { formatDateString } from '../../../util/dates';
function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)`
color: #c9190b;
margin-left: 20px;
`;
function ScheduleListItem({
i18n,
rowIndex,
isSelected,
onSelect,
schedule,
isMissingInventory,
isMissingSurvey,
}) {
const labelId = `check-action-${schedule.id}`; const labelId = `check-action-${schedule.id}`;
const jobTypeLabels = { const jobTypeLabels = {
@@ -45,6 +62,7 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) {
default: default:
break; break;
} }
const isDisabled = Boolean(isMissingInventory || isMissingSurvey);
return ( return (
<Tr id={`schedule-row-${schedule.id}`}> <Tr id={`schedule-row-${schedule.id}`}>
@@ -61,6 +79,18 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) {
<Link to={`${scheduleBaseUrl}/details`}> <Link to={`${scheduleBaseUrl}/details`}>
<b>{schedule.name}</b> <b>{schedule.name}</b>
</Link> </Link>
{Boolean(isMissingInventory || isMissingSurvey) && (
<span>
<Tooltip
content={[isMissingInventory, isMissingSurvey].map(message => (
<div key={message}>{message}</div>
))}
position="right"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</Td> </Td>
<Td dataLabel={i18n._(t`Type`)}> <Td dataLabel={i18n._(t`Type`)}>
{ {
@@ -80,7 +110,7 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) {
)} )}
</Td> </Td>
<ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px"> <ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px">
<ScheduleToggle schedule={schedule} /> <ScheduleToggle schedule={schedule} isDisabled={isDisabled} />
<ActionItem <ActionItem
visible={schedule.summary_fields.user_capabilities.edit} visible={schedule.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Schedule`)} tooltip={i18n._(t`Edit Schedule`)}

View File

@@ -50,15 +50,13 @@ describe('ScheduleListItem', () => {
describe('User has edit permissions', () => { describe('User has edit permissions', () => {
beforeAll(() => { beforeAll(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<table> <ScheduleListItem
<tbody> isSelected={false}
<ScheduleListItem onSelect={onSelect}
isSelected={false} schedule={mockSchedule}
onSelect={onSelect} isMissingSurvey={false}
schedule={mockSchedule} isMissingInventory={false}
/> />
</tbody>
</table>
); );
}); });
@@ -118,6 +116,9 @@ describe('ScheduleListItem', () => {
.simulate('change'); .simulate('change');
expect(onSelect).toHaveBeenCalledTimes(1); expect(onSelect).toHaveBeenCalledTimes(1);
}); });
test('Toggle button is enabled', () => {
expect(wrapper.find('ScheduleToggle').prop('isDisabled')).toBe(false);
});
}); });
describe('User has read-only permissions', () => { describe('User has read-only permissions', () => {
@@ -186,4 +187,35 @@ describe('ScheduleListItem', () => {
).toBe(true); ).toBe(true);
}); });
}); });
describe('schedule has missing prompt data', () => {
beforeAll(() => {
wrapper = mountWithContexts(
<ScheduleListItem
isSelected={false}
onSelect={onSelect}
schedule={{
...mockSchedule,
summary_fields: {
...mockSchedule.summary_fields,
user_capabilities: {
edit: false,
delete: false,
},
},
}}
isMissingInventory="Inventory Error"
isMissingSurvey="Survey Error"
/>
);
});
afterAll(() => {
wrapper.unmount();
});
test('should show missing resource icon', () => {
expect(wrapper.find('ExclamationTriangleIcon').length).toBe(1);
expect(wrapper.find('ScheduleToggle').prop('isDisabled')).toBe(true);
});
});
}); });

View File

@@ -8,7 +8,7 @@ import ErrorDetail from '../../ErrorDetail';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { SchedulesAPI } from '../../../api'; import { SchedulesAPI } from '../../../api';
function ScheduleToggle({ schedule, onToggle, className, i18n }) { function ScheduleToggle({ schedule, onToggle, className, i18n, isDisabled }) {
const [isEnabled, setIsEnabled] = useState(schedule.enabled); const [isEnabled, setIsEnabled] = useState(schedule.enabled);
const [showError, setShowError] = useState(false); const [showError, setShowError] = useState(false);
@@ -55,7 +55,9 @@ function ScheduleToggle({ schedule, onToggle, className, i18n }) {
labelOff={i18n._(t`Off`)} labelOff={i18n._(t`Off`)}
isChecked={isEnabled} isChecked={isEnabled}
isDisabled={ isDisabled={
isLoading || !schedule.summary_fields.user_capabilities.edit isLoading ||
!schedule.summary_fields.user_capabilities.edit ||
isDisabled
} }
onChange={toggleSchedule} onChange={toggleSchedule}
aria-label={i18n._(t`Toggle schedule`)} aria-label={i18n._(t`Toggle schedule`)}

View File

@@ -10,6 +10,8 @@ function Schedules({
loadScheduleOptions, loadScheduleOptions,
loadSchedules, loadSchedules,
setBreadcrumb, setBreadcrumb,
launchConfig,
surveyConfig,
resource, resource,
}) { }) {
const match = useRouteMatch(); const match = useRouteMatch();
@@ -17,14 +19,27 @@ function Schedules({
return ( return (
<Switch> <Switch>
<Route path={`${match.path}/add`}> <Route path={`${match.path}/add`}>
<ScheduleAdd apiModel={apiModel} resource={resource} /> <ScheduleAdd
apiModel={apiModel}
resource={resource}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
/>
</Route> </Route>
<Route key="details" path={`${match.path}/:scheduleId`}> <Route key="details" path={`${match.path}/:scheduleId`}>
<Schedule setBreadcrumb={setBreadcrumb} resource={resource} /> <Schedule
setBreadcrumb={setBreadcrumb}
resource={resource}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
/>
</Route> </Route>
<Route key="list" path={`${match.path}`}> <Route key="list" path={`${match.path}`}>
<ScheduleList <ScheduleList
resource={resource}
loadSchedules={loadSchedules} loadSchedules={loadSchedules}
launchConfig={launchConfig}
surveyConfig={surveyConfig}
loadScheduleOptions={loadScheduleOptions} loadScheduleOptions={loadScheduleOptions}
/> />
</Route> </Route>

View File

@@ -8,6 +8,7 @@
"rrule": "rrule":
"DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
"id": 1, "id": 1,
"extra_data":{},
"summary_fields": { "summary_fields": {
"unified_job_template": { "unified_job_template": {
"id": 6, "id": 6,
@@ -27,6 +28,7 @@
"rrule": "rrule":
"DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
"id": 2, "id": 2,
"extra_data":{},
"summary_fields": { "summary_fields": {
"unified_job_template": { "unified_job_template": {
"id": 7, "id": 7,
@@ -46,6 +48,7 @@
"rrule": "rrule":
"DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
"id": 3, "id": 3,
"extra_data":{},
"summary_fields": { "summary_fields": {
"unified_job_template": { "unified_job_template": {
"id": 8, "id": 8,
@@ -65,6 +68,7 @@
"rrule": "rrule":
"DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
"id": 4, "id": 4,
"extra_data":{},
"summary_fields": { "summary_fields": {
"unified_job_template": { "unified_job_template": {
"id": 9, "id": 9,
@@ -84,6 +88,7 @@
"rrule": "rrule":
"DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1",
"id": 5, "id": 5,
"extra_data":{"novalue":null},
"summary_fields": { "summary_fields": {
"unified_job_template": { "unified_job_template": {
"id": 10, "id": 10,
@@ -103,4 +108,4 @@
"next_run": "2020-02-20T05:00:00Z" "next_run": "2020-02-20T05:00:00Z"
} }
] ]
} }

View File

@@ -12,11 +12,7 @@ import {
ActionGroup, ActionGroup,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { Config } from '../../../contexts/Config'; import { Config } from '../../../contexts/Config';
import { import { SchedulesAPI } from '../../../api';
SchedulesAPI,
JobTemplatesAPI,
WorkflowJobTemplatesAPI,
} from '../../../api';
import AnsibleSelect from '../../AnsibleSelect'; import AnsibleSelect from '../../AnsibleSelect';
import ContentError from '../../ContentError'; import ContentError from '../../ContentError';
import ContentLoading from '../../ContentLoading'; import ContentLoading from '../../ContentLoading';
@@ -194,6 +190,8 @@ function ScheduleForm({
schedule, schedule,
submitError, submitError,
resource, resource,
launchConfig,
surveyConfig,
...rest ...rest
}) { }) {
const [isWizardOpen, setIsWizardOpen] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false);
@@ -210,37 +208,15 @@ function ScheduleForm({
const isTemplate = const isTemplate =
resource.type === 'workflow_job_template' || resource.type === 'workflow_job_template' ||
resource.type === 'job_template'; resource.type === 'job_template';
const isWorkflowJobTemplate =
isTemplate && resource.type === 'workflow_job_template';
const { const {
request: loadScheduleData, request: loadScheduleData,
error: contentError, error: contentError,
contentLoading, contentLoading,
result: { zoneOptions, surveyConfig, launchConfig, credentials }, result: { zoneOptions, credentials },
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const readLaunch = const { data } = await SchedulesAPI.readZoneInfo();
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; let creds;
if (schedule.id) { if (schedule.id) {
const { const {
@@ -249,37 +225,6 @@ function ScheduleForm({
creds = results; 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 => { const zones = data.map(zone => {
return { return {
value: zone.name, value: zone.name,
@@ -290,18 +235,61 @@ function ScheduleForm({
return { return {
zoneOptions: zones, zoneOptions: zones,
surveyConfig: surveyConfiguration || {},
launchConfig: launchConfiguration,
credentials: creds || [], credentials: creds || [],
}; };
}, [isTemplate, isWorkflowJobTemplate, resource, schedule]), }, [schedule]),
{ {
zonesOptions: [], zonesOptions: [],
surveyConfig: {},
launchConfig: {},
credentials: [], credentials: [],
} }
); );
const missingRequiredInventory = useCallback(() => {
let missingInventory = false;
if (
launchConfig.inventory_needed_to_start &&
!schedule?.summary_fields?.inventory?.id
) {
missingInventory = true;
}
return missingInventory;
}, [launchConfig, schedule]);
const hasMissingSurveyValue = useCallback(() => {
let missingValues = false;
if (launchConfig?.survey_enabled) {
surveyConfig.spec.forEach(question => {
const hasDefaultValue = Boolean(question.default);
const hasSchedule = Object.keys(schedule).length;
const isRequired = question.required;
if (isRequired && !hasDefaultValue) {
if (!hasSchedule) {
missingValues = true;
} else {
const hasMatchingKey = Object.keys(schedule?.extra_data).includes(
question.variable
);
Object.values(schedule?.extra_data).forEach(value => {
if (!value || !hasMatchingKey) {
missingValues = true;
} else {
missingValues = false;
}
});
if (!Object.values(schedule.extra_data).length) {
missingValues = true;
}
}
}
});
}
return missingValues;
}, [launchConfig, schedule, surveyConfig]);
useEffect(() => {
if (isTemplate && (missingRequiredInventory() || hasMissingSurveyValue())) {
setIsSaveDisabled(true);
}
}, [isTemplate, hasMissingSurveyValue, missingRequiredInventory]);
useEffect(() => { useEffect(() => {
loadScheduleData(); loadScheduleData();
@@ -532,6 +520,7 @@ function ScheduleForm({
> >
{i18n._(t`Save`)} {i18n._(t`Save`)}
</Button> </Button>
{isTemplate && showPromptButton && ( {isTemplate && showPromptButton && (
<Button <Button
variant="secondary" variant="secondary"

View File

@@ -12,24 +12,6 @@ jest.mock('../../../api/models/Schedules');
jest.mock('../../../api/models/JobTemplates'); jest.mock('../../../api/models/JobTemplates');
jest.mock('../../../api/models/Inventories'); 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 = { const credentials = {
data: { data: {
results: [ results: [
@@ -145,6 +127,29 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{
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: '',
},
}}
resource={{ id: 23, type: 'job_template' }} resource={{ id: 23, type: 'job_template' }}
/> />
); );
@@ -158,7 +163,6 @@ describe('<ScheduleForm />', () => {
const handleCancel = jest.fn(); const handleCancel = jest.fn();
JobTemplatesAPI.readLaunch.mockResolvedValue(launchData); JobTemplatesAPI.readLaunch.mockResolvedValue(launchData);
JobTemplatesAPI.readSurvey.mockResolvedValue(survey);
SchedulesAPI.readCredentials.mockResolvedValue(credentials); SchedulesAPI.readCredentials.mockResolvedValue(credentials);
SchedulesAPI.readZoneInfo.mockResolvedValue({ SchedulesAPI.readZoneInfo.mockResolvedValue({
data: [ data: [
@@ -172,7 +176,30 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={handleCancel} handleCancel={handleCancel}
resource={{ id: 23, type: 'job_template' }} launchConfig={{
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: '',
},
}}
resource={{ id: 23, type: 'job_template', inventory: 1 }}
/> />
); );
}); });
@@ -186,31 +213,6 @@ describe('<ScheduleForm />', () => {
describe('Prompted Schedule', () => { describe('Prompted Schedule', () => {
let promptWrapper; let promptWrapper;
beforeEach(async () => { 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({ SchedulesAPI.readZoneInfo.mockResolvedValue({
data: [ data: [
{ {
@@ -231,6 +233,30 @@ describe('<ScheduleForm />', () => {
credentials: [], credentials: [],
}, },
}} }}
launchConfig={{
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: '',
},
}}
surveyConfig={{ spec: [{ required: true, default: '' }] }}
/> />
); );
}); });
@@ -257,6 +283,12 @@ describe('<ScheduleForm />', () => {
expect(promptWrapper.find('WizardNavItem').length).toBe(2); expect(promptWrapper.find('WizardNavItem').length).toBe(2);
}); });
test('should render disabled save button due to missing required surevy values', () => {
expect(
promptWrapper.find('Button[aria-label="Save"]').prop('isDisabled')
).toBe(true);
});
test('should update prompt modal data', async () => { test('should update prompt modal data', async () => {
InventoriesAPI.read.mockResolvedValue({ InventoriesAPI.read.mockResolvedValue({
data: { data: {
@@ -337,6 +369,29 @@ describe('<ScheduleForm />', () => {
id: 23, id: 23,
type: 'job_template', type: 'job_template',
}} }}
launchConfig={{
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: '',
},
}}
/> />
); );
}); });
@@ -359,31 +414,6 @@ 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 () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
@@ -391,6 +421,29 @@ describe('<ScheduleForm />', () => {
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
resource={{ id: 23, type: 'job_template', inventory: 1 }} resource={{ id: 23, type: 'job_template', inventory: 1 }}
launchConfig={{
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: '',
},
}}
/> />
); );
}); });
@@ -687,32 +740,7 @@ 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); SchedulesAPI.readCredentials.mockResolvedValue(credentials);
}); });
afterEach(() => { afterEach(() => {
@@ -728,21 +756,65 @@ describe('<ScheduleForm />', () => {
handleCancel={jest.fn()} handleCancel={jest.fn()}
schedule={{ inventory: null, ...mockSchedule }} schedule={{ inventory: null, ...mockSchedule }}
resource={{ id: 23, type: 'job_template' }} resource={{ id: 23, type: 'job_template' }}
launchConfig={{
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: '',
},
}}
/> />
); );
}); });
expect(JobTemplatesAPI.readLaunch).toBeCalledWith(23);
expect(JobTemplatesAPI.readSurvey).toBeCalledWith(23);
expect(SchedulesAPI.readCredentials).toBeCalledWith(27); expect(SchedulesAPI.readCredentials).toBeCalledWith(27);
}); });
test('should not call API to get credentials ', async () => { test('should not call API to get credentials ', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
resource={{ id: 23, type: 'job_template' }} resource={{ id: 23, type: 'job_template' }}
launchConfig={{
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: '',
},
}}
/> />
); );
}); });
@@ -782,6 +854,7 @@ describe('<ScheduleForm />', () => {
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
schedule={mockSchedule} schedule={mockSchedule}
launchConfig={{ inventory_needed_to_start: false }}
resource={{ id: 23, type: 'job_template' }} resource={{ id: 23, type: 'job_template' }}
/> />
); );
@@ -806,6 +879,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=10;FREQ=MINUTELY', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=10;FREQ=MINUTELY',
@@ -839,6 +913,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=HOURLY;COUNT=10', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=HOURLY;COUNT=10',
@@ -874,6 +949,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=DAILY', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=DAILY',
@@ -908,6 +984,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20210101T050000Z',
@@ -966,6 +1043,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR',
@@ -1012,6 +1090,7 @@ describe('<ScheduleForm />', () => {
<ScheduleForm <ScheduleForm
handleSubmit={jest.fn()} handleSubmit={jest.fn()}
handleCancel={jest.fn()} handleCancel={jest.fn()}
launchConfig={{ inventory_needed_to_start: false }}
schedule={Object.assign(mockSchedule, { schedule={Object.assign(mockSchedule, {
rrule: rrule:
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=5;BYMONTHDAY=6', 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=5;BYMONTHDAY=6',

View File

@@ -22,7 +22,7 @@ export default function useSchedulePromptSteps(
(Object.keys(schedule).length > 0 && schedule) || resource; (Object.keys(schedule).length > 0 && schedule) || resource;
sourceOfValues.summary_fields = { sourceOfValues.summary_fields = {
credentials: [...resourceCredentials, ...scheduleCredentials], credentials: [...(resourceCredentials || []), ...scheduleCredentials],
...sourceOfValues.summary_fields, ...sourceOfValues.summary_fields,
}; };
const { resetForm, values } = useFormikContext(); const { resetForm, values } = useFormikContext();

View File

@@ -326,11 +326,6 @@ function JobDetail({ job, i18n }) {
user={created_by} user={created_by}
/> />
<UserDateDetail label={i18n._(t`Last Modified`)} date={job.modified} /> <UserDateDetail label={i18n._(t`Last Modified`)} date={job.modified} />
<Detail
fullWidth
label={i18n._(t`Explanation`)}
value={job.job_explanation}
/>
</DetailList> </DetailList>
{job.extra_vars && ( {job.extra_vars && (
<VariablesInput <VariablesInput

View File

@@ -40,7 +40,6 @@ describe('<JobDetail />', () => {
name: 'Test Source Workflow', name: 'Test Source Workflow',
}, },
}, },
job_explanation: 'It failed, bummer!',
}} }}
/> />
); );
@@ -70,7 +69,6 @@ describe('<JobDetail />', () => {
assertDetail('Job Slice', '0/1'); assertDetail('Job Slice', '0/1');
assertDetail('Credentials', 'SSH: Demo Credential'); assertDetail('Credentials', 'SSH: Demo Credential');
assertDetail('Machine Credential', 'SSH: Machine cred'); assertDetail('Machine Credential', 'SSH: Machine cred');
assertDetail('Explanation', 'It failed, bummer!');
const credentialChip = wrapper.find( const credentialChip = wrapper.find(
`Detail[label="Credentials"] CredentialChip` `Detail[label="Credentials"] CredentialChip`

View File

@@ -32,20 +32,33 @@ function Template({ i18n, setBreadcrumb }) {
const { me = {} } = useConfig(); const { me = {} } = useConfig();
const { const {
result: { isNotifAdmin, template }, result: { isNotifAdmin, template, surveyConfig, launchConfig },
isLoading, isLoading,
error: contentError, error: contentError,
request: loadTemplateAndRoles, request: loadTemplateAndRoles,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const [{ data }, actions, notifAdminRes] = await Promise.all([ const [
{ data },
actions,
notifAdminRes,
{ data: launchConfiguration },
] = await Promise.all([
JobTemplatesAPI.readDetail(templateId), JobTemplatesAPI.readDetail(templateId),
JobTemplatesAPI.readTemplateOptions(templateId), JobTemplatesAPI.readTemplateOptions(templateId),
OrganizationsAPI.read({ OrganizationsAPI.read({
page_size: 1, page_size: 1,
role_level: 'notification_admin_role', role_level: 'notification_admin_role',
}), }),
JobTemplatesAPI.readLaunch(templateId),
]); ]);
let surveyConfiguration = null;
if (data.survey_enabled) {
const { data: survey } = await JobTemplatesAPI.readSurvey(templateId);
surveyConfiguration = survey;
}
if (data.summary_fields.credentials) { if (data.summary_fields.credentials) {
const params = { const params = {
page: 1, page: 1,
@@ -71,6 +84,8 @@ function Template({ i18n, setBreadcrumb }) {
return { return {
template: data, template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0, isNotifAdmin: notifAdminRes.data.results.length > 0,
surveyConfig: surveyConfiguration,
launchConfig: launchConfiguration,
}; };
}, [templateId]), }, [templateId]),
{ isNotifAdmin: false, template: null } { isNotifAdmin: false, template: null }
@@ -204,6 +219,8 @@ function Template({ i18n, setBreadcrumb }) {
resource={template} resource={template}
loadSchedules={loadSchedules} loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions} loadScheduleOptions={loadScheduleOptions}
surveyConfig={surveyConfig}
launchConfig={launchConfig}
/> />
</Route> </Route>
{canSeeNotificationsTab && ( {canSeeNotificationsTab && (

View File

@@ -21,7 +21,7 @@ describe('<Template />', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
JobTemplatesAPI.readDetail.mockResolvedValue({ JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData, data: { ...mockJobTemplateData, survey_enabled: false },
}); });
JobTemplatesAPI.readTemplateOptions.mockResolvedValue({ JobTemplatesAPI.readTemplateOptions.mockResolvedValue({
data: { data: {
@@ -56,6 +56,7 @@ describe('<Template />', () => {
], ],
}, },
}); });
JobTemplatesAPI.readLaunch.mockResolvedValue({ data: {} });
JobTemplatesAPI.readWebhookKey.mockResolvedValue({ JobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: { data: {
webhook_key: 'key', webhook_key: 'key',

View File

@@ -36,21 +36,37 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
const { me = {} } = useConfig(); const { me = {} } = useConfig();
const { const {
result: { isNotifAdmin, template }, result: { isNotifAdmin, template, surveyConfig, launchConfig },
isLoading: hasRolesandTemplateLoading, isLoading: hasRolesandTemplateLoading,
error: rolesAndTemplateError, error: rolesAndTemplateError,
request: loadTemplateAndRoles, request: loadTemplateAndRoles,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const [{ data }, actions, notifAdminRes] = await Promise.all([ const [
{ data },
actions,
notifAdminRes,
{ data: launchConfiguration },
] = await Promise.all([
WorkflowJobTemplatesAPI.readDetail(templateId), WorkflowJobTemplatesAPI.readDetail(templateId),
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions(templateId), WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions(templateId),
OrganizationsAPI.read({ OrganizationsAPI.read({
page_size: 1, page_size: 1,
role_level: 'notification_admin_role', role_level: 'notification_admin_role',
}), }),
WorkflowJobTemplatesAPI.readLaunch(templateId),
]); ]);
let surveyConfiguration = null;
if (data.survey_enabled) {
const { data: survey } = await WorkflowJobTemplatesAPI.readSurvey(
templateId
);
surveyConfiguration = survey;
}
if (actions.data.actions.PUT) { if (actions.data.actions.PUT) {
if (data.webhook_service && data?.related?.webhook_key) { if (data.webhook_service && data?.related?.webhook_key) {
const { const {
@@ -65,6 +81,8 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
return { return {
template: data, template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0, isNotifAdmin: notifAdminRes.data.results.length > 0,
launchConfig: launchConfiguration,
surveyConfig: surveyConfiguration,
}; };
}, [setBreadcrumb, templateId]), }, [setBreadcrumb, templateId]),
{ isNotifAdmin: false, template: null } { isNotifAdmin: false, template: null }
@@ -207,6 +225,8 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
resource={template} resource={template}
loadSchedules={loadSchedules} loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions} loadScheduleOptions={loadScheduleOptions}
surveyConfig={surveyConfig}
launchConfig={launchConfig}
/> />
</Route> </Route>
)} )}

View File

@@ -26,7 +26,7 @@ describe('<WorkflowJobTemplate />', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({ WorkflowJobTemplatesAPI.readDetail.mockResolvedValue({
data: mockWorkflowJobTemplateData, data: { ...mockWorkflowJobTemplateData, survey_enabled: false },
}); });
WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({ WorkflowJobTemplatesAPI.readWorkflowJobTemplateOptions.mockResolvedValue({
data: { data: {
@@ -45,6 +45,7 @@ describe('<WorkflowJobTemplate />', () => {
], ],
}, },
}); });
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({ data: {} });
WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({ WorkflowJobTemplatesAPI.readWebhookKey.mockResolvedValue({
data: { data: {
webhook_key: 'key', webhook_key: 'key',