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 }) => ( +