diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
index 7fbcd63cfa..3d02eb43bd 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
@@ -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,
};
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
index f39d413fac..4d63fcc663 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
@@ -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 (
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
index 5f3886e20b..a43962bd76 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx
@@ -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 */
diff --git a/awx/ui_next/src/components/Schedule/Schedule.test.jsx b/awx/ui_next/src/components/Schedule/Schedule.test.jsx
index f0f58c0710..e3c394cc95 100644
--- a/awx/ui_next/src/components/Schedule/Schedule.test.jsx
+++ b/awx/ui_next/src/components/Schedule/Schedule.test.jsx
@@ -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('
', () => {
let wrapper;
let history;
diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
index 64b8863fe6..4c63669590 100644
--- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
+++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx
@@ -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
;
@@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) {
{i18n._(t`Prompted Fields`)}
-
- {inventory && summary_fields.inventory && (
+ {ask_job_type_on_launch && (
+
+ )}
+ {ask_inventory_on_launch && (
)}
-
-
- {typeof diff_mode === 'boolean' && (
+ {ask_scm_branch_on_launch && (
+
+ )}
+ {ask_limit_on_launch && (
+
+ )}
+ {ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && (
)}
- {credentials && credentials.length > 0 && (
+ {ask_credential_on_launch && (
)}
- {job_tags && job_tags.length > 0 && (
+ {ask_tags_on_launch && job_tags && job_tags.length > 0 && (
)}
- {skip_tags && skip_tags.length > 0 && (
+ {ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && (
)}
+ {(ask_variables_on_launch || survey_enabled) && (
+
+ )}
>
)}
diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx
index fe9175c6de..da325174d5 100644
--- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx
+++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx
@@ -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('
', () => {
results: [],
},
});
+ JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
', () => {
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('
', () => {
],
},
});
+ JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts);
const scheduleWithPrompts = {
...schedule,
job_type: 'run',
@@ -161,6 +199,7 @@ describe('
', () => {
limit: 'localhost',
diff_mode: true,
verbosity: 1,
+ extra_data: { foo: 'fii' },
};
await act(async () => {
wrapper = mountWithContexts(
@@ -182,7 +221,6 @@ describe('
', () => {
);
});
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('
', () => {
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('
', () => {
},
})
);
+ JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
', () => {
results: [],
},
});
+ JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(
', () => {
results: [],
},
});
+ JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts);
await act(async () => {
wrapper = mountWithContexts(