mirror of
https://github.com/ansible/awx.git
synced 2026-05-23 16:47:45 -02:30
Merge pull request #9210 from AlexSCorey/7692-PromptsOnSchedules
Prompts on schedules Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -10,6 +10,7 @@ class InventorySources extends LaunchUpdateMixin(
|
|||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/inventory_sources/';
|
this.baseUrl = '/api/v2/inventory_sources/';
|
||||||
|
|
||||||
|
this.createSchedule = this.createSchedule.bind(this);
|
||||||
this.createSyncStart = this.createSyncStart.bind(this);
|
this.createSyncStart = this.createSyncStart.bind(this);
|
||||||
this.destroyGroups = this.destroyGroups.bind(this);
|
this.destroyGroups = this.destroyGroups.bind(this);
|
||||||
this.destroyHosts = this.destroyHosts.bind(this);
|
this.destroyHosts = this.destroyHosts.bind(this);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class JobTemplates extends SchedulesMixin(
|
|||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/job_templates/';
|
this.baseUrl = '/api/v2/job_templates/';
|
||||||
|
|
||||||
|
this.createSchedule = this.createSchedule.bind(this);
|
||||||
this.launch = this.launch.bind(this);
|
this.launch = this.launch.bind(this);
|
||||||
this.readLaunch = this.readLaunch.bind(this);
|
this.readLaunch = this.readLaunch.bind(this);
|
||||||
this.associateLabel = this.associateLabel.bind(this);
|
this.associateLabel = this.associateLabel.bind(this);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Projects extends SchedulesMixin(
|
|||||||
this.readPlaybooks = this.readPlaybooks.bind(this);
|
this.readPlaybooks = this.readPlaybooks.bind(this);
|
||||||
this.readSync = this.readSync.bind(this);
|
this.readSync = this.readSync.bind(this);
|
||||||
this.sync = this.sync.bind(this);
|
this.sync = this.sync.bind(this);
|
||||||
|
this.createSchedule = this.createSchedule.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
readAccessList(id, params) {
|
readAccessList(id, params) {
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ class Schedules extends Base {
|
|||||||
return this.http.get(`${this.baseUrl}${resourceId}/credentials/`, params);
|
return this.http.get(`${this.baseUrl}${resourceId}/credentials/`, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
associateCredential(resourceId, credentialId) {
|
||||||
|
return this.http.post(`${this.baseUrl}${resourceId}/credentials/`, {
|
||||||
|
id: credentialId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disassociateCredential(resourceId, credentialId) {
|
||||||
|
return this.http.post(`${this.baseUrl}${resourceId}/credentials/`, {
|
||||||
|
id: credentialId,
|
||||||
|
disassociate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
readZoneInfo() {
|
readZoneInfo() {
|
||||||
return this.http.get(`${this.baseUrl}zoneinfo/`);
|
return this.http.get(`${this.baseUrl}zoneinfo/`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) {
|
|||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||||
|
this.createSchedule = this.createSchedule.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
readWebhookKey(id) {
|
readWebhookKey(id) {
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ function JobListItem({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{job.job_explanation && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Explanation`)}
|
||||||
|
value={job.job_explanation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export default function usePreviewStep(
|
|||||||
resource,
|
resource,
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
showStep
|
showStep,
|
||||||
|
nextButtonText
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
step: showStep
|
step: showStep
|
||||||
@@ -31,7 +32,7 @@ export default function usePreviewStep(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: !hasErrors,
|
enableNext: !hasErrors,
|
||||||
nextButtonText: i18n._(t`Launch`),
|
nextButtonText: nextButtonText || i18n._(t`Launch`),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
|
||||||
@@ -17,37 +17,40 @@ import ContentLoading from '../ContentLoading';
|
|||||||
import ScheduleDetail from './ScheduleDetail';
|
import ScheduleDetail from './ScheduleDetail';
|
||||||
import ScheduleEdit from './ScheduleEdit';
|
import ScheduleEdit from './ScheduleEdit';
|
||||||
import { SchedulesAPI } from '../../api';
|
import { SchedulesAPI } from '../../api';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
|
||||||
function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
function Schedule({
|
||||||
const [schedule, setSchedule] = useState(null);
|
i18n,
|
||||||
const [contentLoading, setContentLoading] = useState(true);
|
setBreadcrumb,
|
||||||
const [contentError, setContentError] = useState(null);
|
resource,
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
}) {
|
||||||
const { scheduleId } = useParams();
|
const { scheduleId } = useParams();
|
||||||
const location = useLocation();
|
|
||||||
const { pathname } = location;
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
|
||||||
|
|
||||||
useEffect(() => {
|
const { isLoading, error, request: loadData, result: schedule } = useRequest(
|
||||||
const loadData = async () => {
|
useCallback(async () => {
|
||||||
try {
|
const { data } = await SchedulesAPI.readDetail(scheduleId);
|
||||||
const { data } = await SchedulesAPI.readDetail(scheduleId);
|
|
||||||
setSchedule(data);
|
|
||||||
} catch (err) {
|
|
||||||
setContentError(err);
|
|
||||||
} finally {
|
|
||||||
setContentLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [scheduleId]),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, [location.pathname, scheduleId]);
|
}, [loadData, pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schedule) {
|
if (schedule) {
|
||||||
setBreadcrumb(unifiedJobTemplate, schedule);
|
setBreadcrumb(resource, schedule);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [schedule, unifiedJobTemplate]);
|
}, [schedule, resource]);
|
||||||
const tabsArray = [
|
const tabsArray = [
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
@@ -66,13 +69,13 @@ function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (contentLoading) {
|
if (isLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schedule.summary_fields.unified_job_template.id !==
|
schedule?.summary_fields.unified_job_template.id !==
|
||||||
parseInt(unifiedJobTemplate.id, 10)
|
parseInt(resource.id, 10)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ContentError>
|
<ContentError>
|
||||||
@@ -83,16 +86,13 @@ function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentError) {
|
if (error) {
|
||||||
return <ContentError error={contentError} />;
|
return <ContentError error={error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
|
|
||||||
if (
|
if (!pathname.includes('schedules/') || pathname.endsWith('edit')) {
|
||||||
!location.pathname.includes('schedules/') ||
|
|
||||||
location.pathname.endsWith('edit')
|
|
||||||
) {
|
|
||||||
showCardHeader = false;
|
showCardHeader = false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -106,18 +106,23 @@ function Schedule({ i18n, setBreadcrumb, unifiedJobTemplate }) {
|
|||||||
/>
|
/>
|
||||||
{schedule && [
|
{schedule && [
|
||||||
<Route key="edit" path={`${pathRoot}schedules/:id/edit`}>
|
<Route key="edit" path={`${pathRoot}schedules/:id/edit`}>
|
||||||
<ScheduleEdit schedule={schedule} />
|
<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="*">
|
||||||
<ContentError>
|
<ContentError>
|
||||||
{unifiedJobTemplate && (
|
{resource && (
|
||||||
<Link to={`${pathRoot}details`}>{i18n._(t`View Details`)}</Link>
|
<Link to={`${pathRoot}details`}>{i18n._(t`View Details`)}</Link>
|
||||||
)}
|
)}
|
||||||
</ContentError>
|
</ContentError>
|
||||||
|
|||||||
@@ -93,10 +93,7 @@ describe('<Schedule />', () => {
|
|||||||
<Route
|
<Route
|
||||||
path="/templates/job_template/:id/schedules"
|
path="/templates/job_template/:id/schedules"
|
||||||
component={() => (
|
component={() => (
|
||||||
<Schedule
|
<Schedule setBreadcrumb={() => {}} resource={unifiedJobTemplate} />
|
||||||
setBreadcrumb={() => {}}
|
|
||||||
unifiedJobTemplate={unifiedJobTemplate}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,31 +1,89 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { func } from 'prop-types';
|
import { func, shape } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { Card } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import { CardBody } from '../../Card';
|
import { CardBody } from '../../Card';
|
||||||
|
import { parseVariableField } from '../../../util/yaml';
|
||||||
|
|
||||||
import buildRuleObj from '../shared/buildRuleObj';
|
import buildRuleObj from '../shared/buildRuleObj';
|
||||||
import ScheduleForm from '../shared/ScheduleForm';
|
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, createSchedule }) {
|
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 => {
|
const handleSubmit = async (
|
||||||
|
values,
|
||||||
|
launchConfiguration,
|
||||||
|
surveyConfiguration
|
||||||
|
) => {
|
||||||
|
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 =
|
||||||
|
launchConfiguration?.ask_variables_on_launch &&
|
||||||
|
(values.extra_vars || '---');
|
||||||
|
if (surveyConfiguration?.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 {
|
try {
|
||||||
const rule = new RRule(buildRuleObj(values, i18n));
|
const rule = new RRule(buildRuleObj(values, i18n));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { id: scheduleId },
|
data: { id: scheduleId },
|
||||||
} = await createSchedule({
|
} = await apiModel.createSchedule(resource.id, {
|
||||||
name: values.name,
|
...submitValues,
|
||||||
description: values.description,
|
|
||||||
rrule: rule.toString().replace(/\n/g, ' '),
|
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}`);
|
history.push(`${pathRoot}schedules/${scheduleId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFormSubmitError(err);
|
setFormSubmitError(err);
|
||||||
@@ -39,6 +97,9 @@ function ScheduleAdd({ i18n, createSchedule }) {
|
|||||||
handleCancel={() => history.push(`${pathRoot}schedules`)}
|
handleCancel={() => history.push(`${pathRoot}schedules`)}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
|
launchConfig={launchConfig}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
|
resource={resource}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -46,7 +107,7 @@ function ScheduleAdd({ i18n, createSchedule }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ScheduleAdd.propTypes = {
|
ScheduleAdd.propTypes = {
|
||||||
createSchedule: func.isRequired,
|
apiModel: shape({ createSchedule: func.isRequired }).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
ScheduleAdd.defaultProps = {};
|
ScheduleAdd.defaultProps = {};
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { SchedulesAPI } from '../../../api';
|
import { SchedulesAPI, JobTemplatesAPI, InventoriesAPI } from '../../../api';
|
||||||
import ScheduleAdd from './ScheduleAdd';
|
import ScheduleAdd from './ScheduleAdd';
|
||||||
|
|
||||||
jest.mock('../../../api/models/Schedules');
|
jest.mock('../../../api/models/Schedules');
|
||||||
|
jest.mock('../../../api/models/JobTemplates');
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
|
||||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||||
data: [
|
data: [
|
||||||
@@ -18,21 +20,62 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let wrapper;
|
const launchConfig = {
|
||||||
|
can_start_without_user_input: false,
|
||||||
const createSchedule = jest.fn().mockImplementation(() => {
|
passwords_needed_to_start: [],
|
||||||
return {
|
ask_scm_branch_on_launch: false,
|
||||||
data: {
|
ask_variables_on_launch: false,
|
||||||
id: 1,
|
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;
|
||||||
|
|
||||||
describe('<ScheduleAdd />', () => {
|
describe('<ScheduleAdd />', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ScheduleAdd createSchedule={createSchedule} />
|
<ScheduleAdd
|
||||||
|
apiModel={JobTemplatesAPI}
|
||||||
|
resource={{
|
||||||
|
id: 700,
|
||||||
|
type: 'job_template',
|
||||||
|
inventory: 2,
|
||||||
|
summary_fields: { credentials: [] },
|
||||||
|
}}
|
||||||
|
launchConfig={launchConfig}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
@@ -42,7 +85,7 @@ describe('<ScheduleAdd />', () => {
|
|||||||
});
|
});
|
||||||
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'none',
|
frequency: 'none',
|
||||||
@@ -52,16 +95,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run once schedule',
|
name: 'Run once schedule',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T100000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
'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 () => {
|
test('Successfully creates a schedule with 10 minute repeat frequency after 10 occurrences', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'after',
|
end: 'after',
|
||||||
frequency: 'minute',
|
frequency: 'minute',
|
||||||
@@ -72,16 +116,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run every 10 minutes 10 times',
|
name: 'Run every 10 minutes 10 times',
|
||||||
|
extra_data: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with hourly repeat frequency ending on a specific date/time', async () => {
|
test('Successfully creates a schedule with hourly repeat frequency ending on a specific date/time', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'onDate',
|
end: 'onDate',
|
||||||
endDateTime: '2020-03-26T10:45:00',
|
endDateTime: '2020-03-26T10:45:00',
|
||||||
@@ -92,16 +137,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run every hour until date',
|
name: 'Run every hour until date',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with daily repeat frequency', async () => {
|
test('Successfully creates a schedule with daily repeat frequency', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'day',
|
frequency: 'day',
|
||||||
@@ -111,16 +157,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run daily',
|
name: 'Run daily',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=DAILY',
|
'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 () => {
|
test('Successfully creates a schedule with weekly repeat frequency on mon/wed/fri', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
@@ -132,15 +179,16 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run weekly on mon/wed/fri',
|
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}`,
|
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 () => {
|
test('Successfully creates a schedule with monthly repeat frequency on the first day of the month', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'month',
|
frequency: 'month',
|
||||||
@@ -153,16 +201,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
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: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
|
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
endDateTime: '2020-03-26T11:00:00',
|
endDateTime: '2020-03-26T11:00:00',
|
||||||
@@ -177,16 +226,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run monthly on the last Tuesday',
|
name: 'Run monthly on the last Tuesday',
|
||||||
|
extra_data: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -200,16 +250,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the first day of March',
|
name: 'Yearly on the first day of March',
|
||||||
|
extra_data: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -224,16 +275,17 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the second Friday in April',
|
name: 'Yearly on the second Friday in April',
|
||||||
|
extra_data: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -248,11 +300,118 @@ describe('<ScheduleAdd />', () => {
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(createSchedule).toHaveBeenCalledWith({
|
expect(JobTemplatesAPI.createSchedule).toHaveBeenCalledWith(700, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the first weekday in October',
|
name: 'Yearly on the first weekday in October',
|
||||||
|
extra_data: {},
|
||||||
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -199,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} />
|
||||||
@@ -256,6 +296,12 @@ function ScheduleDetail({ schedule, i18n }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{ask_verbosity_on_launch && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Verbosity`)}
|
||||||
|
value={VERBOSITY[verbosity]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{ask_scm_branch_on_launch && (
|
{ask_scm_branch_on_launch && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Source Control Branch`)}
|
label={i18n._(t`Source Control Branch`)}
|
||||||
@@ -265,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`)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,28 +4,101 @@ import { useHistory, useLocation } from 'react-router-dom';
|
|||||||
import { RRule } from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { shape } from 'prop-types';
|
import { shape } from 'prop-types';
|
||||||
import { Card } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import { CardBody } from '../../Card';
|
import { CardBody } from '../../Card';
|
||||||
import { SchedulesAPI } from '../../../api';
|
import { SchedulesAPI } from '../../../api';
|
||||||
import buildRuleObj from '../shared/buildRuleObj';
|
import buildRuleObj from '../shared/buildRuleObj';
|
||||||
import ScheduleForm from '../shared/ScheduleForm';
|
import ScheduleForm from '../shared/ScheduleForm';
|
||||||
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
|
|
||||||
function ScheduleEdit({ i18n, schedule }) {
|
import { parseVariableField } from '../../../util/yaml';
|
||||||
|
import mergeExtraVars from '../../../util/prompt/mergeExtraVars';
|
||||||
|
import getSurveyValues from '../../../util/prompt/getSurveyValues';
|
||||||
|
|
||||||
|
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();
|
||||||
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 => {
|
const handleSubmit = async (
|
||||||
|
values,
|
||||||
|
launchConfiguration,
|
||||||
|
surveyConfiguration,
|
||||||
|
scheduleCredentials = []
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
inventory,
|
||||||
|
credentials = [],
|
||||||
|
end,
|
||||||
|
frequency,
|
||||||
|
interval,
|
||||||
|
startDateTime,
|
||||||
|
timezone,
|
||||||
|
occurences,
|
||||||
|
runOn,
|
||||||
|
runOnTheDay,
|
||||||
|
runOnTheMonth,
|
||||||
|
runOnDayMonth,
|
||||||
|
runOnDayNumber,
|
||||||
|
endDateTime,
|
||||||
|
runOnTheOccurence,
|
||||||
|
daysOfWeek,
|
||||||
|
...submitValues
|
||||||
|
} = values;
|
||||||
|
const { added, removed } = getAddedAndRemoved(
|
||||||
|
[...(resource?.summary_fields.credentials || []), ...scheduleCredentials],
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
|
let extraVars;
|
||||||
|
const surveyValues = getSurveyValues(values);
|
||||||
|
const initialExtraVars =
|
||||||
|
launchConfiguration?.ask_variables_on_launch &&
|
||||||
|
(values.extra_vars || '---');
|
||||||
|
if (surveyConfiguration?.spec) {
|
||||||
|
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues));
|
||||||
|
} else {
|
||||||
|
extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {}));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
if (inventory) {
|
||||||
|
submitValues.inventory = inventory.id;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rule = new RRule(buildRuleObj(values, i18n));
|
const rule = new RRule(buildRuleObj(values, i18n));
|
||||||
const {
|
const {
|
||||||
data: { id: scheduleId },
|
data: { id: scheduleId },
|
||||||
} = await SchedulesAPI.update(schedule.id, {
|
} = await SchedulesAPI.update(schedule.id, {
|
||||||
name: values.name,
|
...submitValues,
|
||||||
description: values.description,
|
|
||||||
rrule: rule.toString().replace(/\n/g, ' '),
|
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`);
|
history.push(`${pathRoot}schedules/${scheduleId}/details`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -43,6 +116,9 @@ function ScheduleEdit({ i18n, schedule }) {
|
|||||||
}
|
}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
|
resource={resource}
|
||||||
|
launchConfig={launchConfig}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { SchedulesAPI } from '../../../api';
|
import {
|
||||||
|
SchedulesAPI,
|
||||||
|
InventoriesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import ScheduleEdit from './ScheduleEdit';
|
import ScheduleEdit from './ScheduleEdit';
|
||||||
|
|
||||||
jest.mock('../../../api/models/Schedules');
|
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({
|
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||||
data: [
|
data: [
|
||||||
@@ -18,6 +27,35 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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({
|
SchedulesAPI.update.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
id: 27,
|
id: 27,
|
||||||
@@ -37,13 +75,14 @@ const mockSchedule = {
|
|||||||
edit: true,
|
edit: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
},
|
},
|
||||||
|
inventory: { id: 702, name: 'Inventory' },
|
||||||
},
|
},
|
||||||
created: '2020-04-02T18:43:12.664142Z',
|
created: '2020-04-02T18:43:12.664142Z',
|
||||||
modified: '2020-04-02T18:43:12.664185Z',
|
modified: '2020-04-02T18:43:12.664185Z',
|
||||||
name: 'mock schedule',
|
name: 'mock schedule',
|
||||||
description: '',
|
description: '',
|
||||||
extra_data: {},
|
extra_data: {},
|
||||||
inventory: null,
|
inventory: 1,
|
||||||
scm_branch: null,
|
scm_branch: null,
|
||||||
job_type: null,
|
job_type: null,
|
||||||
job_tags: null,
|
job_tags: null,
|
||||||
@@ -61,18 +100,71 @@ const mockSchedule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('<ScheduleEdit />', () => {
|
describe('<ScheduleEdit />', () => {
|
||||||
beforeAll(async () => {
|
beforeEach(async () => {
|
||||||
await act(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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
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={{}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
test('Successfully creates a schedule with repeat frequency: None (run once)', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'none',
|
frequency: 'none',
|
||||||
@@ -85,13 +177,14 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run once schedule',
|
name: 'Run once schedule',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T100000 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
'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 () => {
|
test('Successfully creates a schedule with 10 minute repeat frequency after 10 occurrences', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'after',
|
end: 'after',
|
||||||
frequency: 'minute',
|
frequency: 'minute',
|
||||||
@@ -105,13 +198,15 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run every 10 minutes 10 times',
|
name: 'Run every 10 minutes 10 times',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with hourly repeat frequency ending on a specific date/time', async () => {
|
test('Successfully creates a schedule with hourly repeat frequency ending on a specific date/time', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'onDate',
|
end: 'onDate',
|
||||||
endDateTime: '2020-03-26T10:45:00',
|
endDateTime: '2020-03-26T10:45:00',
|
||||||
@@ -125,13 +220,14 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run every hour until date',
|
name: 'Run every hour until date',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=HOURLY;UNTIL=20200326T104500',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with daily repeat frequency', async () => {
|
test('Successfully creates a schedule with daily repeat frequency', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'day',
|
frequency: 'day',
|
||||||
@@ -144,13 +240,14 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run daily',
|
name: 'Run daily',
|
||||||
|
extra_data: {},
|
||||||
rrule:
|
rrule:
|
||||||
'DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=DAILY',
|
'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 () => {
|
test('Successfully creates a schedule with weekly repeat frequency on mon/wed/fri', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
daysOfWeek: [RRule.MO, RRule.WE, RRule.FR],
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
@@ -165,12 +262,14 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run weekly on mon/wed/fri',
|
name: 'Run weekly on mon/wed/fri',
|
||||||
|
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}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with monthly repeat frequency on the first day of the month', async () => {
|
test('Successfully creates a schedule with monthly repeat frequency on the first day of the month', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'month',
|
frequency: 'month',
|
||||||
@@ -186,13 +285,15 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
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: {},
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
|
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
endDateTime: '2020-03-26T11:00:00',
|
endDateTime: '2020-03-26T11:00:00',
|
||||||
@@ -210,13 +311,16 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Run monthly on the last Tuesday',
|
name: 'Run monthly on the last Tuesday',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -233,13 +337,15 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the first day of March',
|
name: 'Yearly on the first day of March',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -257,13 +363,16 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the second Friday in April',
|
name: 'Yearly on the second Friday in April',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
|
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ScheduleForm').invoke('handleSubmit')({
|
wrapper.find('Formik').invoke('onSubmit')({
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
end: 'never',
|
end: 'never',
|
||||||
frequency: 'year',
|
frequency: 'year',
|
||||||
@@ -281,8 +390,221 @@ describe('<ScheduleEdit />', () => {
|
|||||||
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
expect(SchedulesAPI.update).toHaveBeenCalledWith(27, {
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
name: 'Yearly on the first weekday in October',
|
name: 'Yearly on the first weekday in October',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
occurrences: 1,
|
||||||
|
runOnTheOccurrence: 1,
|
||||||
|
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: {},
|
||||||
|
occurrences: 1,
|
||||||
|
runOnTheOccurrence: 1,
|
||||||
|
name: 'foo',
|
||||||
|
inventory: 702,
|
||||||
|
rrule:
|
||||||
|
'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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={[
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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`)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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`)}
|
||||||
|
|||||||
@@ -6,28 +6,40 @@ import ScheduleAdd from './ScheduleAdd';
|
|||||||
import ScheduleList from './ScheduleList';
|
import ScheduleList from './ScheduleList';
|
||||||
|
|
||||||
function Schedules({
|
function Schedules({
|
||||||
createSchedule,
|
apiModel,
|
||||||
loadScheduleOptions,
|
loadScheduleOptions,
|
||||||
loadSchedules,
|
loadSchedules,
|
||||||
setBreadcrumb,
|
setBreadcrumb,
|
||||||
unifiedJobTemplate,
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
|
resource,
|
||||||
}) {
|
}) {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/add`}>
|
<Route path={`${match.path}/add`}>
|
||||||
<ScheduleAdd createSchedule={createSchedule} />
|
<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
|
<Schedule
|
||||||
unifiedJobTemplate={unifiedJobTemplate}
|
|
||||||
setBreadcrumb={setBreadcrumb}
|
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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
import { shape, func } from 'prop-types';
|
import { shape, 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 { Formik, useField } from 'formik';
|
import { Formik, useField } from 'formik';
|
||||||
import { RRule } from 'rrule';
|
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 { Config } from '../../../contexts/Config';
|
||||||
import { SchedulesAPI } from '../../../api';
|
import { SchedulesAPI } 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';
|
||||||
import FormActionGroup from '../../FormActionGroup/FormActionGroup';
|
|
||||||
import FormField, { FormSubmitError } from '../../FormField';
|
import FormField, { FormSubmitError } from '../../FormField';
|
||||||
import { FormColumnLayout, SubFormLayout } from '../../FormLayout';
|
import {
|
||||||
|
FormColumnLayout,
|
||||||
|
SubFormLayout,
|
||||||
|
FormFullWidthLayout,
|
||||||
|
} from '../../FormLayout';
|
||||||
import { dateToInputDateTime, formatDateStringUTC } from '../../../util/dates';
|
import { dateToInputDateTime, formatDateStringUTC } from '../../../util/dates';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { required } from '../../../util/validators';
|
import { required } from '../../../util/validators';
|
||||||
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
import FrequencyDetailSubform from './FrequencyDetailSubform';
|
||||||
|
import SchedulePromptableFields from './SchedulePromptableFields';
|
||||||
|
|
||||||
const generateRunOnTheDay = (days = []) => {
|
const generateRunOnTheDay = (days = []) => {
|
||||||
if (
|
if (
|
||||||
@@ -179,8 +189,14 @@ function ScheduleForm({
|
|||||||
i18n,
|
i18n,
|
||||||
schedule,
|
schedule,
|
||||||
submitError,
|
submitError,
|
||||||
|
resource,
|
||||||
|
launchConfig,
|
||||||
|
surveyConfig,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
|
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||||
|
|
||||||
let rruleError;
|
let rruleError;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const closestQuarterHour = new Date(
|
const closestQuarterHour = new Date(
|
||||||
@@ -189,6 +205,113 @@ function ScheduleForm({
|
|||||||
const tomorrow = new Date(closestQuarterHour);
|
const tomorrow = new Date(closestQuarterHour);
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
|
||||||
|
const isTemplate =
|
||||||
|
resource.type === 'workflow_job_template' ||
|
||||||
|
resource.type === 'job_template';
|
||||||
|
const {
|
||||||
|
request: loadScheduleData,
|
||||||
|
error: contentError,
|
||||||
|
contentLoading,
|
||||||
|
result: { zoneOptions, credentials },
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await SchedulesAPI.readZoneInfo();
|
||||||
|
|
||||||
|
let creds;
|
||||||
|
if (schedule.id) {
|
||||||
|
const {
|
||||||
|
data: { results },
|
||||||
|
} = await SchedulesAPI.readCredentials(schedule.id);
|
||||||
|
creds = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zones = data.map(zone => {
|
||||||
|
return {
|
||||||
|
value: zone.name,
|
||||||
|
key: zone.name,
|
||||||
|
label: zone.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
zoneOptions: zones,
|
||||||
|
credentials: creds || [],
|
||||||
|
};
|
||||||
|
}, [schedule]),
|
||||||
|
{
|
||||||
|
zonesOptions: [],
|
||||||
|
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(() => {
|
||||||
|
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 = {
|
const initialValues = {
|
||||||
daysOfWeek: [],
|
daysOfWeek: [],
|
||||||
description: schedule.description || '',
|
description: schedule.description || '',
|
||||||
@@ -207,6 +330,19 @@ function ScheduleForm({
|
|||||||
startDateTime: dateToInputDateTime(closestQuarterHour),
|
startDateTime: dateToInputDateTime(closestQuarterHour),
|
||||||
timezone: schedule.timezone || 'America/New_York',
|
timezone: schedule.timezone || 'America/New_York',
|
||||||
};
|
};
|
||||||
|
const submitSchedule = (
|
||||||
|
values,
|
||||||
|
launchConfiguration,
|
||||||
|
surveyConfiguration,
|
||||||
|
scheduleCredentials
|
||||||
|
) => {
|
||||||
|
handleSubmit(
|
||||||
|
values,
|
||||||
|
launchConfiguration,
|
||||||
|
surveyConfiguration,
|
||||||
|
scheduleCredentials
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const overriddenValues = {};
|
const overriddenValues = {};
|
||||||
|
|
||||||
@@ -297,28 +433,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) {
|
if (contentError || rruleError) {
|
||||||
return <ContentError error={contentError || rruleError} />;
|
return <ContentError error={contentError || rruleError} />;
|
||||||
}
|
}
|
||||||
@@ -333,7 +447,9 @@ function ScheduleForm({
|
|||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={Object.assign(initialValues, overriddenValues)}
|
initialValues={Object.assign(initialValues, overriddenValues)}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={values => {
|
||||||
|
submitSchedule(values, launchConfig, surveyConfig, credentials);
|
||||||
|
}}
|
||||||
validate={values => {
|
validate={values => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
const {
|
const {
|
||||||
@@ -375,11 +491,56 @@ function ScheduleForm({
|
|||||||
zoneOptions={zoneOptions}
|
zoneOptions={zoneOptions}
|
||||||
{...rest}
|
{...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} />
|
<FormSubmitError error={submitError} />
|
||||||
<FormActionGroup
|
<FormFullWidthLayout>
|
||||||
onCancel={handleCancel}
|
<ActionGroup>
|
||||||
onSubmit={formik.handleSubmit}
|
<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>
|
</FormColumnLayout>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
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';
|
import ScheduleForm from './ScheduleForm';
|
||||||
|
|
||||||
jest.mock('../../../api/models/Schedules');
|
jest.mock('../../../api/models/Schedules');
|
||||||
|
jest.mock('../../../api/models/JobTemplates');
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
|
||||||
|
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 = {
|
const mockSchedule = {
|
||||||
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',
|
||||||
@@ -23,7 +65,7 @@ const mockSchedule = {
|
|||||||
name: 'mock schedule',
|
name: 'mock schedule',
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
extra_data: {},
|
extra_data: {},
|
||||||
inventory: null,
|
inventory: 1,
|
||||||
scm_branch: null,
|
scm_branch: null,
|
||||||
job_type: null,
|
job_type: null,
|
||||||
job_tags: null,
|
job_tags: null,
|
||||||
@@ -82,7 +124,34 @@ describe('<ScheduleForm />', () => {
|
|||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
<ScheduleForm
|
||||||
|
handleSubmit={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' }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -92,6 +161,9 @@ describe('<ScheduleForm />', () => {
|
|||||||
describe('Cancel', () => {
|
describe('Cancel', () => {
|
||||||
test('should make the appropriate callback', async () => {
|
test('should make the appropriate callback', async () => {
|
||||||
const handleCancel = jest.fn();
|
const handleCancel = jest.fn();
|
||||||
|
JobTemplatesAPI.readLaunch.mockResolvedValue(launchData);
|
||||||
|
|
||||||
|
SchedulesAPI.readCredentials.mockResolvedValue(credentials);
|
||||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
@@ -101,7 +173,34 @@ describe('<ScheduleForm />', () => {
|
|||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={handleCancel} />
|
<ScheduleForm
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
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 }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -111,6 +210,201 @@ describe('<ScheduleForm />', () => {
|
|||||||
expect(handleCancel).toHaveBeenCalledTimes(1);
|
expect(handleCancel).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('Prompted Schedule', () => {
|
||||||
|
let promptWrapper;
|
||||||
|
beforeEach(async () => {
|
||||||
|
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: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
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: '' }] }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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 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 () => {
|
||||||
|
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',
|
||||||
|
}}
|
||||||
|
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: '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Button[aria-label="Prompt"]',
|
||||||
|
el => el.length > 0
|
||||||
|
);
|
||||||
|
expect(wrapper.find('Button[aria-label="Save"]').prop('isDisabled')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('Add', () => {
|
describe('Add', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||||
@@ -120,9 +414,37 @@ describe('<ScheduleForm />', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ScheduleForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
<ScheduleForm
|
||||||
|
handleSubmit={jest.fn()}
|
||||||
|
handleCancel={jest.fn()}
|
||||||
|
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: '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -312,6 +634,14 @@ describe('<ScheduleForm />', () => {
|
|||||||
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1);
|
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1);
|
||||||
});
|
});
|
||||||
test('occurrences field properly shown when end after selection is made', async () => {
|
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 () => {
|
await act(async () => {
|
||||||
wrapper.find('Radio#end-after').invoke('onChange')('after', {
|
wrapper.find('Radio#end-after').invoke('onChange')('after', {
|
||||||
target: { name: 'end' },
|
target: { name: 'end' },
|
||||||
@@ -331,6 +661,14 @@ describe('<ScheduleForm />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
test('error shown when end date/time comes before start date/time', async () => {
|
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-never').prop('checked')).toBe(true);
|
||||||
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
|
||||||
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
|
||||||
@@ -361,13 +699,28 @@ describe('<ScheduleForm />', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('error shown when on day number is not between 1 and 31', async () => {
|
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', {
|
wrapper.find('input#schedule-run-on-day-number').simulate('change', {
|
||||||
target: { value: 32, name: 'runOnDayNumber' },
|
target: { value: 32, name: 'runOnDayNumber' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('input#schedule-run-on-day-number').prop('value')
|
||||||
|
).toBe(32);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
});
|
});
|
||||||
@@ -379,7 +732,7 @@ describe('<ScheduleForm />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Edit', () => {
|
describe('Edit', () => {
|
||||||
beforeAll(async () => {
|
beforeEach(async () => {
|
||||||
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
SchedulesAPI.readZoneInfo.mockResolvedValue({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
@@ -387,10 +740,113 @@ describe('<ScheduleForm />', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SchedulesAPI.readCredentials.mockResolvedValue(credentials);
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
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' }}
|
||||||
|
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(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' }}
|
||||||
|
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(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 () => {
|
test('initially renders expected fields and values with existing schedule that runs once', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
@@ -398,6 +854,8 @@ 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' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -421,11 +879,13 @@ 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',
|
||||||
dtend: null,
|
dtend: null,
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -453,12 +913,14 @@ 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',
|
||||||
dtend: '2020-04-03T03:45:00Z',
|
dtend: '2020-04-03T03:45:00Z',
|
||||||
until: '',
|
until: '',
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -487,12 +949,14 @@ 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',
|
||||||
dtend: null,
|
dtend: null,
|
||||||
until: '',
|
until: '',
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||||
@@ -520,12 +984,14 @@ 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',
|
||||||
dtend: '2020-10-30T18:45:00Z',
|
dtend: '2020-10-30T18:45:00Z',
|
||||||
until: '2021-01-01T00:00:00',
|
until: '2021-01-01T00:00:00',
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -577,12 +1043,14 @@ 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',
|
||||||
dtend: null,
|
dtend: null,
|
||||||
until: '',
|
until: '',
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
||||||
@@ -622,12 +1090,14 @@ 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',
|
||||||
dtend: null,
|
dtend: null,
|
||||||
until: '',
|
until: '',
|
||||||
})}
|
})}
|
||||||
|
resource={{ id: 23, type: 'job_template' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(wrapper.find('ScheduleForm').length).toBe(1);
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -69,9 +69,6 @@ function InventorySource({ i18n, inventory, setBreadcrumb, me }) {
|
|||||||
[source]
|
[source]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createSchedule = data =>
|
|
||||||
InventorySourcesAPI.createSchedule(source?.id, data);
|
|
||||||
|
|
||||||
const loadScheduleOptions = useCallback(() => {
|
const loadScheduleOptions = useCallback(() => {
|
||||||
return InventorySourcesAPI.readScheduleOptions(source?.id);
|
return InventorySourcesAPI.readScheduleOptions(source?.id);
|
||||||
}, [source]);
|
}, [source]);
|
||||||
@@ -160,11 +157,11 @@ function InventorySource({ i18n, inventory, setBreadcrumb, me }) {
|
|||||||
path="/inventories/inventory/:id/sources/:sourceId/schedules"
|
path="/inventories/inventory/:id/sources/:sourceId/schedules"
|
||||||
>
|
>
|
||||||
<Schedules
|
<Schedules
|
||||||
createSchedule={createSchedule}
|
apiModel={InventorySourcesAPI}
|
||||||
setBreadcrumb={(unifiedJobTemplate, schedule) =>
|
setBreadcrumb={schedule =>
|
||||||
setBreadcrumb(inventory, source, schedule)
|
setBreadcrumb(inventory, source, schedule)
|
||||||
}
|
}
|
||||||
unifiedJobTemplate={source}
|
resource={source}
|
||||||
loadSchedules={loadSchedules}
|
loadSchedules={loadSchedules}
|
||||||
loadScheduleOptions={loadScheduleOptions}
|
loadScheduleOptions={loadScheduleOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ function Project({ i18n, setBreadcrumb }) {
|
|||||||
}
|
}
|
||||||
}, [project, setBreadcrumb]);
|
}, [project, setBreadcrumb]);
|
||||||
|
|
||||||
function createSchedule(data) {
|
|
||||||
return ProjectsAPI.createSchedule(project.id, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadScheduleOptions = useCallback(() => {
|
const loadScheduleOptions = useCallback(() => {
|
||||||
return ProjectsAPI.readScheduleOptions(project.id);
|
return ProjectsAPI.readScheduleOptions(project.id);
|
||||||
}, [project]);
|
}, [project]);
|
||||||
@@ -188,8 +184,8 @@ function Project({ i18n, setBreadcrumb }) {
|
|||||||
<Route path="/projects/:id/schedules">
|
<Route path="/projects/:id/schedules">
|
||||||
<Schedules
|
<Schedules
|
||||||
setBreadcrumb={setBreadcrumb}
|
setBreadcrumb={setBreadcrumb}
|
||||||
unifiedJobTemplate={project}
|
resource={project}
|
||||||
createSchedule={createSchedule}
|
apiModel={ProjectsAPI}
|
||||||
loadSchedules={loadSchedules}
|
loadSchedules={loadSchedules}
|
||||||
loadScheduleOptions={loadScheduleOptions}
|
loadScheduleOptions={loadScheduleOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import DataListCell from '../../../components/DataListCell';
|
|||||||
import ChipGroup from '../../../components/ChipGroup';
|
import ChipGroup from '../../../components/ChipGroup';
|
||||||
|
|
||||||
const DataListAction = styled(_DataListAction)`
|
const DataListAction = styled(_DataListAction)`
|
||||||
margin-left: 0;
|
&& {
|
||||||
margin-right: 20px;
|
margin-left: 0;
|
||||||
padding-top: 15px;
|
margin-right: 20px;
|
||||||
padding-bottom: 15px;
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
const Button = styled(_Button)`
|
const Button = styled(_Button)`
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -86,10 +101,6 @@ function Template({ i18n, setBreadcrumb }) {
|
|||||||
}
|
}
|
||||||
}, [template, setBreadcrumb]);
|
}, [template, setBreadcrumb]);
|
||||||
|
|
||||||
const createSchedule = data => {
|
|
||||||
return JobTemplatesAPI.createSchedule(template.id, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadScheduleOptions = useCallback(() => {
|
const loadScheduleOptions = useCallback(() => {
|
||||||
return JobTemplatesAPI.readScheduleOptions(templateId);
|
return JobTemplatesAPI.readScheduleOptions(templateId);
|
||||||
}, [templateId]);
|
}, [templateId]);
|
||||||
@@ -203,11 +214,13 @@ function Template({ i18n, setBreadcrumb }) {
|
|||||||
path="/templates/:templateType/:id/schedules"
|
path="/templates/:templateType/:id/schedules"
|
||||||
>
|
>
|
||||||
<Schedules
|
<Schedules
|
||||||
createSchedule={createSchedule}
|
apiModel={JobTemplatesAPI}
|
||||||
setBreadcrumb={setBreadcrumb}
|
setBreadcrumb={setBreadcrumb}
|
||||||
unifiedJobTemplate={template}
|
resource={template}
|
||||||
loadSchedules={loadSchedules}
|
loadSchedules={loadSchedules}
|
||||||
loadScheduleOptions={loadScheduleOptions}
|
loadScheduleOptions={loadScheduleOptions}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
|
launchConfig={launchConfig}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{canSeeNotificationsTab && (
|
{canSeeNotificationsTab && (
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -73,10 +91,6 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
|
|||||||
loadTemplateAndRoles();
|
loadTemplateAndRoles();
|
||||||
}, [loadTemplateAndRoles, location.pathname]);
|
}, [loadTemplateAndRoles, location.pathname]);
|
||||||
|
|
||||||
const createSchedule = data => {
|
|
||||||
return WorkflowJobTemplatesAPI.createSchedule(templateId, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadScheduleOptions = useCallback(() => {
|
const loadScheduleOptions = useCallback(() => {
|
||||||
return WorkflowJobTemplatesAPI.readScheduleOptions(templateId);
|
return WorkflowJobTemplatesAPI.readScheduleOptions(templateId);
|
||||||
}, [templateId]);
|
}, [templateId]);
|
||||||
@@ -206,11 +220,13 @@ function WorkflowJobTemplate({ i18n, setBreadcrumb }) {
|
|||||||
path="/templates/:templateType/:id/schedules"
|
path="/templates/:templateType/:id/schedules"
|
||||||
>
|
>
|
||||||
<Schedules
|
<Schedules
|
||||||
createSchedule={createSchedule}
|
apiModel={WorkflowJobTemplatesAPI}
|
||||||
setBreadcrumb={setBreadcrumb}
|
setBreadcrumb={setBreadcrumb}
|
||||||
unifiedJobTemplate={template}
|
resource={template}
|
||||||
loadSchedules={loadSchedules}
|
loadSchedules={loadSchedules}
|
||||||
loadScheduleOptions={loadScheduleOptions}
|
loadScheduleOptions={loadScheduleOptions}
|
||||||
|
surveyConfig={surveyConfig}
|
||||||
|
launchConfig={launchConfig}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user