From 222a65c8753ce6b2552d6c0e4449545c6f2adf1c Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 17 Aug 2020 16:49:15 -0400 Subject: [PATCH] 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. --- .../CodeMirrorInput/VariablesDetail.jsx | 28 +++-- .../CodeMirrorInput/VariablesField.jsx | 6 +- .../CodeMirrorInput/VariablesInput.jsx | 6 +- .../src/components/Schedule/Schedule.test.jsx | 20 ++- .../ScheduleDetail/ScheduleDetail.jsx | 115 ++++++++++++++---- .../ScheduleDetail/ScheduleDetail.test.jsx | 46 ++++++- awx/ui_next/src/util/yaml.js | 8 +- 7 files changed, 186 insertions(+), 43 deletions(-) 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(