mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 15:09:32 -02:30
Adds extra variables to schedule details. Updates parameters by which we display prompt fields on schedule details. Extend VariableDetails component to be able to handle values that come in raw JSON form.
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user