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(