mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03:30
Merge pull request #7921 from mabashian/6172-schedule-detail-vars
Adds extra variables to schedule details Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
5248ac4498
@ -1,10 +1,15 @@
|
||||
import 'styled-components/macro';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { string, node, number } from 'prop-types';
|
||||
import { node, number, oneOfType, shape, string } from 'prop-types';
|
||||
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
||||
import { DetailName, DetailValue } from '../DetailList';
|
||||
import MultiButtonToggle from '../MultiButtonToggle';
|
||||
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
|
||||
import {
|
||||
yamlToJson,
|
||||
jsonToYaml,
|
||||
isJsonObject,
|
||||
isJsonString,
|
||||
} from '../../util/yaml';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import { JSON_MODE, YAML_MODE } from './constants';
|
||||
|
||||
@ -15,7 +20,7 @@ function getValueAsMode(value, mode) {
|
||||
}
|
||||
return '---';
|
||||
}
|
||||
const modeMatches = isJson(value) === (mode === JSON_MODE);
|
||||
const modeMatches = isJsonString(value) === (mode === JSON_MODE);
|
||||
if (modeMatches) {
|
||||
return value;
|
||||
}
|
||||
@ -23,12 +28,21 @@ function getValueAsMode(value, mode) {
|
||||
}
|
||||
|
||||
function VariablesDetail({ value, label, rows, fullHeight }) {
|
||||
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
|
||||
const [currentValue, setCurrentValue] = useState(value || '---');
|
||||
const [mode, setMode] = useState(
|
||||
isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE
|
||||
);
|
||||
const [currentValue, setCurrentValue] = useState(
|
||||
isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---'
|
||||
);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(getValueAsMode(value, mode));
|
||||
setCurrentValue(
|
||||
getValueAsMode(
|
||||
isJsonObject(value) ? JSON.stringify(value, null, 2) : value,
|
||||
mode
|
||||
)
|
||||
);
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [value]);
|
||||
|
||||
@ -95,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) {
|
||||
);
|
||||
}
|
||||
VariablesDetail.propTypes = {
|
||||
value: string.isRequired,
|
||||
value: oneOfType([shape({}), string]).isRequired,
|
||||
label: node.isRequired,
|
||||
rows: number,
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ import styled from 'styled-components';
|
||||
import { Split, SplitItem } from '@patternfly/react-core';
|
||||
import { CheckboxField, FieldTooltip } from '../FormField';
|
||||
import MultiButtonToggle from '../MultiButtonToggle';
|
||||
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
|
||||
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import { JSON_MODE, YAML_MODE } from './constants';
|
||||
|
||||
@ -30,7 +30,9 @@ function VariablesField({
|
||||
tooltip,
|
||||
}) {
|
||||
const [field, meta, helpers] = useField(name);
|
||||
const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE);
|
||||
const [mode, setMode] = useState(
|
||||
isJsonString(field.value) ? JSON_MODE : YAML_MODE
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pf-c-form__group">
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { string, func, bool, number } from 'prop-types';
|
||||
import { Split, SplitItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
|
||||
import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml';
|
||||
import MultiButtonToggle from '../MultiButtonToggle';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import { JSON_MODE, YAML_MODE } from './constants';
|
||||
@ -18,11 +18,11 @@ const SplitItemRight = styled(SplitItem)`
|
||||
function VariablesInput(props) {
|
||||
const { id, label, readOnly, rows, error, onError, className } = props;
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
const defaultValue = isJson(props.value)
|
||||
const defaultValue = isJsonString(props.value)
|
||||
? formatJson(props.value)
|
||||
: props.value;
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
|
||||
const [mode, setMode] = useState(isJsonString(value) ? JSON_MODE : YAML_MODE);
|
||||
const isControlled = !!props.onChange;
|
||||
/* eslint-enable react/destructuring-assignment */
|
||||
|
||||
|
||||
@ -6,10 +6,12 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { SchedulesAPI } from '../../api';
|
||||
import { JobTemplatesAPI, SchedulesAPI } from '../../api';
|
||||
import Schedule from './Schedule';
|
||||
|
||||
jest.mock('../../api/models/JobTemplates');
|
||||
jest.mock('../../api/models/Schedules');
|
||||
jest.mock('../../api/models/WorkflowJobTemplates');
|
||||
|
||||
SchedulesAPI.readDetail.mockResolvedValue({
|
||||
data: {
|
||||
@ -62,6 +64,22 @@ SchedulesAPI.readCredentials.mockResolvedValue({
|
||||
},
|
||||
});
|
||||
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
ask_credential_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_inventory_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
survey_enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
describe('<Schedule />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
@ -17,10 +17,15 @@ import ScheduleOccurrences from '../ScheduleOccurrences';
|
||||
import ScheduleToggle from '../ScheduleToggle';
|
||||
import { formatDateString } from '../../../util/dates';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
SchedulesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '../../../api';
|
||||
import DeleteButton from '../../DeleteButton';
|
||||
import ErrorDetail from '../../ErrorDetail';
|
||||
import ChipGroup from '../../ChipGroup';
|
||||
import { VariablesDetail } from '../../CodeMirrorInput';
|
||||
|
||||
const PromptTitle = styled(Title)`
|
||||
--pf-c-title--m-md--FontWeight: 700;
|
||||
@ -35,9 +40,9 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
diff_mode,
|
||||
dtend,
|
||||
dtstart,
|
||||
extra_data,
|
||||
job_tags,
|
||||
job_type,
|
||||
inventory,
|
||||
limit,
|
||||
modified,
|
||||
name,
|
||||
@ -67,20 +72,47 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
|
||||
const {
|
||||
result: [credentials, preview],
|
||||
result: [credentials, preview, launchData],
|
||||
isLoading,
|
||||
error: readContentError,
|
||||
request: fetchCredentialsAndPreview,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [{ data }, { data: schedulePreview }] = await Promise.all([
|
||||
const promises = [
|
||||
SchedulesAPI.readCredentials(id),
|
||||
SchedulesAPI.createPreview({
|
||||
rrule,
|
||||
}),
|
||||
]);
|
||||
return [data.results, schedulePreview];
|
||||
}, [id, rrule]),
|
||||
];
|
||||
|
||||
if (
|
||||
schedule?.summary_fields?.unified_job_template?.unified_job_type ===
|
||||
'job'
|
||||
) {
|
||||
promises.push(
|
||||
JobTemplatesAPI.readLaunch(
|
||||
schedule.summary_fields.unified_job_template.id
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
schedule?.summary_fields?.unified_job_template?.unified_job_type ===
|
||||
'workflow_job'
|
||||
) {
|
||||
promises.push(
|
||||
WorkflowJobTemplatesAPI.readLaunch(
|
||||
schedule.summary_fields.unified_job_template.id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
promises.push(Promise.resolve());
|
||||
}
|
||||
|
||||
const [{ data }, { data: schedulePreview }, launch] = await Promise.all(
|
||||
promises
|
||||
);
|
||||
|
||||
return [data.results, schedulePreview, launch?.data];
|
||||
}, [id, schedule, rrule]),
|
||||
[]
|
||||
);
|
||||
|
||||
@ -93,15 +125,33 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
rule.options.freq === RRule.MINUTELY && dtstart === dtend
|
||||
? i18n._(t`None (Run Once)`)
|
||||
: rule.toText().replace(/^\w/, c => c.toUpperCase());
|
||||
|
||||
const {
|
||||
ask_credential_on_launch,
|
||||
ask_diff_mode_on_launch,
|
||||
ask_inventory_on_launch,
|
||||
ask_job_type_on_launch,
|
||||
ask_limit_on_launch,
|
||||
ask_scm_branch_on_launch,
|
||||
ask_skip_tags_on_launch,
|
||||
ask_tags_on_launch,
|
||||
ask_variables_on_launch,
|
||||
ask_verbosity_on_launch,
|
||||
survey_enabled,
|
||||
} = launchData || {};
|
||||
|
||||
const showPromptedFields =
|
||||
(credentials && credentials.length > 0) ||
|
||||
job_type ||
|
||||
(inventory && summary_fields.inventory) ||
|
||||
scm_branch ||
|
||||
limit ||
|
||||
typeof diff_mode === 'boolean' ||
|
||||
(job_tags && job_tags.length > 0) ||
|
||||
(skip_tags && skip_tags.length > 0);
|
||||
ask_credential_on_launch ||
|
||||
ask_diff_mode_on_launch ||
|
||||
ask_inventory_on_launch ||
|
||||
ask_job_type_on_launch ||
|
||||
ask_limit_on_launch ||
|
||||
ask_scm_branch_on_launch ||
|
||||
ask_skip_tags_on_launch ||
|
||||
ask_tags_on_launch ||
|
||||
ask_variables_on_launch ||
|
||||
ask_verbosity_on_launch ||
|
||||
survey_enabled;
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
<PromptTitle headingLevel="h2">
|
||||
{i18n._(t`Prompted Fields`)}
|
||||
</PromptTitle>
|
||||
<Detail label={i18n._(t`Job Type`)} value={job_type} />
|
||||
{inventory && summary_fields.inventory && (
|
||||
{ask_job_type_on_launch && (
|
||||
<Detail label={i18n._(t`Job Type`)} value={job_type} />
|
||||
)}
|
||||
{ask_inventory_on_launch && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={
|
||||
@ -161,18 +213,22 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Source Control Branch`)}
|
||||
value={scm_branch}
|
||||
/>
|
||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||
{typeof diff_mode === 'boolean' && (
|
||||
{ask_scm_branch_on_launch && (
|
||||
<Detail
|
||||
label={i18n._(t`Source Control Branch`)}
|
||||
value={scm_branch}
|
||||
/>
|
||||
)}
|
||||
{ask_limit_on_launch && (
|
||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||
)}
|
||||
{ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && (
|
||||
<Detail
|
||||
label={i18n._(t`Show Changes`)}
|
||||
value={diff_mode ? 'On' : 'Off'}
|
||||
/>
|
||||
)}
|
||||
{credentials && credentials.length > 0 && (
|
||||
{ask_credential_on_launch && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Credentials`)}
|
||||
@ -185,7 +241,7 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{job_tags && job_tags.length > 0 && (
|
||||
{ask_tags_on_launch && job_tags && job_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Job Tags`)}
|
||||
@ -203,7 +259,7 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{skip_tags && skip_tags.length > 0 && (
|
||||
{ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Skip Tags`)}
|
||||
@ -221,6 +277,13 @@ function ScheduleDetail({ schedule, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(ask_variables_on_launch || survey_enabled) && (
|
||||
<VariablesDetail
|
||||
value={extra_data}
|
||||
rows={4}
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DetailList>
|
||||
|
||||
@ -2,14 +2,48 @@ import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { SchedulesAPI } from '../../../api';
|
||||
import { SchedulesAPI, JobTemplatesAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import ScheduleDetail from './ScheduleDetail';
|
||||
|
||||
jest.mock('../../../api/models/JobTemplates');
|
||||
jest.mock('../../../api/models/Schedules');
|
||||
jest.mock('../../../api/models/WorkflowJobTemplates');
|
||||
|
||||
const allPrompts = {
|
||||
data: {
|
||||
ask_credential_on_launch: true,
|
||||
ask_diff_mode_on_launch: true,
|
||||
ask_inventory_on_launch: true,
|
||||
ask_job_type_on_launch: true,
|
||||
ask_limit_on_launch: true,
|
||||
ask_scm_branch_on_launch: true,
|
||||
ask_skip_tags_on_launch: true,
|
||||
ask_tags_on_launch: true,
|
||||
ask_variables_on_launch: true,
|
||||
ask_verbosity_on_launch: true,
|
||||
survey_enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
const noPrompts = {
|
||||
data: {
|
||||
ask_credential_on_launch: false,
|
||||
ask_diff_mode_on_launch: false,
|
||||
ask_inventory_on_launch: false,
|
||||
ask_job_type_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_scm_branch_on_launch: false,
|
||||
ask_skip_tags_on_launch: false,
|
||||
ask_tags_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_verbosity_on_launch: false,
|
||||
survey_enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
const schedule = {
|
||||
url: '/api/v2/schedules/1',
|
||||
@ -53,6 +87,7 @@ const schedule = {
|
||||
dtstart: '2020-03-16T04:00:00Z',
|
||||
dtend: '2020-07-06T04:00:00Z',
|
||||
next_run: '2020-03-16T04:00:00Z',
|
||||
extra_data: {},
|
||||
};
|
||||
|
||||
SchedulesAPI.createPreview.mockResolvedValue({
|
||||
@ -79,6 +114,7 @@ describe('<ScheduleDetail />', () => {
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
@ -134,6 +170,7 @@ describe('<ScheduleDetail />', () => {
|
||||
expect(wrapper.find('Detail[label="Credentials"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0);
|
||||
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0);
|
||||
expect(wrapper.find('VariablesDetail').length).toBe(0);
|
||||
});
|
||||
test('details should render with the proper values with prompts', async () => {
|
||||
SchedulesAPI.readCredentials.mockResolvedValue({
|
||||
@ -151,6 +188,7 @@ describe('<ScheduleDetail />', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts);
|
||||
const scheduleWithPrompts = {
|
||||
...schedule,
|
||||
job_type: 'run',
|
||||
@ -161,6 +199,7 @@ describe('<ScheduleDetail />', () => {
|
||||
limit: 'localhost',
|
||||
diff_mode: true,
|
||||
verbosity: 1,
|
||||
extra_data: { foo: 'fii' },
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
@ -182,7 +221,6 @@ describe('<ScheduleDetail />', () => {
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
// await waitForElement(wrapper, 'Title', el => el.length > 0);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Detail[label="Name"]')
|
||||
@ -231,6 +269,7 @@ describe('<ScheduleDetail />', () => {
|
||||
expect(wrapper.find('Detail[label="Credentials"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(1);
|
||||
expect(wrapper.find('VariablesDetail').length).toBe(1);
|
||||
});
|
||||
test('error shown when error encountered fetching credentials', async () => {
|
||||
SchedulesAPI.readCredentials.mockRejectedValueOnce(
|
||||
@ -245,6 +284,7 @@ describe('<ScheduleDetail />', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
@ -274,6 +314,7 @@ describe('<ScheduleDetail />', () => {
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
@ -313,6 +354,7 @@ describe('<ScheduleDetail />', () => {
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Route
|
||||
|
||||
@ -22,7 +22,11 @@ export function jsonToYaml(jsonString) {
|
||||
return yaml.safeDump(value);
|
||||
}
|
||||
|
||||
export function isJson(jsonString) {
|
||||
export function isJsonObject(value) {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
export function isJsonString(jsonString) {
|
||||
if (typeof jsonString !== 'string') {
|
||||
return false;
|
||||
}
|
||||
@ -40,7 +44,7 @@ export function parseVariableField(variableField) {
|
||||
if (variableField === '---' || variableField === '{}') {
|
||||
return {};
|
||||
}
|
||||
if (!isJson(variableField)) {
|
||||
if (!isJsonString(variableField)) {
|
||||
variableField = yamlToJson(variableField);
|
||||
}
|
||||
variableField = JSON.parse(variableField);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user