diff --git a/awx/ui_next/src/components/Schedule/Schedule.jsx b/awx/ui_next/src/components/Schedule/Schedule.jsx index 7974ba072b..e1e59c5d85 100644 --- a/awx/ui_next/src/components/Schedule/Schedule.jsx +++ b/awx/ui_next/src/components/Schedule/Schedule.jsx @@ -19,7 +19,13 @@ import ScheduleEdit from './ScheduleEdit'; import { SchedulesAPI } from '../../api'; import useRequest from '../../util/useRequest'; -function Schedule({ i18n, setBreadcrumb, resource }) { +function Schedule({ + i18n, + setBreadcrumb, + resource, + launchConfig, + surveyConfig, +}) { const { scheduleId } = useParams(); const { pathname } = useLocation(); @@ -100,13 +106,18 @@ function Schedule({ i18n, setBreadcrumb, resource }) { /> {schedule && [ - + , - + , ]} diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx index 1fd59a91f7..81f42f5b72 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.jsx @@ -15,14 +15,18 @@ import mergeExtraVars from '../../../util/prompt/mergeExtraVars'; import getSurveyValues from '../../../util/prompt/getSurveyValues'; import { getAddedAndRemoved } from '../../../util/lists'; -function ScheduleAdd({ i18n, resource, apiModel }) { +function ScheduleAdd({ i18n, resource, apiModel, launchConfig, surveyConfig }) { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); const location = useLocation(); const { pathname } = location; const pathRoot = pathname.substr(0, pathname.indexOf('schedules')); - const handleSubmit = async (values, launchConfig, surveyConfig) => { + const handleSubmit = async ( + values, + launchConfiguration, + surveyConfiguration + ) => { const { inventory, extra_vars, @@ -51,8 +55,9 @@ function ScheduleAdd({ i18n, resource, apiModel }) { let extraVars; const surveyValues = getSurveyValues(values); const initialExtraVars = - launchConfig?.ask_variables_on_launch && (values.extra_vars || '---'); - if (surveyConfig?.spec) { + launchConfiguration?.ask_variables_on_launch && + (values.extra_vars || '---'); + if (surveyConfiguration?.spec) { extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues)); } else { extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {})); @@ -92,6 +97,8 @@ function ScheduleAdd({ i18n, resource, apiModel }) { handleCancel={() => history.push(`${pathRoot}schedules`)} handleSubmit={handleSubmit} submitError={formSubmitError} + launchConfig={launchConfig} + surveyConfig={surveyConfig} resource={resource} /> diff --git a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx index 41f6b02d1d..970bb91476 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleAdd/ScheduleAdd.test.jsx @@ -19,45 +19,45 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({ }, ], }); -JobTemplatesAPI.readLaunch.mockResolvedValue({ - data: { - can_start_without_user_input: false, - passwords_needed_to_start: [], - ask_scm_branch_on_launch: false, - ask_variables_on_launch: false, - ask_tags_on_launch: false, - ask_diff_mode_on_launch: false, - ask_skip_tags_on_launch: false, - ask_job_type_on_launch: false, - ask_limit_on_launch: false, - ask_verbosity_on_launch: false, - ask_inventory_on_launch: true, - ask_credential_on_launch: false, - survey_enabled: false, - variables_needed_to_start: [], - credential_needed_to_start: false, - inventory_needed_to_start: true, - job_template_data: { - name: 'Demo Job Template', - id: 7, - description: '', - }, - defaults: { - extra_vars: '---', - diff_mode: false, - limit: '', - job_tags: '', - skip_tags: '', - job_type: 'run', - verbosity: 0, - inventory: { - name: null, - id: null, - }, - scm_branch: '', - }, + +const launchConfig = { + can_start_without_user_input: false, + passwords_needed_to_start: [], + ask_scm_branch_on_launch: false, + ask_variables_on_launch: false, + ask_tags_on_launch: false, + ask_diff_mode_on_launch: false, + ask_skip_tags_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_verbosity_on_launch: false, + ask_inventory_on_launch: true, + ask_credential_on_launch: false, + survey_enabled: false, + variables_needed_to_start: [], + credential_needed_to_start: false, + inventory_needed_to_start: true, + job_template_data: { + name: 'Demo Job Template', + id: 7, + description: '', }, -}); + defaults: { + extra_vars: '---', + diff_mode: false, + limit: '', + job_tags: '', + skip_tags: '', + job_type: 'run', + verbosity: 0, + inventory: { + name: null, + id: null, + }, + scm_branch: '', + }, +}; + JobTemplatesAPI.createSchedule.mockResolvedValue({ data: { id: 3 } }); let wrapper; @@ -74,6 +74,7 @@ describe('', () => { inventory: 2, summary_fields: { credentials: [] }, }} + launchConfig={launchConfig} /> ); }); @@ -377,7 +378,6 @@ describe('', () => { ); wrapper.update(); expect(wrapper.find('Wizard').length).toBe(0); - // console.log(wrapper.debug()); await act(async () => { wrapper.find('Formik').invoke('onSubmit')({ name: 'Schedule', diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index 19ea0495c5..c9e4f17fa4 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -42,7 +42,7 @@ const PromptDetailList = styled(DetailList)` padding: 0px 20px; `; -function ScheduleDetail({ schedule, i18n }) { +function ScheduleDetail({ schedule, i18n, surveyConfig }) { const { id, created, @@ -148,6 +148,7 @@ function ScheduleDetail({ schedule, i18n }) { const { ask_credential_on_launch, + inventory_needed_to_start, ask_diff_mode_on_launch, ask_inventory_on_launch, ask_job_type_on_launch, @@ -160,6 +161,41 @@ function ScheduleDetail({ schedule, i18n }) { survey_enabled, } = launchData || {}; + const missingRequiredInventory = () => { + if (!inventory_needed_to_start || schedule?.summary_fields?.inventory?.id) { + return false; + } + return true; + }; + + const hasMissingSurveyValue = () => { + let missingValues = false; + if (survey_enabled) { + surveyConfig.spec.forEach(question => { + const hasDefaultValue = Boolean(question.default); + if (question.required && !hasDefaultValue) { + const extraDataKeys = Object.keys(schedule?.extra_data); + + const hasMatchingKey = extraDataKeys.includes(question.variable); + Object.values(schedule?.extra_data).forEach(value => { + if (!value || !hasMatchingKey) { + missingValues = true; + } else { + missingValues = false; + } + }); + if (!Object.values(schedule.extra_data).length) { + missingValues = true; + } + } + }); + } + return missingValues; + }; + const isDisabled = Boolean( + missingRequiredInventory() || hasMissingSurveyValue() + ); + const showCredentialsDetail = ask_credential_on_launch && credentials.length > 0; const showInventoryDetail = ask_inventory_on_launch && inventory; @@ -189,14 +225,6 @@ function ScheduleDetail({ schedule, i18n }) { showVerbosityDetail || showVariablesDetail; - const VERBOSITY = { - 0: i18n._(t`0 (Normal)`), - 1: i18n._(t`1 (Verbose)`), - 2: i18n._(t`2 (More Verbose)`), - 3: i18n._(t`3 (Debug)`), - 4: i18n._(t`4 (Connection Debug)`), - }; - if (isLoading) { return ; } @@ -207,7 +235,11 @@ function ScheduleDetail({ schedule, i18n }) { return ( - + @@ -279,12 +311,6 @@ function ScheduleDetail({ schedule, i18n }) { {ask_limit_on_launch && ( )} - {ask_verbosity_on_launch && ( - - )} {showDiffModeDetail && ( ', () => { ); expect(SchedulesAPI.destroy).toHaveBeenCalledTimes(1); }); + test('should have disabled toggle', async () => { + SchedulesAPI.readCredentials.mockResolvedValueOnce({ + data: { + count: 0, + results: [], + }, + }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts); + await act(async () => { + wrapper = mountWithContexts( + ( + + )} + />, + { + context: { + router: { + history, + route: { + location: history.location, + match: { params: { id: 1 } }, + }, + }, + }, + } + ); + }); + await waitForElement( + wrapper, + 'ScheduleToggle', + el => el.prop('isDisabled') === true + ); + }); }); diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx index e5faf4dbeb..9f11e8676b 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.jsx @@ -15,7 +15,13 @@ import { parseVariableField } from '../../../util/yaml'; import mergeExtraVars from '../../../util/prompt/mergeExtraVars'; import getSurveyValues from '../../../util/prompt/getSurveyValues'; -function ScheduleEdit({ i18n, schedule, resource }) { +function ScheduleEdit({ + i18n, + schedule, + resource, + launchConfig, + surveyConfig, +}) { const [formSubmitError, setFormSubmitError] = useState(null); const history = useHistory(); const location = useLocation(); @@ -24,8 +30,8 @@ function ScheduleEdit({ i18n, schedule, resource }) { const handleSubmit = async ( values, - launchConfig, - surveyConfig, + launchConfiguration, + surveyConfiguration, scheduleCredentials = [] ) => { const { @@ -36,32 +42,40 @@ function ScheduleEdit({ i18n, schedule, resource }) { interval, startDateTime, timezone, - occurrences, + occurences, runOn, runOnTheDay, runOnTheMonth, runOnDayMonth, runOnDayNumber, endDateTime, - runOnTheOccurrence, + runOnTheOccurence, daysOfWeek, ...submitValues } = values; const { added, removed } = getAddedAndRemoved( - [...resource?.summary_fields.credentials, ...scheduleCredentials], + [...(resource?.summary_fields.credentials || []), ...scheduleCredentials], credentials ); let extraVars; const surveyValues = getSurveyValues(values); const initialExtraVars = - launchConfig?.ask_variables_on_launch && (values.extra_vars || '---'); - if (surveyConfig?.spec) { + launchConfiguration?.ask_variables_on_launch && + (values.extra_vars || '---'); + if (surveyConfiguration?.spec) { extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, surveyValues)); } else { extraVars = yaml.safeDump(mergeExtraVars(initialExtraVars, {})); } submitValues.extra_data = extraVars && parseVariableField(extraVars); + + if ( + Object.keys(submitValues.extra_data).length === 0 && + Object.keys(schedule.extra_data).length > 0 + ) { + submitValues.extra_data = schedule.extra_data; + } delete values.extra_vars; if (inventory) { submitValues.inventory = inventory.id; @@ -103,6 +117,8 @@ function ScheduleEdit({ i18n, schedule, resource }) { handleSubmit={handleSubmit} submitError={formSubmitError} resource={resource} + launchConfig={launchConfig} + surveyConfig={surveyConfig} /> diff --git a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx index 404f7334cb..c70eccad6a 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleEdit/ScheduleEdit.test.jsx @@ -7,7 +7,6 @@ import { } from '../../../../testUtils/enzymeHelpers'; import { SchedulesAPI, - JobTemplatesAPI, InventoriesAPI, CredentialsAPI, CredentialTypesAPI, @@ -28,46 +27,6 @@ SchedulesAPI.readZoneInfo.mockResolvedValue({ ], }); -JobTemplatesAPI.readLaunch.mockResolvedValue({ - data: { - can_start_without_user_input: false, - passwords_needed_to_start: [], - ask_scm_branch_on_launch: false, - ask_variables_on_launch: false, - ask_tags_on_launch: false, - ask_diff_mode_on_launch: false, - ask_skip_tags_on_launch: false, - ask_job_type_on_launch: false, - ask_limit_on_launch: false, - ask_verbosity_on_launch: false, - ask_inventory_on_launch: true, - ask_credential_on_launch: true, - survey_enabled: false, - variables_needed_to_start: [], - credential_needed_to_start: true, - inventory_needed_to_start: true, - job_template_data: { - name: 'Demo Job Template', - id: 7, - description: '', - }, - defaults: { - extra_vars: '---', - diff_mode: false, - limit: '', - job_tags: '', - skip_tags: '', - job_type: 'run', - verbosity: 0, - inventory: { - name: null, - id: null, - }, - scm_branch: '', - }, - }, -}); - SchedulesAPI.readCredentials.mockResolvedValue({ data: { results: [ @@ -156,6 +115,44 @@ describe('', () => { ], }, }} + launchConfig={{ + can_start_without_user_input: false, + passwords_needed_to_start: [], + ask_scm_branch_on_launch: false, + ask_variables_on_launch: false, + ask_tags_on_launch: false, + ask_diff_mode_on_launch: false, + ask_skip_tags_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_verbosity_on_launch: false, + ask_inventory_on_launch: true, + ask_credential_on_launch: true, + survey_enabled: false, + variables_needed_to_start: [], + credential_needed_to_start: true, + inventory_needed_to_start: true, + job_template_data: { + name: 'Demo Job Template', + id: 7, + description: '', + }, + defaults: { + extra_vars: '---', + diff_mode: false, + limit: '', + job_tags: '', + skip_tags: '', + job_type: 'run', + verbosity: 0, + inventory: { + name: null, + id: null, + }, + scm_branch: '', + }, + }} + surveyConfig={{}} /> ); }); @@ -202,6 +199,7 @@ describe('', () => { description: 'test description', name: 'Run every 10 minutes 10 times', extra_data: {}, + occurrences: 10, rrule: 'DTSTART;TZID=America/New_York:20200325T103000 RRULE:INTERVAL=10;FREQ=MINUTELY;COUNT=10', }); @@ -265,6 +263,7 @@ describe('', () => { description: 'test description', name: 'Run weekly on mon/wed/fri', extra_data: {}, + occurrences: 1, rrule: `DTSTART;TZID=America/New_York:20200325T104500 RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=${RRule.MO},${RRule.WE},${RRule.FR}`, }); }); @@ -287,6 +286,7 @@ describe('', () => { description: 'test description', name: 'Run on the first day of the month', extra_data: {}, + occurrences: 1, rrule: 'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1', }); @@ -312,6 +312,8 @@ describe('', () => { description: 'test description', name: 'Run monthly on the last Tuesday', extra_data: {}, + occurrences: 1, + runOnTheOccurrence: -1, rrule: 'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU', }); @@ -336,6 +338,7 @@ describe('', () => { description: 'test description', name: 'Yearly on the first day of March', extra_data: {}, + occurrences: 1, rrule: 'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1', }); @@ -361,6 +364,8 @@ describe('', () => { description: 'test description', name: 'Yearly on the second Friday in April', extra_data: {}, + occurrences: 1, + runOnTheOccurrence: 2, rrule: 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4', }); @@ -386,6 +391,8 @@ describe('', () => { description: 'test description', name: 'Yearly on the first weekday in October', extra_data: {}, + occurrences: 1, + runOnTheOccurrence: 1, rrule: 'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10', }); @@ -515,6 +522,8 @@ describe('', () => { expect(SchedulesAPI.update).toBeCalledWith(27, { extra_data: {}, name: 'mock schedule', + occurrences: 1, + runOnTheOccurrence: 1, rrule: 'DTSTART;TZID=America/New_York:20210128T141500 RRULE:COUNT=1;FREQ=MINUTELY', skip_tags: '', @@ -590,8 +599,10 @@ describe('', () => { expect(SchedulesAPI.update).toBeCalledWith(27, { description: '', extra_data: {}, - inventory: 702, + occurrences: 1, + runOnTheOccurrence: 1, name: 'foo', + inventory: 702, rrule: 'DTSTART;TZID=America/New_York:20200402T144500 RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY', }); diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx index 9bdef2c437..7e5f7d9805 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.jsx @@ -24,6 +24,9 @@ function ScheduleList({ loadSchedules, loadScheduleOptions, hideAddButton, + resource, + launchConfig, + surveyConfig, }) { const [selected, setSelected] = useState([]); @@ -114,6 +117,47 @@ function ScheduleList({ actions && Object.prototype.hasOwnProperty.call(actions, 'POST') && !hideAddButton; + const isTemplate = + resource?.type === 'workflow_job_template' || + resource?.type === 'job_template'; + + const missingRequiredInventory = schedule => { + if ( + !launchConfig.inventory_needed_to_start || + schedule?.summary_fields?.inventory?.id + ) { + return null; + } + return i18n._(t`This schedule is missing an Inventory`); + }; + + const hasMissingSurveyValue = schedule => { + let missingValues; + if (launchConfig.survey_enabled) { + surveyConfig.spec.forEach(question => { + const hasDefaultValue = Boolean(question.default); + if (question.required && !hasDefaultValue) { + const extraDataKeys = Object.keys(schedule?.extra_data); + + const hasMatchingKey = extraDataKeys.includes(question.variable); + Object.values(schedule?.extra_data).forEach(value => { + if (!value || !hasMatchingKey) { + missingValues = true; + } else { + missingValues = false; + } + }); + if (!Object.values(schedule.extra_data).length) { + missingValues = true; + } + } + }); + } + return ( + missingValues && + i18n._(t`This schedule is missing required survey values`) + ); + }; return ( <> @@ -139,6 +183,8 @@ function ScheduleList({ onSelect={() => handleSelect(item)} schedule={item} rowIndex={index} + isMissingInventory={isTemplate && missingRequiredInventory(item)} + isMissingSurvey={isTemplate && hasMissingSurveyValue(item)} /> )} toolbarSearchColumns={[ diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx index 963a51cfcf..2da9d89d6c 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleList.test.jsx @@ -32,19 +32,22 @@ describe('ScheduleList', () => { }); describe('read call successful', () => { - beforeAll(async () => { + beforeEach(async () => { await act(async () => { wrapper = mountWithContexts( ); }); wrapper.update(); }); - afterAll(() => { + afterEach(() => { wrapper.unmount(); }); @@ -203,6 +206,60 @@ describe('ScheduleList', () => { wrapper.update(); expect(wrapper.find('ToolbarAddButton').length).toBe(0); }); + test('should show missing resource icon and disabled toggle', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper + .find('ScheduleListItem') + .at(4) + .prop('isMissingSurvey') + ).toBe('This schedule is missing required survey values'); + expect(wrapper.find('ExclamationTriangleIcon').length).toBe(5); + expect(wrapper.find('Switch#schedule-5-toggle').prop('isDisabled')).toBe( + true + ); + }); + test('should show missing resource icon and disabled toggle', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + + expect( + wrapper + .find('ScheduleListItem') + .at(3) + .prop('isMissingInventory') + ).toBe('This schedule is missing an Inventory'); + expect(wrapper.find('ExclamationTriangleIcon').length).toBe(4); + expect(wrapper.find('Switch#schedule-3-toggle').prop('isDisabled')).toBe( + true + ); + }); }); describe('read call unsuccessful', () => { diff --git a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx index 6e71a64e18..b642f28638 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleList/ScheduleListItem.jsx @@ -4,16 +4,33 @@ import { bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { Button } from '@patternfly/react-core'; +import { Button, Tooltip } from '@patternfly/react-core'; import { Tr, Td } from '@patternfly/react-table'; -import { PencilAltIcon } from '@patternfly/react-icons'; +import { + PencilAltIcon, + ExclamationTriangleIcon as PFExclamationTriangleIcon, +} from '@patternfly/react-icons'; +import styled from 'styled-components'; import { DetailList, Detail } from '../../DetailList'; import { ActionsTd, ActionItem } from '../../PaginatedTable'; import { ScheduleToggle } from '..'; import { Schedule } from '../../../types'; import { formatDateString } from '../../../util/dates'; -function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { +const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)` + color: #c9190b; + margin-left: 20px; +`; + +function ScheduleListItem({ + i18n, + rowIndex, + isSelected, + onSelect, + schedule, + isMissingInventory, + isMissingSurvey, +}) { const labelId = `check-action-${schedule.id}`; const jobTypeLabels = { @@ -45,6 +62,7 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { default: break; } + const isDisabled = Boolean(isMissingInventory || isMissingSurvey); return ( @@ -61,6 +79,18 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { {schedule.name} + {Boolean(isMissingInventory || isMissingSurvey) && ( + + ( +
{message}
+ ))} + position="right" + > + +
+
+ )} { @@ -80,7 +110,7 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) { )} - + { describe('User has edit permissions', () => { beforeAll(() => { wrapper = mountWithContexts( - - - - -
+ ); }); @@ -118,6 +116,9 @@ describe('ScheduleListItem', () => { .simulate('change'); expect(onSelect).toHaveBeenCalledTimes(1); }); + test('Toggle button is enabled', () => { + expect(wrapper.find('ScheduleToggle').prop('isDisabled')).toBe(false); + }); }); describe('User has read-only permissions', () => { @@ -186,4 +187,35 @@ describe('ScheduleListItem', () => { ).toBe(true); }); }); + describe('schedule has missing prompt data', () => { + beforeAll(() => { + wrapper = mountWithContexts( + + ); + }); + + afterAll(() => { + wrapper.unmount(); + }); + + test('should show missing resource icon', () => { + expect(wrapper.find('ExclamationTriangleIcon').length).toBe(1); + expect(wrapper.find('ScheduleToggle').prop('isDisabled')).toBe(true); + }); + }); }); diff --git a/awx/ui_next/src/components/Schedule/ScheduleToggle/ScheduleToggle.jsx b/awx/ui_next/src/components/Schedule/ScheduleToggle/ScheduleToggle.jsx index cb15696415..cc9d333fa3 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleToggle/ScheduleToggle.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleToggle/ScheduleToggle.jsx @@ -8,7 +8,7 @@ import ErrorDetail from '../../ErrorDetail'; import useRequest from '../../../util/useRequest'; import { SchedulesAPI } from '../../../api'; -function ScheduleToggle({ schedule, onToggle, className, i18n }) { +function ScheduleToggle({ schedule, onToggle, className, i18n, isDisabled }) { const [isEnabled, setIsEnabled] = useState(schedule.enabled); const [showError, setShowError] = useState(false); @@ -55,7 +55,9 @@ function ScheduleToggle({ schedule, onToggle, className, i18n }) { labelOff={i18n._(t`Off`)} isChecked={isEnabled} isDisabled={ - isLoading || !schedule.summary_fields.user_capabilities.edit + isLoading || + !schedule.summary_fields.user_capabilities.edit || + isDisabled } onChange={toggleSchedule} aria-label={i18n._(t`Toggle schedule`)} diff --git a/awx/ui_next/src/components/Schedule/Schedules.jsx b/awx/ui_next/src/components/Schedule/Schedules.jsx index f0daa8989b..22f429dd29 100644 --- a/awx/ui_next/src/components/Schedule/Schedules.jsx +++ b/awx/ui_next/src/components/Schedule/Schedules.jsx @@ -10,6 +10,8 @@ function Schedules({ loadScheduleOptions, loadSchedules, setBreadcrumb, + launchConfig, + surveyConfig, resource, }) { const match = useRouteMatch(); @@ -17,14 +19,27 @@ function Schedules({ return ( - + - + diff --git a/awx/ui_next/src/components/Schedule/data.schedules.json b/awx/ui_next/src/components/Schedule/data.schedules.json index 13ef941811..75b1e15ebf 100644 --- a/awx/ui_next/src/components/Schedule/data.schedules.json +++ b/awx/ui_next/src/components/Schedule/data.schedules.json @@ -8,6 +8,7 @@ "rrule": "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "id": 1, + "extra_data":{}, "summary_fields": { "unified_job_template": { "id": 6, @@ -27,6 +28,7 @@ "rrule": "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "id": 2, + "extra_data":{}, "summary_fields": { "unified_job_template": { "id": 7, @@ -46,6 +48,7 @@ "rrule": "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "id": 3, + "extra_data":{}, "summary_fields": { "unified_job_template": { "id": 8, @@ -65,6 +68,7 @@ "rrule": "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "id": 4, + "extra_data":{}, "summary_fields": { "unified_job_template": { "id": 9, @@ -84,6 +88,7 @@ "rrule": "DTSTART;TZID=America/New_York:20200220T000000 RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", "id": 5, + "extra_data":{"novalue":null}, "summary_fields": { "unified_job_template": { "id": 10, @@ -103,4 +108,4 @@ "next_run": "2020-02-20T05:00:00Z" } ] -} \ No newline at end of file +} diff --git a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx index 35504df3b6..3088996a3d 100644 --- a/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx +++ b/awx/ui_next/src/components/Schedule/shared/ScheduleForm.jsx @@ -12,11 +12,7 @@ import { ActionGroup, } from '@patternfly/react-core'; import { Config } from '../../../contexts/Config'; -import { - SchedulesAPI, - JobTemplatesAPI, - WorkflowJobTemplatesAPI, -} from '../../../api'; +import { SchedulesAPI } from '../../../api'; import AnsibleSelect from '../../AnsibleSelect'; import ContentError from '../../ContentError'; import ContentLoading from '../../ContentLoading'; @@ -194,6 +190,8 @@ function ScheduleForm({ schedule, submitError, resource, + launchConfig, + surveyConfig, ...rest }) { const [isWizardOpen, setIsWizardOpen] = useState(false); @@ -210,37 +208,15 @@ function ScheduleForm({ const isTemplate = resource.type === 'workflow_job_template' || resource.type === 'job_template'; - const isWorkflowJobTemplate = - isTemplate && resource.type === 'workflow_job_template'; - const { request: loadScheduleData, error: contentError, contentLoading, - result: { zoneOptions, surveyConfig, launchConfig, credentials }, + result: { zoneOptions, credentials }, } = useRequest( useCallback(async () => { - const readLaunch = - isTemplate && - (isWorkflowJobTemplate - ? WorkflowJobTemplatesAPI.readLaunch(resource.id) - : JobTemplatesAPI.readLaunch(resource.id)); - const [{ data }, { data: launchConfiguration }] = await Promise.all([ - SchedulesAPI.readZoneInfo(), - readLaunch, - ]); + const { data } = await SchedulesAPI.readZoneInfo(); - const readSurvey = isWorkflowJobTemplate - ? WorkflowJobTemplatesAPI.readSurvey(resource.id) - : JobTemplatesAPI.readSurvey(resource.id); - - let surveyConfiguration = null; - - if (isTemplate && launchConfiguration.survey_enabled) { - const { data: survey } = await readSurvey; - - surveyConfiguration = survey; - } let creds; if (schedule.id) { const { @@ -249,37 +225,6 @@ function ScheduleForm({ creds = results; } - const missingRequiredInventory = Boolean( - !resource.inventory && !schedule?.summary_fields?.inventory.id - ); - let missingRequiredSurvey = false; - - if ( - schedule.id && - isTemplate && - !launchConfiguration?.can_start_without_user_input - ) { - missingRequiredSurvey = surveyConfiguration?.spec?.every(question => { - let hasValue; - if (Object.keys(schedule)?.length === 0) { - hasValue = true; - } - Object.entries(schedule?.extra_data).forEach(([key, value]) => { - if ( - question.required && - question.variable === key && - value.length > 0 - ) { - hasValue = false; - } - }); - return hasValue; - }); - } - if (missingRequiredInventory || missingRequiredSurvey) { - setIsSaveDisabled(true); - } - const zones = data.map(zone => { return { value: zone.name, @@ -290,18 +235,61 @@ function ScheduleForm({ return { zoneOptions: zones, - surveyConfig: surveyConfiguration || {}, - launchConfig: launchConfiguration, credentials: creds || [], }; - }, [isTemplate, isWorkflowJobTemplate, resource, schedule]), + }, [schedule]), { zonesOptions: [], - surveyConfig: {}, - launchConfig: {}, credentials: [], } ); + const missingRequiredInventory = useCallback(() => { + let missingInventory = false; + if ( + launchConfig.inventory_needed_to_start && + !schedule?.summary_fields?.inventory?.id + ) { + missingInventory = true; + } + return missingInventory; + }, [launchConfig, schedule]); + + const hasMissingSurveyValue = useCallback(() => { + let missingValues = false; + if (launchConfig?.survey_enabled) { + surveyConfig.spec.forEach(question => { + const hasDefaultValue = Boolean(question.default); + const hasSchedule = Object.keys(schedule).length; + const isRequired = question.required; + if (isRequired && !hasDefaultValue) { + if (!hasSchedule) { + missingValues = true; + } else { + const hasMatchingKey = Object.keys(schedule?.extra_data).includes( + question.variable + ); + Object.values(schedule?.extra_data).forEach(value => { + if (!value || !hasMatchingKey) { + missingValues = true; + } else { + missingValues = false; + } + }); + if (!Object.values(schedule.extra_data).length) { + missingValues = true; + } + } + } + }); + } + return missingValues; + }, [launchConfig, schedule, surveyConfig]); + + useEffect(() => { + if (isTemplate && (missingRequiredInventory() || hasMissingSurveyValue())) { + setIsSaveDisabled(true); + } + }, [isTemplate, hasMissingSurveyValue, missingRequiredInventory]); useEffect(() => { loadScheduleData(); @@ -532,6 +520,7 @@ function ScheduleForm({ > {i18n._(t`Save`)} + {isTemplate && showPromptButton && (