From eb2d7c6a7774361b767819634f3d718680f5be1d Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 25 Sep 2020 15:06:07 -0400 Subject: [PATCH 1/3] Adds Ad Hoc Commands --- .../AdHocCommands/AdHocCommands.jsx | 7 +- .../AdHocCommands/AdHocCommands.test.jsx | 30 ++- .../InventoryGroupHostList.jsx | 57 ++++++ .../InventoryGroupHostList.test.jsx | 49 ++++- .../InventoryGroups/InventoryGroupsList.jsx | 8 +- .../InventoryGroupsList.test.jsx | 123 ++++++++--- .../InventoryHostGroupsList.jsx | 61 +++++- .../InventoryHostGroupsList.test.jsx | 20 +- .../InventoryHosts/InventoryHostList.jsx | 57 ++++++ .../InventoryHosts/InventoryHostList.test.jsx | 20 +- .../SmartInventoryHostList.jsx | 64 +++++- .../SmartInventoryHostList.test.jsx | 192 ++++++++---------- 12 files changed, 538 insertions(+), 150 deletions(-) diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx index ea28e52652..a16ff73a38 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx @@ -25,7 +25,7 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) { const { error: fetchError, request: fetchModuleOptions, - result: { moduleOptions, credentialTypeId }, + result: { moduleOptions, credentialTypeId, isDisabled }, } = useRequest( useCallback(async () => { const [choices, credId] = await Promise.all([ @@ -44,13 +44,13 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) { const options = choices.data.actions.GET.module_name.choices.map( (choice, index) => itemObject(choice[0], index) ); - return { moduleOptions: [itemObject('', -1), ...options], credentialTypeId: credId.data.results[0].id, + isDisabled: !choices.data.actions.POST, }; }, [itemId, apiModule]), - { moduleOptions: [] } + { moduleOptions: [], isDisabled: true } ); useEffect(() => { @@ -118,6 +118,7 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) { {children({ openAdHocCommands: () => setIsWizardOpen(true), + isDisabled, })} {isWizardOpen && ( diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx index b9582daedc..5d54fd1b1f 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx @@ -25,8 +25,12 @@ const adHocItems = [ { name: 'Inventory 2 Org 0' }, ]; -const children = ({ openAdHocCommands }) => ( - + )} + + + + ) + } + , ({ ...jest.requireActual('react-router-dom'), useParams: () => ({ @@ -95,6 +96,52 @@ describe('', () => { }); }); + test('should render enabled ad hoc commands button', async () => { + GroupsAPI.readAllHosts.mockResolvedValue({ + data: { ...mockHosts }, + }); + InventoriesAPI.readHostsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, + }); + await act(async () => { + wrapper = mountWithContexts( + + {({ openAdHocCommands, isDisabled }) => ( + diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx index 6684a2a01e..800e715e5e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.test.jsx @@ -6,7 +6,7 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; -import { InventoriesAPI, GroupsAPI } from '../../../api'; +import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api'; import InventoryGroupsList from './InventoryGroupsList'; jest.mock('../../../api'); @@ -71,13 +71,34 @@ describe('', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, + }); const history = createMemoryHistory({ initialEntries: ['/inventories/inventory/3/groups'], }); await act(async () => { wrapper = mountWithContexts( - + + {({ openAdHocCommands, isDisabled }) => ( + + )} + + + + ) + } + , , diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx index 493e9dc65a..fed065ecb4 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx @@ -6,7 +6,7 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; -import { HostsAPI, InventoriesAPI } from '../../../api'; +import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api'; import InventoryHostGroupsList from './InventoryHostGroupsList'; jest.mock('../../../api'); @@ -80,6 +80,17 @@ describe('', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, + }); const history = createMemoryHistory({ initialEntries: ['/inventories/inventory/1/hosts/3/groups'], }); @@ -272,4 +283,11 @@ describe('', () => { wrapper.update(); expect(wrapper.find('AlertModal ErrorDetail').length).toBe(1); }); + test('should render enabled ad hoc commands button', async () => { + await waitForElement( + wrapper, + 'button[aria-label="Run command"]', + el => el.prop('disabled') === false + ); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx index 6e0168330d..a635aca352 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx @@ -2,6 +2,12 @@ import React, { useEffect, useState } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { + Button, + Tooltip, + DropdownItem, + ToolbarItem, +} from '@patternfly/react-core'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import { InventoriesAPI, HostsAPI } from '../../../api'; @@ -12,6 +18,8 @@ import PaginatedDataList, { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; +import { Kebabified } from '../../../contexts/Kebabified'; +import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands'; import InventoryHostItem from './InventoryHostItem'; const QS_CONFIG = getQSConfig('host', { @@ -149,6 +157,55 @@ function InventoryHostList({ i18n }) { />, ] : []), + + {({ isKebabified }) => + isKebabified ? ( + + {({ openAdHocCommands, isDisabled }) => ( + + {i18n._(t`Run command`)} + + )} + + ) : ( + + + + {({ openAdHocCommands, isDisabled }) => ( + + )} + + + + ) + } + , ', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, + }); await act(async () => { wrapper = mountWithContexts(); }); @@ -293,4 +304,11 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); + test('should render enabled ad hoc commands button', async () => { + await waitForElement( + wrapper, + 'button[aria-label="Run command"]', + el => el.prop('disabled') === false + ); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx index aa6290669c..e5539647be 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx @@ -2,7 +2,12 @@ import React, { useEffect, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; +import { + Button, + Tooltip, + DropdownItem, + ToolbarItem, +} from '@patternfly/react-core'; import DataListToolbar from '../../../components/DataListToolbar'; import PaginatedDataList from '../../../components/PaginatedDataList'; import SmartInventoryHostListItem from './SmartInventoryHostListItem'; @@ -11,6 +16,8 @@ import useSelected from '../../../util/useSelected'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import { InventoriesAPI } from '../../../api'; import { Inventory } from '../../../types'; +import { Kebabified } from '../../../contexts/Kebabified'; +import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands'; const QS_CONFIG = getQSConfig('host', { page: 1, @@ -89,12 +96,55 @@ function SmartInventoryHostList({ i18n, inventory }) { additionalControls={ inventory?.summary_fields?.user_capabilities?.adhoc ? [ - , + + {({ isKebabified }) => + isKebabified ? ( + + {({ openAdHocCommands, isDisabled }) => ( + + {i18n._(t`Run command`)} + + )} + + ) : ( + + + + {({ openAdHocCommands, isDisabled }) => ( + + )} + + + + ) + } + , ] : [] } diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx index ae3f00d66f..a1bb33f221 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { InventoriesAPI } from '../../../api'; +import { InventoriesAPI, CredentialTypesAPI } from '../../../api'; import { mountWithContexts, waitForElement, @@ -12,125 +12,107 @@ import mockHosts from '../shared/data.hosts.json'; jest.mock('../../../api'); describe('', () => { - describe('User has adhoc permissions', () => { - let wrapper; - const clonedInventory = { - ...mockInventory, - summary_fields: { - ...mockInventory.summary_fields, - user_capabilities: { - ...mockInventory.summary_fields.user_capabilities, + // describe('User has adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + ...mockInventory.summary_fields, + user_capabilities: { + ...mockInventory.summary_fields.user_capabilities, + }, + }, + }; + + beforeAll(async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: mockHosts, + }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, }, }, - }; - - beforeAll(async () => { - InventoriesAPI.readHosts.mockResolvedValue({ - data: mockHosts, - }); - await act(async () => { - wrapper = mountWithContexts( - - ); - }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); - - afterAll(() => { - jest.clearAllMocks(); - wrapper.unmount(); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, }); - - test('initially renders successfully', () => { - expect(wrapper.find('SmartInventoryHostList').length).toBe(1); - }); - - test('should fetch hosts from api and render them in the list', () => { - expect(InventoriesAPI.readHosts).toHaveBeenCalled(); - expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3); - }); - - test('should disable run commands button when no hosts are selected', () => { - wrapper.find('DataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - const runCommandsButton = wrapper.find( - 'button[aria-label="Run commands"]' + await act(async () => { + wrapper = mountWithContexts( + + {({ openAdHocCommands, isDisabled }) => ( + - )} - + {i18n._(t`Run command`)} + ) @@ -279,6 +280,7 @@ function InventoryGroupHostList({ i18n }) { emptyStateControls={ canAdd && ( setIsModalOpen(true)} onAddNew={() => history.push(addFormUrl)} /> @@ -296,6 +298,16 @@ function InventoryGroupHostList({ i18n }) { title={i18n._(t`Select Hosts`)} /> )} + {isAdHocCommandsOpen && ( + setIsAdHocCommandsOpen(false)} + credentialTypeId={credentialTypeId} + moduleOptions={moduleOptions} + /> + )} {associateError && ( ', () => { }, }, }); + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { module_name: { choices: [['module']] } }, + POST: {}, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { count: 1, results: [{ id: 1, name: 'cred' }] }, + }); await act(async () => { wrapper = mountWithContexts(); }); @@ -108,31 +119,8 @@ describe('', () => { }, }, }); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { module_name: { choices: [['module']] } }, - POST: {}, - }, - }, - }); - CredentialTypesAPI.read.mockResolvedValue({ - data: { count: 1, results: [{ id: 1, name: 'cred' }] }, - }); await act(async () => { - wrapper = mountWithContexts( - - {({ openAdHocCommands, isDisabled }) => ( - - )} - + {i18n._(t`Run command`)} + @@ -321,6 +316,16 @@ function InventoryGroupsList({ i18n }) { ) } /> + {isAdHocCommandsOpen && ( + setIsAdHocCommandsOpen(false)} + credentialTypeId={credentialTypeId} + moduleOptions={moduleOptions} + /> + )} {deletionError && ( ', () => { await act(async () => { wrapper = mountWithContexts( - - {({ openAdHocCommands, isDisabled }) => ( - - )} - + {i18n._(t`Run command`)} + ) @@ -290,6 +288,16 @@ function InventoryHostGroupsList({ i18n }) { title={i18n._(t`Select Groups`)} /> )} + {isAdHocCommandsOpen && ( + setIsAdHocCommandsOpen(false)} + credentialTypeId={credentialTypeId} + moduleOptions={moduleOptions} + /> + )} {error && ( { - const params = parseQueryString(QS_CONFIG, queryString); - return InventoriesAPI.readHosts(hostId, params); - }; + const { + result: { + hosts, + hostCount, + actions, + relatedSearchableKeys, + searchableKeys, + moduleOptions, + credentialTypeId, + isAdHocDisabled, + }, + error: contentError, + isLoading, + request: fetchData, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, search); + const [response, hostOptions, adHocOptions, cred] = await Promise.all([ + InventoriesAPI.readHosts(id, params), + InventoriesAPI.readHostsOptions(id), + InventoriesAPI.readAdHocOptions(id), + CredentialTypesAPI.read({ namespace: 'ssh' }), + ]); + + return { + hosts: response.data.results, + hostCount: response.data.count, + actions: hostOptions.data.actions, + relatedSearchableKeys: ( + hostOptions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(hostOptions.data.actions?.GET || {}).filter( + key => hostOptions.data.actions?.GET[key].filterable + ), + moduleOptions: adHocOptions.data.actions.GET.module_name.choices, + credentialTypeId: cred.data.results[0].id, + isAdHocDisabled: !adHocOptions.data.actions.POST, + }; + }, [id, search]), + { + hosts: [], + hostCount: 0, + actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], + moduleOptions: [], + isAdHocDisabled: true, + } + ); useEffect(() => { - async function fetchData() { - try { - const [ - { - data: { count, results }, - }, - { - data: { actions: optionActions }, - }, - ] = await Promise.all([ - fetchHosts(id, search), - InventoriesAPI.readOptions(), - ]); - - setHosts(results); - setHostCount(count); - setActions(optionActions); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } - } - fetchData(); - }, [id, search]); + }, [fetchData]); const handleSelectAll = isSelected => { setSelected(isSelected ? [...hosts] : []); @@ -83,30 +99,17 @@ function InventoryHostList({ i18n }) { setSelected(selected.concat(row)); } }; - - const handleDelete = async () => { - setIsLoading(true); - - try { + const { + isLoading: isDeleteLoading, + deleteItems: deleteHosts, + deletionError, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { await Promise.all(selected.map(host => HostsAPI.destroy(host.id))); - } catch (error) { - setDeletionError(error); - } finally { - setSelected([]); - try { - const { - data: { count, results }, - } = await fetchHosts(id, search); - - setHosts(results); - setHostCount(count); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } - } - }; + }, [selected]), + { qsConfig: QS_CONFIG, fetchItems: fetchData } + ); const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); @@ -116,7 +119,7 @@ function InventoryHostList({ i18n }) { <> ( {({ isKebabified }) => isKebabified ? ( - setIsAdHocCommandsOpen(true)} + isDisabled={hostCount === 0 || isAdHocDisabled} + aria-label={i18n._(t`Run command`)} > - {({ openAdHocCommands, isDisabled }) => ( - - {i18n._(t`Run command`)} - - )} - + {i18n._(t`Run command`)} + ) : ( - setIsAdHocCommandsOpen(true)} + isDisabled={hostCount === 0 || isAdHocDisabled} > - {({ openAdHocCommands, isDisabled }) => ( - - )} - + {i18n._(t`Run command`)} + ) @@ -208,7 +198,7 @@ function InventoryHostList({ i18n }) { , , @@ -234,12 +224,22 @@ function InventoryHostList({ i18n }) { ) } /> + {isAdHocCommandsOpen && ( + setIsAdHocCommandsOpen(false)} + credentialTypeId={credentialTypeId} + moduleOptions={moduleOptions} + itemId={id} + /> + )} {deletionError && ( setDeletionError(null)} + onClose={clearDeletionError} > {i18n._(t`Failed to delete one or more hosts.`)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx index 85a25f11bb..b71b0289ca 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.test.jsx @@ -85,7 +85,7 @@ describe('', () => { results: mockHosts, }, }); - InventoriesAPI.readOptions.mockResolvedValue({ + InventoriesAPI.readHostsOptions.mockResolvedValue({ data: { actions: { GET: {}, @@ -276,8 +276,15 @@ describe('', () => { expect(wrapper.find('ToolbarAddButton').length).toBe(1); }); + test('should render enabled ad hoc commands button', async () => { + await waitForElement( + wrapper, + 'button[aria-label="Run command"]', + el => el.prop('disabled') === false + ); + }); test('should hide Add button for users without ability to POST', async () => { - InventoriesAPI.readOptions.mockResolvedValueOnce({ + InventoriesAPI.readHostsOptions.mockResolvedValueOnce({ data: { actions: { GET: {}, @@ -294,7 +301,7 @@ describe('', () => { }); test('should show content error when api throws error on initial render', async () => { - InventoriesAPI.readOptions.mockImplementation(() => + InventoriesAPI.readHostsOptions.mockImplementation(() => Promise.reject(new Error()) ); await act(async () => { @@ -304,11 +311,4 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); - test('should render enabled ad hoc commands button', async () => { - await waitForElement( - wrapper, - 'button[aria-label="Run command"]', - el => el.prop('disabled') === false - ); - }); }); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx index e5539647be..4322463741 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -14,10 +14,10 @@ import SmartInventoryHostListItem from './SmartInventoryHostListItem'; import useRequest from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; import { getQSConfig, parseQueryString } from '../../../util/qs'; -import { InventoriesAPI } from '../../../api'; +import { InventoriesAPI, CredentialTypesAPI } from '../../../api'; import { Inventory } from '../../../types'; import { Kebabified } from '../../../contexts/Kebabified'; -import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands'; +import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; const QS_CONFIG = getQSConfig('host', { page: 1, @@ -27,24 +27,35 @@ const QS_CONFIG = getQSConfig('host', { function SmartInventoryHostList({ i18n, inventory }) { const location = useLocation(); + const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); const { - result: { hosts, count }, + result: { hosts, count, moduleOptions, credentialTypeId, isAdHocDisabled }, error: contentError, isLoading, request: fetchHosts, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const { data } = await InventoriesAPI.readHosts(inventory.id, params); + const [hostResponse, adHocOptions, cred] = await Promise.all([ + InventoriesAPI.readHosts(inventory.id, params), + InventoriesAPI.readAdHocOptions(inventory.id), + CredentialTypesAPI.read({ namespace: 'ssh' }), + ]); + return { - hosts: data.results, - count: data.count, + hosts: hostResponse.data.results, + count: hostResponse.data.count, + moduleOptions: adHocOptions.data.actions.GET.module_name.choices, + credentialTypeId: cred.data.results[0].id, + isAdHocDisabled: !adHocOptions.data.actions.POST, }; }, [location.search, inventory.id]), { hosts: [], count: 0, + moduleOptions: [], + isAdHocDisabled: true, } ); @@ -57,109 +68,106 @@ function SmartInventoryHostList({ i18n, inventory }) { }, [fetchHosts]); return ( - ( - setSelected(isSelected ? [...hosts] : [])} - qsConfig={QS_CONFIG} - additionalControls={ - inventory?.summary_fields?.user_capabilities?.adhoc - ? [ - - {({ isKebabified }) => - isKebabified ? ( - - {({ openAdHocCommands, isDisabled }) => ( - - {i18n._(t`Run command`)} - - )} - - ) : ( - - + ( + + setSelected(isSelected ? [...hosts] : []) + } + qsConfig={QS_CONFIG} + additionalControls={ + inventory?.summary_fields?.user_capabilities?.adhoc + ? [ + + {({ isKebabified }) => + isKebabified ? ( + setIsAdHocCommandsOpen(true)} + isDisabled={count === 0 || isAdHocDisabled} > - - {({ openAdHocCommands, isDisabled }) => ( - + {i18n._(t`Run command`)} + + ) : ( + + - - - ) - } - , - ] - : [] - } + position="top" + key="adhoc" + > + + + + ) + } + , + ] + : [] + } + /> + )} + renderItem={host => ( + row.id === host.id)} + onSelect={() => handleSelect(host)} + /> + )} + /> + {isAdHocCommandsOpen && ( + setIsAdHocCommandsOpen(false)} + credentialTypeId={credentialTypeId} + moduleOptions={moduleOptions} /> )} - renderItem={host => ( - row.id === host.id)} - onSelect={() => handleSelect(host)} - /> - )} - /> + ); } diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx index a1bb33f221..60fd23d75a 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx @@ -12,7 +12,6 @@ import mockHosts from '../shared/data.hosts.json'; jest.mock('../../../api'); describe('', () => { - // describe('User has adhoc permissions', () => { let wrapper; const clonedInventory = { ...mockInventory, @@ -41,17 +40,7 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - - {({ openAdHocCommands, isDisabled }) => ( -