From bebaf2d97e6f29e975ffd97c97b21ec2f7f88638 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 8 Oct 2020 18:31:22 -0400 Subject: [PATCH] Uses existing kebabified workflow for run command --- .../AdHocCommands/AdHocCommands.jsx | 124 +++++++++---- .../AdHocCommands/AdHocCommands.test.jsx | 163 ++++++++++++++---- .../AdHocCommands/AdHocCommandsWizard.jsx | 3 +- .../AdHocCommands/AdHocDetailsStep.jsx | 4 +- .../InventoryGroupHostList.jsx | 75 +------- .../InventoryGroupHostList.test.jsx | 36 +--- .../InventoryGroups/InventoryGroupsList.jsx | 125 +++----------- .../InventoryGroupsList.test.jsx | 68 +------- .../InventoryHostGroupsList.jsx | 70 +------- .../InventoryHostGroupsList.test.jsx | 20 +-- .../InventoryHosts/InventoryHostList.jsx | 70 +------- .../InventoryHosts/InventoryHostList.test.jsx | 20 +-- .../InventoryRelatedGroupList.jsx | 61 +------ .../SmartInventoryHostList.jsx | 77 ++------- .../SmartInventoryHostList.test.jsx | 42 +---- .../screens/Inventory/shared/AddDropdown.jsx | 2 +- 16 files changed, 289 insertions(+), 671 deletions(-) diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx index 927ad09f78..48fc566e2c 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx @@ -1,26 +1,27 @@ -import React, { useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { useCallback, useEffect, useState, useContext } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; +import { Button, DropdownItem } from '@patternfly/react-core'; import useRequest, { useDismissableError } from '../../util/useRequest'; -import { InventoriesAPI } from '../../api'; +import { InventoriesAPI, CredentialTypesAPI } from '../../api'; import AlertModal from '../AlertModal'; import ErrorDetail from '../ErrorDetail'; import AdHocCommandsWizard from './AdHocCommandsWizard'; +import { KebabifiedContext } from '../../contexts/Kebabified'; import ContentLoading from '../ContentLoading'; +import ContentError from '../ContentError'; -function AdHocCommands({ - onClose, - adHocItems, - itemId, - i18n, - moduleOptions, - credentialTypeId, -}) { +function AdHocCommands({ adHocItems, i18n, hasListItems }) { const history = useHistory(); + const { id } = useParams(); + + const [isWizardOpen, setIsWizardOpen] = useState(false); + const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext); + const verbosityOptions = [ { value: '0', key: '0', label: i18n._(t`0 (Normal)`) }, { value: '1', key: '1', label: i18n._(t`1 (Verbose)`) }, @@ -28,26 +29,51 @@ function AdHocCommands({ { value: '3', key: '3', label: i18n._(t`3 (Debug)`) }, { value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) }, ]; + useEffect(() => { + if (isKebabified) { + onKebabModalChange(isWizardOpen); + } + }, [isKebabified, isWizardOpen, onKebabModalChange]); + const { + result: { moduleOptions, credentialTypeId, isAdHocDisabled }, + request: fetchData, + error: fetchError, + } = useRequest( + useCallback(async () => { + const [options, cred] = await Promise.all([ + InventoriesAPI.readAdHocOptions(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, + }; + }, [id]), + { moduleOptions: [], isAdHocDisabled: true } + ); + useEffect(() => { + fetchData(); + }, [fetchData]); const { isloading: isLaunchLoading, - error, + error: launchError, request: launchAdHocCommands, } = useRequest( useCallback( async values => { - const { data } = await InventoriesAPI.launchAdHocCommands( - itemId, - values - ); + const { data } = await InventoriesAPI.launchAdHocCommands(id, values); history.push(`/jobs/command/${data.id}/output`); }, - [itemId, history] + [id, history] ) ); - const { dismissError } = useDismissableError(error); + const { error, dismissError } = useDismissableError( + launchError || fetchError + ); const handleSubmit = async values => { const { credential, ...remainingValues } = values; @@ -64,7 +90,7 @@ function AdHocCommands({ return ; } - if (error) { + if (error && isWizardOpen) { return ( { dismissError(); + setIsWizardOpen(false); }} > - <> - {i18n._(t`Failed to launch job.`)} - - + {launchError ? ( + <> + {i18n._(t`Failed to launch job.`)} + + + ) : ( + + )} ); } return ( - dismissError()} - /> + // render buttons for drop down and for toolbar + // if modal is open render the modal + <> + {isKebabified ? ( + setIsWizardOpen(true)} + > + {i18n._(t`Run Command`)} + + ) : ( + + )} + + {isWizardOpen && ( + setIsWizardOpen(false)} + onLaunch={handleSubmit} + onDismissError={() => dismissError()} + /> + )} + ); } AdHocCommands.propTypes = { adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired, - itemId: PropTypes.number.isRequired, + hasListItems: PropTypes.bool.isRequired, }; export default withI18n()(AdHocCommands); diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx index f83009b46a..2cbe7250f0 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx @@ -10,7 +10,12 @@ import AdHocCommands from './AdHocCommands'; jest.mock('../../api/models/CredentialTypes'); jest.mock('../../api/models/Inventories'); jest.mock('../../api/models/Credentials'); - +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + }), +})); const credentials = [ { id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' }, { id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' }, @@ -18,10 +23,7 @@ const credentials = [ { id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' }, { id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' }, ]; -const moduleOptions = [ - ['command', 'command'], - ['shell', 'shell'], -]; + const adHocItems = [ { name: 'Inventory 1 Org 0', @@ -30,6 +32,26 @@ const adHocItems = [ ]; describe('', () => { + beforeEach(() => { + 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' }] }, + }); + }); let wrapper; afterEach(() => { wrapper.unmount(); @@ -39,19 +61,45 @@ describe('', () => { test('mounts successfully', async () => { await act(async () => { wrapper = mountWithContexts( - {}} - itemId={1} - credentialTypeId={1} - adHocItems={adHocItems} - moduleOptions={moduleOptions} - /> + ); }); expect(wrapper.find('AdHocCommands').length).toBe(1); }); + test('should open the wizard', async () => { + InventoriesAPI.readAdHocOptions.mockResolvedValue({ + data: { + actions: { + GET: { + module_name: { + choices: [ + ['command', 'command'], + ['foo', 'foo'], + ], + }, + verbosity: { choices: [[1], [2]] }, + }, + }, + }, + }); + CredentialTypesAPI.read.mockResolvedValue({ + data: { results: [{ id: 1 }] }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('button[aria-label="Run Command"]').prop('onClick')() + ); + + wrapper.update(); + + expect(wrapper.find('AdHocCommandsWizard').length).toBe(1); + }); + test('should submit properly', async () => { InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } }); CredentialsAPI.read.mockResolvedValue({ @@ -62,17 +110,13 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - {}} - itemId={1} - credentialTypeId={1} - adHocItems={adHocItems} - moduleOptions={moduleOptions} - /> + ); }); + await act(async () => + wrapper.find('button[aria-label="Run Command"]').prop('onClick')() + ); wrapper.update(); expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true); @@ -174,17 +218,13 @@ describe('', () => { }); await act(async () => { wrapper = mountWithContexts( - {}} - credentialTypeId={1} - itemId={1} - adHocItems={adHocItems} - moduleOptions={moduleOptions} - /> + ); }); + await act(async () => + wrapper.find('button[aria-label="Run Command"]').prop('onClick')() + ); wrapper.update(); expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true); @@ -237,4 +277,69 @@ 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( + + ); + }); + 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( + + ); + }); + 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 open alert modal when error on fetching data', async () => { + InventoriesAPI.readAdHocOptions.mockRejectedValue( + new Error({ + response: { + config: { + method: 'options', + url: '/api/v2/inventories/1/', + }, + data: 'An error occurred', + status: 403, + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => wrapper.find('button').prop('onClick')()); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); }); diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx index 45d7426d30..865564ba92 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocCommandsWizard.jsx @@ -134,8 +134,7 @@ const FormikApp = withFormik({ FormikApp.propTypes = { onLaunch: PropTypes.func.isRequired, - moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)) - .isRequired, + moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired, verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired, onCloseWizard: PropTypes.func.isRequired, credentialTypeId: PropTypes.number.isRequired, diff --git a/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx b/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx index e2f8898c98..8947d81846 100644 --- a/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx +++ b/awx/ui_next/src/components/AdHocCommands/AdHocDetailsStep.jsx @@ -110,7 +110,6 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) { label={i18n._(t`Arguments`)} validated={isValid ? 'default' : 'error'} onBlur={() => argumentsHelpers.setTouched(true)} - placeholder={i18n._(t`Enter arguments`)} isRequired={ moduleNameField.value === 'command' || moduleNameField.value === 'shell' @@ -317,8 +316,7 @@ function AdHocDetailsStep({ i18n, verbosityOptions, moduleOptions }) { } AdHocDetailsStep.propTypes = { - moduleOptions: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)) - .isRequired, + moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired, verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired, }; diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 91e8981f82..df1ebe2177 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -2,14 +2,8 @@ import React, { useEffect, useCallback, useState } from 'react'; import { useHistory, useLocation, useParams } 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, mergeParams, parseQueryString } from '../../../util/qs'; -import { GroupsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api'; +import { GroupsAPI, InventoriesAPI } from '../../../api'; import useRequest, { useDeleteItems, @@ -22,7 +16,6 @@ import ErrorDetail from '../../../components/ErrorDetail'; import PaginatedDataList from '../../../components/PaginatedDataList'; import AssociateModal from '../../../components/AssociateModal'; import DisassociateButton from '../../../components/DisassociateButton'; -import { Kebabified } from '../../../contexts/Kebabified'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; import InventoryGroupHostListItem from './InventoryGroupHostListItem'; import AddDropdown from '../shared/AddDropdown'; @@ -35,7 +28,6 @@ const QS_CONFIG = getQSConfig('host', { function InventoryGroupHostList({ i18n }) { const [isModalOpen, setIsModalOpen] = useState(false); - const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); const { id: inventoryId, groupId } = useParams(); const location = useLocation(); const history = useHistory(); @@ -47,9 +39,6 @@ function InventoryGroupHostList({ i18n }) { actions, relatedSearchableKeys, searchableKeys, - moduleOptions, - credentialTypeId, - isAdHocDisabled, }, error: contentError, isLoading, @@ -57,16 +46,9 @@ function InventoryGroupHostList({ i18n }) { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [ - response, - actionsResponse, - adHocOptions, - cred, - ] = await Promise.all([ + const [response, actionsResponse] = await Promise.all([ GroupsAPI.readAllHosts(groupId, params), InventoriesAPI.readHostsOptions(inventoryId), - InventoriesAPI.readAdHocOptions(inventoryId), - CredentialTypesAPI.read({ namespace: 'ssh' }), ]); return { @@ -79,9 +61,6 @@ function InventoryGroupHostList({ i18n }) { searchableKeys: Object.keys( actionsResponse.data.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), - moduleOptions: adHocOptions.data.actions.GET.module_name.choices, - credentialTypeId: cred.data.results[0].id, - isAdHocDisabled: !adHocOptions.data.actions.POST, }; }, [groupId, inventoryId, location.search]), { @@ -90,8 +69,6 @@ function InventoryGroupHostList({ i18n }) { actions: {}, relatedSearchableKeys: [], searchableKeys: [], - moduleOptions: [], - isAdHocDisabled: true, } ); @@ -230,40 +207,10 @@ function InventoryGroupHostList({ i18n }) { qsConfig={QS_CONFIG} additionalControls={[ ...(canAdd ? [addButton] : []), - - {({ isKebabified }) => - isKebabified ? ( - setIsAdHocCommandsOpen(true)} - isDisabled={hostCount === 0 || isAdHocDisabled} - > - {i18n._(t`Run command`)} - - ) : ( - - - - - - ) - } - , + 0} + />, )} - {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(); }); @@ -107,29 +96,6 @@ describe('', () => { }); }); - test('should render enabled ad hoc commands button', async () => { - GroupsAPI.readAllHosts.mockResolvedValue({ - data: { ...mockHosts }, - }); - InventoriesAPI.readHostsOptions.mockResolvedValue({ - data: { - actions: { - GET: {}, - POST: {}, - }, - }, - }); - await act(async () => { - wrapper = mountWithContexts(); - }); - - await waitForElement( - wrapper, - 'button[aria-label="Run command"]', - el => el.prop('disabled') === false - ); - }); - test('should show add dropdown button according to permissions', async () => { expect(wrapper.find('AddDropdown').length).toBe(1); InventoriesAPI.readHostsOptions.mockResolvedValueOnce({ diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx index 66277ab537..8c8676faf1 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -2,17 +2,11 @@ import React, { useCallback, useState, useEffect } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Button, - Tooltip, - DropdownItem, - ToolbarGroup, - ToolbarItem, -} from '@patternfly/react-core'; +import { Button, Tooltip } from '@patternfly/react-core'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import useSelected from '../../../util/useSelected'; import useRequest from '../../../util/useRequest'; -import { InventoriesAPI, GroupsAPI, CredentialTypesAPI } from '../../../api'; +import { InventoriesAPI, GroupsAPI } from '../../../api'; import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; import DataListToolbar from '../../../components/DataListToolbar'; @@ -24,7 +18,6 @@ import InventoryGroupItem from './InventoryGroupItem'; import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; -import { Kebabified } from '../../../contexts/Kebabified'; const QS_CONFIG = getQSConfig('group', { page: 1, @@ -52,7 +45,7 @@ const useModal = () => { function InventoryGroupsList({ i18n }) { const [deletionError, setDeletionError] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); + const location = useLocation(); const { isModalOpen, toggleModal } = useModal(); const { id: inventoryId } = useParams(); @@ -64,9 +57,6 @@ function InventoryGroupsList({ i18n }) { actions, relatedSearchableKeys, searchableKeys, - moduleOptions, - credentialTypeId, - isAdHocDisabled, }, error: contentError, isLoading, @@ -74,11 +64,9 @@ function InventoryGroupsList({ i18n }) { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [response, groupOptions, adHocOptions, cred] = await Promise.all([ + const [response, groupOptions] = await Promise.all([ InventoriesAPI.readGroups(inventoryId, params), InventoriesAPI.readGroupsOptions(inventoryId), - InventoriesAPI.readAdHocOptions(inventoryId), - CredentialTypesAPI.read({ namespace: 'ssh' }), ]); return { @@ -91,9 +79,6 @@ function InventoryGroupsList({ i18n }) { searchableKeys: Object.keys( groupOptions.data.actions?.GET || {} ).filter(key => groupOptions.data.actions?.GET[key].filterable), - moduleOptions: adHocOptions.data.actions.GET.module_name.choices, - credentialTypeId: cred.data.results[0].id, - isAdHocDisabled: !adHocOptions.data.actions.POST, }; }, [inventoryId, location]), { @@ -161,29 +146,6 @@ function InventoryGroupsList({ i18n }) { }; const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const kebabedAdditionalControls = () => { - return ( - <> - setIsAdHocCommandsOpen(true)} - isDisabled={groupCount === 0 || isAdHocDisabled} - > - {i18n._(t`Run command`)} - - - - {i18n._(t`Delete`)} - - - ); - }; return ( <> @@ -253,57 +215,24 @@ function InventoryGroupsList({ i18n }) { />, ] : []), - - {({ isKebabified }) => ( - <> - {isKebabified ? ( - kebabedAdditionalControls() - ) : ( - - - - - - - - -
- -
-
-
-
- )} - - )} -
, + 0} + />, + +
+ +
+
, ]} /> )} @@ -316,16 +245,6 @@ function InventoryGroupsList({ i18n }) { ) } /> - {isAdHocCommandsOpen && ( - setIsAdHocCommandsOpen(false)} - credentialTypeId={credentialTypeId} - moduleOptions={moduleOptions} - /> - )} {deletionError && ( ', () => { }, }, }); - 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'], }); @@ -158,13 +147,6 @@ describe('', () => { expect(el.props().checked).toBe(false); }); }); - test('should render enabled ad hoc commands button', async () => { - await waitForElement( - wrapper, - 'button[aria-label="Run command"]', - el => el.prop('disabled') === false - ); - }); }); describe(' error handling', () => { let wrapper; @@ -194,16 +176,6 @@ describe(' error handling', () => { }, }) ); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { module_name: { choices: [['module']] } }, - }, - }, - }); - CredentialTypesAPI.read.mockResolvedValue({ - data: { count: 1, results: [{ id: 1, name: 'cred' }] }, - }); }); afterEach(() => { jest.clearAllMocks(); @@ -230,21 +202,8 @@ describe(' error handling', () => { }); test('should show error modal when group is not successfully deleted from api', async () => { - const history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/3/groups'], - }); - await act(async () => { - wrapper = mountWithContexts( - - - , - { - context: { - router: { history, route: { location: history.location } }, - }, - } - ); + wrapper = mountWithContexts(); }); waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); @@ -281,27 +240,4 @@ describe(' error handling', () => { .invoke('onClose')(); }); }); - test('should render disabled ad hoc button', async () => { - const history = createMemoryHistory({ - initialEntries: ['/inventories/inventory/3/groups'], - }); - - await act(async () => { - wrapper = mountWithContexts( - - - , - { - context: { - router: { history, route: { location: history.location } }, - }, - } - ); - }); - - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - expect( - wrapper.find('button[aria-label="Run command"]').prop('disabled') - ).toBe(true); - }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 8aa095a7a8..74d929d779 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -2,19 +2,13 @@ import React, { useState, useEffect, useCallback } 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, mergeParams } from '../../../util/qs'; import useRequest, { useDismissableError, useDeleteItems, } from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; -import { HostsAPI, InventoriesAPI, CredentialTypesAPI } from '../../../api'; +import { HostsAPI, InventoriesAPI } from '../../../api'; import DataListToolbar from '../../../components/DataListToolbar'; import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; @@ -23,7 +17,6 @@ import PaginatedDataList, { } from '../../../components/PaginatedDataList'; import AssociateModal from '../../../components/AssociateModal'; import DisassociateButton from '../../../components/DisassociateButton'; -import { Kebabified } from '../../../contexts/Kebabified'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; import InventoryHostGroupItem from './InventoryHostGroupItem'; @@ -35,7 +28,6 @@ const QS_CONFIG = getQSConfig('group', { function InventoryHostGroupsList({ i18n }) { const [isModalOpen, setIsModalOpen] = useState(false); - const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); const { hostId, id: invId } = useParams(); const { search } = useLocation(); @@ -46,9 +38,6 @@ function InventoryHostGroupsList({ i18n }) { actions, relatedSearchableKeys, searchableKeys, - moduleOptions, - isAdHocDisabled, - credentialTypeId, }, error: contentError, isLoading, @@ -62,13 +51,9 @@ function InventoryHostGroupsList({ i18n }) { data: { count, results }, }, hostGroupOptions, - adHocOptions, - cred, ] = await Promise.all([ HostsAPI.readAllGroups(hostId, params), HostsAPI.readGroupsOptions(hostId), - InventoriesAPI.readAdHocOptions(invId), - CredentialTypesAPI.read({ namespace: 'ssh' }), ]); return { @@ -81,9 +66,6 @@ function InventoryHostGroupsList({ i18n }) { searchableKeys: Object.keys( hostGroupOptions.data.actions?.GET || {} ).filter(key => hostGroupOptions.data.actions?.GET[key].filterable), - moduleOptions: adHocOptions.data.actions.GET.module_name.choices, - credentialTypeId: cred.data.results[0].id, - isAdHocDisabled: !adHocOptions.data.actions.POST, }; }, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps { @@ -92,8 +74,6 @@ function InventoryHostGroupsList({ i18n }) { actions: {}, relatedSearchableKeys: [], searchableKeys: [], - moduleOptions: [], - isAdHocDisabled: true, } ); @@ -222,40 +202,10 @@ function InventoryHostGroupsList({ i18n }) { />, ] : []), - - {({ isKebabified }) => - isKebabified ? ( - setIsAdHocCommandsOpen(true)} - isDisabled={itemCount === 0 || isAdHocDisabled} - > - {i18n._(t`Run command`)} - - ) : ( - - - - - - ) - } - , + 0} + />, )} - {isAdHocCommandsOpen && ( - setIsAdHocCommandsOpen(false)} - credentialTypeId={credentialTypeId} - moduleOptions={moduleOptions} - /> - )} {error && ( ', () => { }, }, }); - 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'], }); @@ -283,11 +272,4 @@ 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 73bd3516b0..fbee9ae6d5 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHosts/InventoryHostList.jsx @@ -2,14 +2,8 @@ import React, { useEffect, useState, useCallback } 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, CredentialTypesAPI } from '../../../api'; +import { InventoriesAPI, HostsAPI } from '../../../api'; import useRequest, { useDeleteItems } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import DataListToolbar from '../../../components/DataListToolbar'; @@ -18,7 +12,6 @@ import PaginatedDataList, { ToolbarAddButton, ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; -import { Kebabified } from '../../../contexts/Kebabified'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; import InventoryHostItem from './InventoryHostItem'; @@ -29,7 +22,6 @@ const QS_CONFIG = getQSConfig('host', { }); function InventoryHostList({ i18n }) { - const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); const [selected, setSelected] = useState([]); const { id } = useParams(); const { search } = useLocation(); @@ -41,9 +33,6 @@ function InventoryHostList({ i18n }) { actions, relatedSearchableKeys, searchableKeys, - moduleOptions, - credentialTypeId, - isAdHocDisabled, }, error: contentError, isLoading, @@ -51,11 +40,9 @@ function InventoryHostList({ i18n }) { } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); - const [response, hostOptions, adHocOptions, cred] = await Promise.all([ + const [response, hostOptions] = await Promise.all([ InventoriesAPI.readHosts(id, params), InventoriesAPI.readHostsOptions(id), - InventoriesAPI.readAdHocOptions(id), - CredentialTypesAPI.read({ namespace: 'ssh' }), ]); return { @@ -68,9 +55,6 @@ function InventoryHostList({ i18n }) { 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]), { @@ -79,8 +63,6 @@ function InventoryHostList({ i18n }) { actions: {}, relatedSearchableKeys: [], searchableKeys: [], - moduleOptions: [], - isAdHocDisabled: true, } ); @@ -162,40 +144,10 @@ function InventoryHostList({ i18n }) { />, ] : []), - - {({ isKebabified }) => - isKebabified ? ( - setIsAdHocCommandsOpen(true)} - isDisabled={hostCount === 0 || isAdHocDisabled} - aria-label={i18n._(t`Run command`)} - > - {i18n._(t`Run command`)} - - ) : ( - - - - - - ) - } - , + 0} + />, - {isAdHocCommandsOpen && ( - setIsAdHocCommandsOpen(false)} - credentialTypeId={credentialTypeId} - moduleOptions={moduleOptions} - itemId={parseInt(id, 10)} - /> - )} {deletionError && ( ', () => { }, }, }); - 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(); }); @@ -276,13 +265,6 @@ 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.readHostsOptions.mockResolvedValueOnce({ data: { diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx index 6860f18101..eb171006c4 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx @@ -1,12 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Button, - Tooltip, - DropdownItem, - ToolbarItem, -} from '@patternfly/react-core'; import { useParams, useLocation, useHistory } from 'react-router-dom'; import { GroupsAPI, InventoriesAPI } from '../../../api'; @@ -18,7 +12,6 @@ import DataListToolbar from '../../../components/DataListToolbar'; import PaginatedDataList from '../../../components/PaginatedDataList'; import InventoryGroupRelatedGroupListItem from './InventoryRelatedGroupListItem'; import AddDropdown from '../shared/AddDropdown'; -import { Kebabified } from '../../../contexts/Kebabified'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; import AssociateModal from '../../../components/AssociateModal'; import DisassociateButton from '../../../components/DisassociateButton'; @@ -157,56 +150,10 @@ function InventoryRelatedGroupList({ i18n }) { qsConfig={QS_CONFIG} additionalControls={[ ...(canAdd ? [addButton] : []), - - {({ isKebabified }) => - isKebabified ? ( - - {({ openAdHocCommands, isDisabled }) => ( - - {i18n._(t`Run command`)} - - )} - - ) : ( - - - - {({ openAdHocCommands, isDisabled }) => ( - - )} - - - - ) - } - , + 0} + />, {}} diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx index 4322463741..52e0640518 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx @@ -1,22 +1,15 @@ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { 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 DataListToolbar from '../../../components/DataListToolbar'; import PaginatedDataList from '../../../components/PaginatedDataList'; import SmartInventoryHostListItem from './SmartInventoryHostListItem'; import useRequest from '../../../util/useRequest'; import useSelected from '../../../util/useSelected'; import { getQSConfig, parseQueryString } from '../../../util/qs'; -import { InventoriesAPI, CredentialTypesAPI } from '../../../api'; +import { InventoriesAPI } from '../../../api'; import { Inventory } from '../../../types'; -import { Kebabified } from '../../../contexts/Kebabified'; import AdHocCommands from '../../../components/AdHocCommands/AdHocCommands'; const QS_CONFIG = getQSConfig('host', { @@ -27,35 +20,27 @@ const QS_CONFIG = getQSConfig('host', { function SmartInventoryHostList({ i18n, inventory }) { const location = useLocation(); - const [isAdHocCommandsOpen, setIsAdHocCommandsOpen] = useState(false); const { - result: { hosts, count, moduleOptions, credentialTypeId, isAdHocDisabled }, + result: { hosts, count }, error: contentError, isLoading, request: fetchHosts, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const [hostResponse, adHocOptions, cred] = await Promise.all([ - InventoriesAPI.readHosts(inventory.id, params), - InventoriesAPI.readAdHocOptions(inventory.id), - CredentialTypesAPI.read({ namespace: 'ssh' }), - ]); + const { + data: { results, count: hostCount }, + } = await InventoriesAPI.readHosts(inventory.id, params); return { - 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, + hosts: results, + count: hostCount, }; }, [location.search, inventory.id]), { hosts: [], count: 0, - moduleOptions: [], - isAdHocDisabled: true, } ); @@ -110,38 +95,10 @@ function SmartInventoryHostList({ i18n, inventory }) { additionalControls={ inventory?.summary_fields?.user_capabilities?.adhoc ? [ - - {({ isKebabified }) => - isKebabified ? ( - setIsAdHocCommandsOpen(true)} - isDisabled={count === 0 || isAdHocDisabled} - > - {i18n._(t`Run command`)} - - ) : ( - - - - - - ) - } - , + 0} + />, ] : [] } @@ -157,16 +114,6 @@ function SmartInventoryHostList({ i18n, inventory }) { /> )} /> - {isAdHocCommandsOpen && ( - setIsAdHocCommandsOpen(false)} - credentialTypeId={credentialTypeId} - moduleOptions={moduleOptions} - /> - )} ); } 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 60fd23d75a..6f7c743a3a 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, CredentialTypesAPI } from '../../../api'; +import { InventoriesAPI } from '../../../api'; import { mountWithContexts, waitForElement, @@ -27,17 +27,6 @@ describe('', () => { InventoriesAPI.readHosts.mockResolvedValue({ data: mockHosts, }); - 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( @@ -60,15 +49,6 @@ describe('', () => { expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3); }); - test('should have run command button', () => { - wrapper.find('DataListCheck').forEach(el => { - expect(el.props().checked).toBe(false); - }); - const runCommandsButton = wrapper.find('button[aria-label="Run command"]'); - expect(runCommandsButton.length).toBe(1); - expect(runCommandsButton.prop('disabled')).toBe(false); - }); - test('should select and deselect all items', async () => { act(() => { wrapper.find('DataListToolbar').invoke('onSelectAll')(true); @@ -97,24 +77,4 @@ describe('', () => { }); await waitForElement(wrapper, 'ContentError', el => el.length === 1); }); - test('should disable run commands button', async () => { - InventoriesAPI.readHosts.mockResolvedValue({ - data: { results: [], count: 0 }, - }); - InventoriesAPI.readAdHocOptions.mockResolvedValue({ - data: { - actions: { - GET: { module_name: { choices: [['module']] } }, - }, - }, - }); - await act(async () => { - wrapper = mountWithContexts( - - ); - }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - const runCommandsButton = wrapper.find('button[aria-label="Run command"]'); - expect(runCommandsButton.prop('disabled')).toBe(true); - }); }); diff --git a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx index f85c619e3d..1f6820fd7f 100644 --- a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx @@ -48,7 +48,7 @@ function AddDropdown({ dropdownItems, i18n }) {