From a293a60d5c88bdc9e5aa9de73fb783d7671bd904 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 25 Feb 2021 16:07:55 -0500 Subject: [PATCH] Disable job templates in node modal that are missing inv or project --- .../CheckboxListItem/CheckboxListItem.jsx | 16 +++- .../Modals/NodeModals/NodeModal.test.jsx | 1 + .../NodeTypeStep/JobTemplatesList.jsx | 57 ++++++++++--- .../NodeTypeStep/JobTemplatesList.test.jsx | 80 +++++++++++++++++++ 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx index befaa10674..6ac79cc811 100644 --- a/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx +++ b/awx/ui_next/src/components/CheckboxListItem/CheckboxListItem.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import styled from 'styled-components'; import { DataListItem, DataListItemRow, @@ -9,6 +10,14 @@ import { } from '@patternfly/react-core'; import DataListCell from '../DataListCell'; +const Label = styled.label` + ${({ isDisabled }) => + isDisabled && + ` + opacity: 0.5; + `} +`; + const CheckboxListItem = ({ isDisabled = false, isRadio = false, @@ -32,7 +41,7 @@ const CheckboxListItem = ({ aria-label={`check-action-item-${itemId}`} aria-labelledby={`check-action-item-${itemId}`} checked={isSelected} - disabled={isDisabled} + isDisabled={isDisabled} id={`selected-${itemId}`} isChecked={isSelected} name={name} @@ -42,13 +51,14 @@ const CheckboxListItem = ({ - + , ]} /> diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx index 7bcf1ea6ee..946718079a 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx @@ -94,6 +94,7 @@ const mockJobTemplate = { }, related: { webhook_receiver: '' }, inventory: 1, + project: 5, }; describe('NodeModal', () => { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx index 3de9f280b5..77a6de336b 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.jsx @@ -3,6 +3,7 @@ import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { func, shape } from 'prop-types'; +import { Tooltip } from '@patternfly/react-core'; import { JobTemplatesAPI } from '../../../../../../api'; import { getQSConfig, parseQueryString } from '../../../../../../util/qs'; import useRequest from '../../../../../../util/useRequest'; @@ -56,26 +57,56 @@ function JobTemplatesList({ i18n, nodeResource, onUpdateNodeResource }) { fetchJobTemplates(); }, [fetchJobTemplates]); + const onSelectRow = row => { + if ( + row.project && + row.project !== null && + ((row.inventory && row.inventory !== null) || row.ask_inventory_on_launch) + ) { + onUpdateNodeResource(row); + } + }; + return ( onUpdateNodeResource(row)} + onRowClick={row => onSelectRow(row)} qsConfig={QS_CONFIG} - renderItem={item => ( - onUpdateNodeResource(item)} - onDeselect={() => onUpdateNodeResource(null)} - isRadio - /> - )} + renderItem={item => { + const isDisabled = + !item.project || + item.project === null || + ((!item.inventory || item.inventory === null) && + !item.ask_inventory_on_launch); + const listItem = ( + onSelectRow(item)} + onDeselect={() => onUpdateNodeResource(null)} + isRadio + /> + ); + return isDisabled ? ( + + {listItem} + + ) : ( + listItem + ); + }} renderToolbar={props => } showPageSizeOptions={false} toolbarSearchColumns={[ diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx index 3b9fdec0e9..e59b9a7b48 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx @@ -16,6 +16,7 @@ const onUpdateNodeResource = jest.fn(); describe('JobTemplatesList', () => { let wrapper; afterEach(() => { + jest.clearAllMocks(); wrapper.unmount(); }); test('Row selected when nodeResource id matches row id and clicking new row makes expected callback', async () => { @@ -28,12 +29,16 @@ describe('JobTemplatesList', () => { name: 'Test Job Template', type: 'job_template', url: '/api/v2/job_templates/1', + inventory: 1, + project: 2, }, { id: 2, name: 'Test Job Template 2', type: 'job_template', url: '/api/v2/job_templates/2', + inventory: 1, + project: 2, }, ], }, @@ -60,10 +65,18 @@ describe('JobTemplatesList', () => { wrapper.find('CheckboxListItem[name="Test Job Template"]').props() .isSelected ).toBe(true); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template"]').props() + .isDisabled + ).toBe(false); expect( wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props() .isSelected ).toBe(false); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props() + .isDisabled + ).toBe(false); wrapper .find('CheckboxListItem[name="Test Job Template 2"]') .simulate('click'); @@ -72,8 +85,75 @@ describe('JobTemplatesList', () => { name: 'Test Job Template 2', type: 'job_template', url: '/api/v2/job_templates/2', + inventory: 1, + project: 2, }); }); + test('Row disabled when job template missing inventory or project', async () => { + JobTemplatesAPI.read.mockResolvedValueOnce({ + data: { + count: 2, + results: [ + { + id: 1, + name: 'Test Job Template', + type: 'job_template', + url: '/api/v2/job_templates/1', + inventory: 1, + project: null, + ask_inventory_on_launch: false, + }, + { + id: 2, + name: 'Test Job Template 2', + type: 'job_template', + url: '/api/v2/job_templates/2', + inventory: null, + project: 2, + ask_inventory_on_launch: false, + }, + ], + }, + }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template"]').props() + .isSelected + ).toBe(true); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template"]').props() + .isDisabled + ).toBe(true); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props() + .isSelected + ).toBe(false); + expect( + wrapper.find('CheckboxListItem[name="Test Job Template 2"]').props() + .isDisabled + ).toBe(true); + wrapper + .find('CheckboxListItem[name="Test Job Template 2"]') + .simulate('click'); + expect(onUpdateNodeResource).not.toHaveBeenCalled(); + }); test('Error shown when read() request errors', async () => { JobTemplatesAPI.read.mockRejectedValue(new Error()); JobTemplatesAPI.readOptions.mockResolvedValue({