From d389362ca313dcf5e61f012b9d44f175deb35930 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 15 Jun 2021 14:16:23 -0400 Subject: [PATCH 1/2] renders ad hoc command fields in job detail view --- .../src/screens/Job/JobDetail/JobDetail.js | 4 ++- .../screens/Job/JobDetail/JobDetail.test.js | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.js b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.js index 30d6da5512..62c88047e3 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.js +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.js @@ -76,7 +76,7 @@ function JobDetail({ job }) { project_update: t`Source Control Update`, inventory_update: t`Inventory Sync`, job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`, - ad_hoc_command: t`Command`, + ad_hoc_command: t`Run Command`, system_job: t`Management Job`, workflow_job: t`Workflow Job`, }; @@ -337,6 +337,8 @@ function JobDetail({ job }) { } /> )} + + ', () => { ).toHaveLength(1); }); + test('should display module name and module arguments', () => { + wrapper = mountWithContexts( + + ); + assertDetail('Module Name', 'command'); + assertDetail('Module Arguments', 'echo hello_world'); + assertDetail('Job Type', 'Run Command'); + }); + test('should show schedule that launched workflow job', async () => { wrapper = mountWithContexts( Date: Tue, 15 Jun 2021 14:17:04 -0400 Subject: [PATCH 2/2] improves permissions for ad hoc commands execution and tooltip handling --- .../components/AdHocCommands/AdHocCommands.js | 75 ++++++----- .../AdHocCommands/AdHocCommands.test.js | 123 ++++++------------ .../InventoryGroupHostList.js | 24 +++- .../InventoryGroupHostList.test.js | 39 +++++- .../InventoryGroups/InventoryGroupsList.js | 46 ++++--- .../InventoryGroupsList.test.js | 62 ++++++++- .../InventoryHostGroupsList.js | 23 +++- .../InventoryHostGroupsList.test.js | 40 ++++++ .../InventoryHosts/InventoryHostList.js | 24 +++- .../InventoryHosts/InventoryHostList.test.js | 44 +++++++ .../InventoryRelatedGroupList.js | 30 ++++- .../InventoryRelatedGroupList.test.js | 40 ++++++ 12 files changed, 406 insertions(+), 164 deletions(-) diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.js b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.js index f3bddc6abf..673f1028c7 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.js +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.js @@ -3,7 +3,7 @@ import { useHistory, useParams } from 'react-router-dom'; import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; -import { Button, DropdownItem } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import useRequest, { useDismissableError } from 'hooks/useRequest'; import { InventoriesAPI, CredentialTypesAPI } from 'api'; @@ -14,7 +14,12 @@ import ErrorDetail from '../ErrorDetail'; import AdHocCommandsWizard from './AdHocCommandsWizard'; import ContentError from '../ContentError'; -function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) { +function AdHocCommands({ + adHocItems, + hasListItems, + onLaunchLoading, + moduleOptions, +}) { const history = useHistory(); const { id } = useParams(); @@ -35,29 +40,21 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) { }, [isKebabified, isWizardOpen, onKebabModalChange]); const { - result: { - moduleOptions, - credentialTypeId, - isAdHocDisabled, - organizationId, - }, + result: { credentialTypeId, organizationId }, request: fetchData, error: fetchError, } = useRequest( useCallback(async () => { - const [options, { data }, cred] = await Promise.all([ - InventoriesAPI.readAdHocOptions(id), + const [{ data }, cred] = await Promise.all([ InventoriesAPI.readDetail(id), CredentialTypesAPI.read({ namespace: 'ssh' }), ]); return { - moduleOptions: options.data.actions.GET.module_name.choices, credentialTypeId: cred.data.results[0].id, - isAdHocDisabled: !options.data.actions.POST, organizationId: data.organization, }; }, [id]), - { moduleOptions: [], isAdHocDisabled: true, organizationId: null } + { organizationId: null } ); useEffect(() => { fetchData(); @@ -77,10 +74,6 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) { ) ); - const { error, dismissError } = useDismissableError( - launchError || fetchError - ); - const handleSubmit = async values => { const { credential, execution_environment, ...remainingValues } = values; const newCredential = credential[0].id; @@ -97,6 +90,10 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) { onLaunchLoading, ]); + const { error, dismissError } = useDismissableError( + launchError || fetchError + ); + if (error && isWizardOpen) { return ( - {isKebabified ? ( - setIsWizardOpen(true)} - > - {t`Run Command`} - - ) : ( - - )} + + {isKebabified ? ( + setIsWizardOpen(true)} + > + {t`Run Command`} + + ) : ( + + )} + {isWizardOpen && ( ', () => { BRAND_NAME: 'AWX', }, }); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { - module_name: { - choices: [ - ['command', 'command'], - ['shell', 'shell'], - ], - }, - }, - POST: {}, - }, - }, - }); CredentialTypesAPI.read.mockResolvedValue({ data: { count: 1, results: [{ id: 1, name: 'cred' }] }, }); @@ -95,21 +80,6 @@ describe('', () => { }); test('should open the wizard', async () => { - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { - module_name: { - choices: [ - ['command', 'command'], - ['foo', 'foo'], - ], - }, - verbosity: { choices: [[1], [2]] }, - }, - }, - }, - }); InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } }); CredentialTypesAPI.read.mockResolvedValue({ data: { results: [{ id: 1 }] }, @@ -126,12 +96,21 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( jest.fn()} /> ); }); + await waitForElement( + wrapper, + 'button[aria-label="Run Command"]', + el => el.length === 1 + ); await act(async () => wrapper.find('button[aria-label="Run Command"]').prop('onClick')() ); @@ -167,18 +146,26 @@ describe('', () => { }, }); ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({ - data: { actions: { GET: {} } }, + data: { actions: { GET: {}, POST: {} } }, }); await act(async () => { wrapper = mountWithContexts( jest.fn()} /> ); }); - + await waitForElement( + wrapper, + 'button[aria-label="Run Command"]', + el => el.length === 1 + ); await act(async () => wrapper.find('button[aria-label="Run Command"]').prop('onClick')() ); @@ -279,23 +266,6 @@ describe('', () => { }, }) ); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { - module_name: { - choices: [ - ['command', 'command'], - ['foo', 'foo'], - ], - }, - verbosity: { - choices: [[1], [2]], - }, - }, - }, - }, - }); InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 }, }); @@ -336,18 +306,26 @@ describe('', () => { }, }); ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({ - data: { actions: { GET: {} } }, + data: { actions: { GET: {}, POST: {} } }, }); await act(async () => { wrapper = mountWithContexts( jest.fn()} /> ); }); - + await waitForElement( + wrapper, + 'button[aria-label="Run Command"]', + el => el.length === 1 + ); await act(async () => wrapper.find('button[aria-label="Run Command"]').prop('onClick')() ); @@ -427,45 +405,18 @@ describe('', () => { await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0); }); - test('should disable run command button due to permissions', async () => { - InventoriesAPI.readHosts.mockResolvedValue({ - data: { results: [], count: 0 }, - }); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { module_name: { choices: [['module']] } }, - }, - }, - }); - await act(async () => { - wrapper = mountWithContexts( - jest.fn()} - /> - ); - }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - const runCommandsButton = wrapper.find('button[aria-label="Run Command"]'); - expect(runCommandsButton.prop('disabled')).toBe(true); - }); - test('should disable run command button due to lack of list items', async () => { InventoriesAPI.readHosts.mockResolvedValue({ data: { results: [], count: 0 }, }); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { module_name: { choices: [['module']] } }, - }, - }, - }); + await act(async () => { wrapper = mountWithContexts( jest.fn()} @@ -478,7 +429,7 @@ describe('', () => { }); test('should open alert modal when error on fetching data', async () => { - InventoriesAPI.readAdHocOptions.mockRejectedValue( + InventoriesAPI.readDetail.mockRejectedValue( new Error({ response: { config: { @@ -493,6 +444,10 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts( jest.fn()} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js index 79185a648c..5f02147d11 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.js @@ -43,6 +43,8 @@ function InventoryGroupHostList() { actions, relatedSearchableKeys, searchableKeys, + moduleOptions, + isAdHocDisabled, }, error: contentError, isLoading, @@ -50,12 +52,15 @@ function InventoryGroupHostList() { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [response, actionsResponse] = await Promise.all([ + const [response, actionsResponse, options] = await Promise.all([ GroupsAPI.readAllHosts(groupId, params), InventoriesAPI.readHostsOptions(inventoryId), + InventoriesAPI.readAdHocOptions(inventoryId), ]); return { + moduleOptions: options.data.actions.GET.module_name.choices, + isAdHocDisabled: !options.data.actions.POST, hosts: response.data.results, hostCount: response.data.count, actions: actionsResponse.data.actions, @@ -68,6 +73,8 @@ function InventoryGroupHostList() { }; }, [groupId, inventoryId, location.search]), { + moduleOptions: [], + isAdHocDisabled: true, hosts: [], hostCount: 0, actions: {}, @@ -225,11 +232,16 @@ function InventoryGroupHostList() { qsConfig={QS_CONFIG} additionalControls={[ ...(canAdd ? [addButton] : []), - 0} - onLaunchLoading={setIsAdHocLaunchLoading} - />, + ...(!isAdHocDisabled + ? [ + 0} + moduleOptions={moduleOptions} + onLaunchLoading={setIsAdHocLaunchLoading} + />, + ] + : []), ', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); await act(async () => { wrapper = mountWithContexts(); }); @@ -52,6 +67,7 @@ describe('', () => { test('should fetch inventory group hosts from api and render them in the list', () => { expect(GroupsAPI.readAllHosts).toHaveBeenCalled(); expect(InventoriesAPI.readHostsOptions).toHaveBeenCalled(); + expect(InventoriesAPI.readAdHocOptions).toHaveBeenCalled(); expect(wrapper.find('InventoryGroupHostListItem').length).toBe(3); }); @@ -95,7 +111,7 @@ describe('', () => { }); }); - test('should show add dropdown button according to permissions', async () => { + test('should show add dropdown button and Run Commands according to permissions', async () => { expect(wrapper.find('AddDropDownButton').length).toBe(1); InventoriesAPI.readHostsOptions.mockResolvedValueOnce({ data: { @@ -109,6 +125,7 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('AddDropDownButton').length).toBe(0); + expect(wrapper.find('AdHocCommands').length).toBe(1); }); test('expected api calls are made for multi-delete', async () => { @@ -271,4 +288,24 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); + test('should not render ad hoc commands button', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.find('AdHocCommands')).toHaveLength(0); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js index e0093da827..dfb93c89a5 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.js @@ -38,6 +38,8 @@ function InventoryGroupsList() { actions, relatedSearchableKeys, searchableKeys, + moduleOptions, + isAdHocDisabled, }, error: contentError, isLoading, @@ -45,12 +47,15 @@ function InventoryGroupsList() { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [response, groupOptions] = await Promise.all([ + const [response, groupOptions, options] = await Promise.all([ InventoriesAPI.readGroups(inventoryId, params), InventoriesAPI.readGroupsOptions(inventoryId), + InventoriesAPI.readAdHocOptions(inventoryId), ]); return { + moduleOptions: options.data.actions.GET.module_name.choices, + isAdHocDisabled: !options.data.actions.POST, groups: response.data.results, groupCount: response.data.count, actions: groupOptions.data.actions, @@ -68,6 +73,8 @@ function InventoryGroupsList() { actions: {}, relatedSearchableKeys: [], searchableKeys: [], + moduleOptions: [], + isAdHocDisabled: true, } ); @@ -171,22 +178,29 @@ function InventoryGroupsList() { />, ] : []), - 0} - onLaunchLoading={setIsAdHocLaunchLoading} - />, + ...(!isAdHocDisabled + ? [ + 0} + onLaunchLoading={setIsAdHocLaunchLoading} + moduleOptions={moduleOptions} + />, + ] + : []), - { - fetchData(); - clearSelected(); - }} - /> +
+ { + fetchData(); + clearSelected(); + }} + /> +
, ]} /> diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js index 3b05b548ce..c60a9c8edb 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.js @@ -76,6 +76,21 @@ describe('', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); const history = createMemoryHistory({ initialEntries: ['/inventories/inventory/3/groups'], }); @@ -86,7 +101,12 @@ describe('', () => { , { context: { - router: { history, route: { location: history.location } }, + router: { + history, + route: { + location: history.location, + }, + }, }, } ); @@ -103,6 +123,10 @@ describe('', () => { expect(wrapper.find('InventoryGroupItem').length).toBe(3); }); + test('should render Run Commands button', async () => { + expect(wrapper.find('AdHocCommands')).toHaveLength(1); + }); + test('should check and uncheck the row item', async () => { expect( wrapper @@ -165,6 +189,27 @@ describe('', () => { expect(el.find('input').props().checked).toBe(false); }); }); + + test('should not render ad hoc commands button', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.find('AdHocCommands')).toHaveLength(0); + }); }); describe(' error handling', () => { @@ -185,6 +230,21 @@ describe(' error handling', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); GroupsAPI.destroy.mockRejectedValue( new Error({ response: { diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.js b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.js index 0659332f58..3d6f3a44fb 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.js +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.js @@ -41,6 +41,8 @@ function InventoryHostGroupsList() { actions, relatedSearchableKeys, searchableKeys, + moduleOptions, + isAdHocDisabled, }, error: contentError, isLoading, @@ -54,12 +56,16 @@ function InventoryHostGroupsList() { data: { count, results }, }, hostGroupOptions, + adHocOptions, ] = await Promise.all([ HostsAPI.readAllGroups(hostId, params), HostsAPI.readGroupsOptions(hostId), + InventoriesAPI.readAdHocOptions(invId), ]); return { + moduleOptions: adHocOptions.data.actions.GET.module_name.choices, + isAdHocDisabled: !adHocOptions.data.actions.POST, groups: results, itemCount: count, actions: hostGroupOptions.data.actions, @@ -77,6 +83,8 @@ function InventoryHostGroupsList() { actions: {}, relatedSearchableKeys: [], searchableKeys: [], + moduleOptions: [], + isAdHocDisabled: true, } ); @@ -209,11 +217,16 @@ function InventoryHostGroupsList() { />, ] : []), - 0} - onLaunchLoading={setIsAdHocLaunchLoading} - />, + ...(!isAdHocDisabled + ? [ + 0} + moduleOptions={moduleOptions} + onLaunchLoading={setIsAdHocLaunchLoading} + />, + ] + : []), ', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); const history = createMemoryHistory({ initialEntries: ['/inventories/inventory/1/hosts/3/groups'], }); @@ -111,6 +126,10 @@ describe('', () => { expect(wrapper.find('InventoryHostGroupItem').length).toBe(3); }); + test('should render Run Commands button', async () => { + expect(wrapper.find('AdHocCommands')).toHaveLength(1); + }); + test('should check and uncheck the row item', async () => { expect( wrapper @@ -174,6 +193,27 @@ describe('', () => { }); }); + test('should not render Run Commands button', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.find('AdHocCommands')).toHaveLength(0); + }); + test('should show content error when api throws error on initial render', async () => { HostsAPI.readAllGroups.mockImplementation(() => Promise.reject(new Error()) diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.js b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.js index 0113757f11..0ab9571858 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.js +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.js @@ -35,6 +35,8 @@ function InventoryHostList() { actions, relatedSearchableKeys, searchableKeys, + moduleOptions, + isAdHocDisabled, }, error: contentError, isLoading, @@ -42,12 +44,15 @@ function InventoryHostList() { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); - const [response, hostOptions] = await Promise.all([ + const [response, hostOptions, adHocOptions] = await Promise.all([ InventoriesAPI.readHosts(id, params), InventoriesAPI.readHostsOptions(id), + InventoriesAPI.readAdHocOptions(id), ]); return { + moduleOptions: adHocOptions.data.actions.GET.module_name.choices, + isAdHocDisabled: !adHocOptions.data.actions.POST, hosts: response.data.results, hostCount: response.data.count, actions: hostOptions.data.actions, @@ -65,6 +70,8 @@ function InventoryHostList() { actions: {}, relatedSearchableKeys: [], searchableKeys: [], + moduleOptions: [], + isAdHocDisabled: true, } ); @@ -152,11 +159,16 @@ function InventoryHostList() { />, ] : []), - 0} - onLaunchLoading={setIsAdHocLaunchLoading} - />, + ...(!isAdHocDisabled + ? [ + 0} + onLaunchLoading={setIsAdHocLaunchLoading} + />, + ] + : []), ', () => { }, }, }); + + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); + await act(async () => { wrapper = mountWithContexts(); }); @@ -108,6 +125,10 @@ describe('', () => { expect(wrapper.find('InventoryHostItem').length).toBe(3); }); + test('should render Run Commands button', async () => { + expect(wrapper.find('AdHocCommands')).toHaveLength(1); + }); + test('should check and uncheck the row item', async () => { expect( wrapper @@ -322,4 +343,27 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); + + test('should not render Run Commands button', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('AdHocCommands')).toHaveLength(0); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js index 617efbee37..b30d0f4d71 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js +++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.js @@ -43,18 +43,23 @@ function InventoryRelatedGroupList() { relatedSearchableKeys, searchableKeys, canAdd, + moduleOptions, + isAdHocDisabled, }, isLoading, error: contentError, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [response, actions] = await Promise.all([ + const [response, actions, adHocOptions] = await Promise.all([ GroupsAPI.readChildren(groupId, params), InventoriesAPI.readGroupsOptions(inventoryId), + InventoriesAPI.readAdHocOptions(inventoryId), ]); return { + moduleOptions: adHocOptions.data.actions.GET.module_name.choices, + isAdHocDisabled: !adHocOptions.data.actions.POST, groups: response.data.results, itemCount: response.data.count, relatedSearchableKeys: ( @@ -68,7 +73,13 @@ function InventoryRelatedGroupList() { Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST'), }; }, [groupId, location.search, inventoryId]), - { groups: [], itemCount: 0, canAdd: false } + { + groups: [], + itemCount: 0, + canAdd: false, + moduleOptions: [], + isAdHocDisabled: true, + } ); useEffect(() => { fetchRelated(); @@ -199,11 +210,16 @@ function InventoryRelatedGroupList() { qsConfig={QS_CONFIG} additionalControls={[ ...(canAdd ? [addButton] : []), - 0} - onLaunchLoading={setIsAdHocLaunchLoading} - />, + ...(!isAdHocDisabled + ? [ + 0} + onLaunchLoading={setIsAdHocLaunchLoading} + moduleOptions={moduleOptions} + />, + ] + : []), ', () => { ], }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + POST: {}, + }, + }, + }); await act(async () => { wrapper = mountWithContexts(); }); @@ -107,6 +122,10 @@ describe('', () => { expect(wrapper.find('InventoryRelatedGroupListItem').length).toBe(3); }); + test('should render Run Commands Button', async () => { + expect(wrapper.find('AdHocCommands')).toHaveLength(1); + }); + test('should check and uncheck the row item', async () => { expect( wrapper.find('input[aria-label="Select row 0"]').props().checked @@ -220,4 +239,25 @@ describe('', () => { ); expect(GroupsAPI.associateChildGroup).toBeCalledTimes(1); }); + + test('should not render Run Commands button', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['shell', 'shell'], + ], + }, + }, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.find('AdHocCommands')).toHaveLength(0); + }); });