diff --git a/awx/ui_next/src/api/models/Metrics.js b/awx/ui_next/src/api/models/Metrics.js new file mode 100644 index 0000000000..e808d26662 --- /dev/null +++ b/awx/ui_next/src/api/models/Metrics.js @@ -0,0 +1,9 @@ +import Base from '../Base'; + +class Metrics extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/inventories/'; + } +} +export default Metrics; diff --git a/awx/ui_next/src/components/AlertModal/AlertModal.jsx b/awx/ui_next/src/components/AlertModal/AlertModal.jsx index 6d238943dd..0c443300be 100644 --- a/awx/ui_next/src/components/AlertModal/AlertModal.jsx +++ b/awx/ui_next/src/components/AlertModal/AlertModal.jsx @@ -77,7 +77,7 @@ function AlertModal({ aria-label={label || i18n._(t`Alert modal`)} aria-labelledby="alert-modal-header-label" isOpen={Boolean(isOpen)} - variant="medium" + variant="small" title={title} {...props} > diff --git a/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx b/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx index 2810854e64..7d82a3239d 100644 --- a/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx +++ b/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; -import { Button, Badge, Alert } from '@patternfly/react-core'; +import { Button, Badge, Alert, Tooltip } from '@patternfly/react-core'; import AlertModal from '../AlertModal'; import { getRelatedResourceDeleteCounts } from '../../util/getRelatedResourceDeleteDetails'; import ErrorDetail from '../ErrorDetail'; @@ -11,9 +11,9 @@ import ErrorDetail from '../ErrorDetail'; const WarningMessage = styled(Alert)` margin-top: 10px; `; -const DetailsWrapper = styled.span` - :not(:first-of-type) { - padding-left: 10px; +const Label = styled.span` + && { + margin-right: 10px; } `; function DeleteButton({ @@ -27,11 +27,11 @@ function DeleteButton({ ouiaId, deleteMessage, deleteDetailsRequests, + disabledTooltip, }) { const [isOpen, setIsOpen] = useState(false); const [deleteMessageError, setDeleteMessageError] = useState(); const [deleteDetails, setDeleteDetails] = useState({}); - const toggleModal = async isModalOpen => { if (deleteDetailsRequests?.length && isModalOpen) { const { results, error } = await getRelatedResourceDeleteCounts( @@ -62,15 +62,29 @@ function DeleteButton({ } return ( <> - - + {disabledTooltip ? ( + +
+ +
+
+ ) : ( + + )} { + onConfirm(); + toggleModal(false); + }} > {i18n._(t`Delete`)} , @@ -110,9 +127,9 @@ function DeleteButton({
{deleteMessage}

{Object.entries(deleteDetails).map(([key, value]) => ( - - {key} {value} - +
+ {value} +
))} } diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx index 2f5d4e4ac0..0f74b05fcf 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx @@ -28,9 +28,10 @@ import ErrorDetail from '../ErrorDetail'; const WarningMessage = styled(Alert)` margin-top: 10px; `; -const DetailsWrapper = styled.span` - :not(:first-of-type) { - padding-left: 10px; + +const Label = styled.span` + && { + margin-right: 10px; } `; @@ -99,11 +100,7 @@ function ToolbarDeleteButton({ }) { const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext); const [isModalOpen, setIsModalOpen] = useState(false); - const [deleteDetails, setDeleteDetails] = useState({}); - - const deleteMessages = [warningMessage, deleteMessage].filter( - message => message - ); + const [deleteDetails, setDeleteDetails] = useState(null); const [deleteMessageError, setDeleteMessageError] = useState(); const handleDelete = () => { @@ -112,6 +109,7 @@ function ToolbarDeleteButton({ }; const toggleModal = async isOpen => { + setDeleteDetails(null); if ( isOpen && itemsToDelete.length === 1 && @@ -164,6 +162,36 @@ function ToolbarDeleteButton({ const isDisabled = itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete); + const buildDeleteWarning = () => { + const deleteMessages = []; + if (warningMessage) { + deleteMessages.push(warningMessage); + } + if (deleteMessage) { + if (itemsToDelete[0]?.type !== 'inventory') { + deleteMessages.push(deleteMessage); + } else if (deleteDetails || itemsToDelete.length > 1) { + deleteMessages.push(deleteMessage); + } + } + return ( +
+ {deleteMessages.map(message => ( +
+ {message} +
+ ))} + {deleteDetails && + Object.entries(deleteDetails).map(([key, value]) => ( +
+ + {value} +
+ ))} +
+ ); + }; + if (deleteMessageError) { return ( {i18n._(t`Delete`)} @@ -239,42 +270,11 @@ function ToolbarDeleteButton({
))} - {itemsToDelete.length === 1 && - Object.values(deleteDetails).length > 0 && ( - - {deleteMessages.map(message => ( -
- {message} -
- ))} - {itemsToDelete.length === 1 && ( - <> -
- {Object.entries(deleteDetails).map(([key, value]) => ( - - {key} {value} - - ))} - - )} - - } - /> - )} - {itemsToDelete.length > 1 && ( + {(deleteDetails || deleteMessage || warningMessage) && ( ( -
{message}
- ))} + title={buildDeleteWarning()} /> )}
diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx index 12b25157cc..e4366a16eb 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx @@ -27,6 +27,7 @@ const itemC = { describe('', () => { let deleteDetailsRequests; + let wrapper; beforeEach(() => { deleteDetailsRequests = [ { @@ -35,8 +36,13 @@ describe('', () => { }, ]; }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); test('should render button', () => { - const wrapper = mountWithContexts( + wrapper = mountWithContexts( {}} itemsToDelete={[]} /> ); expect(wrapper.find('button')).toHaveLength(1); @@ -44,7 +50,6 @@ describe('', () => { }); test('should open confirmation modal', async () => { - let wrapper; await act(async () => { wrapper = mountWithContexts( ', () => { expect(CredentialsAPI.read).toBeCalled(); expect(wrapper.find('Modal')).toHaveLength(1); expect( - wrapper.find('span[aria-label="Workflow Job Template Node: 1"]') + wrapper.find('div[aria-label="Workflow Job Template Node: 1"]') ).toHaveLength(1); + expect( + wrapper.find('Button[aria-label="confirm delete"]').prop('isDisabled') + ).toBe(false); + expect(wrapper.find('div[aria-label="Delete this?"]')).toHaveLength(1); + }); + + test('should open confirmation with enabled delete button modal', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + itemsToDelete={[ + { + name: 'foo', + id: 1, + type: 'credential_type', + summary_fields: { user_capabilities: { delete: true } }, + }, + { + name: 'bar', + id: 2, + type: 'credential_type', + summary_fields: { user_capabilities: { delete: true } }, + }, + ]} + deleteDetailsRequests={deleteDetailsRequests} + deleteMessage="Delete this?" + warningMessage="Are you sure to want to delete this" + /> + ); + }); + + expect(wrapper.find('Modal')).toHaveLength(0); + await act(async () => { + wrapper.find('button').prop('onClick')(); + }); + await waitForElement(wrapper, 'Modal', el => el.length > 0); + expect(CredentialsAPI.read).not.toBeCalled(); + expect(wrapper.find('Modal')).toHaveLength(1); + expect( + wrapper.find('Button[aria-label="confirm delete"]').prop('isDisabled') + ).toBe(false); + }); + + test('should disable confirm delete button', async () => { + const request = [ + { + label: 'Workflow Job Template Node', + request: CredentialsAPI.read.mockResolvedValue({ data: { count: 3 } }), + }, + ]; + await act(async () => { + wrapper = mountWithContexts( + {}} + itemsToDelete={[ + { + name: 'foo', + id: 1, + type: 'credential_type', + summary_fields: { user_capabilities: { delete: true } }, + }, + ]} + deleteDetailsRequests={request} + deleteMessage="Delete this?" + warningMessage="Are you sure to want to delete this" + /> + ); + }); + + expect(wrapper.find('Modal')).toHaveLength(0); + await act(async () => { + wrapper.find('button').prop('onClick')(); + }); + await waitForElement(wrapper, 'Modal', el => el.length > 0); + expect(CredentialsAPI.read).toBeCalled(); + expect(wrapper.find('Modal')).toHaveLength(1); + + expect( + wrapper.find('Button[aria-label="confirm delete"]').prop('isDisabled') + ).toBe(true); expect(wrapper.find('div[aria-label="Delete this?"]')).toHaveLength(1); }); @@ -88,7 +174,7 @@ describe('', () => { ), }, ]; - let wrapper; + await act(async () => { wrapper = mountWithContexts( ', () => { test('should invoke onDelete prop', () => { const onDelete = jest.fn(); - const wrapper = mountWithContexts( + wrapper = mountWithContexts( ); wrapper.find('button').simulate('click'); @@ -127,14 +213,14 @@ describe('', () => { }); test('should disable button when no delete permissions', () => { - const wrapper = mountWithContexts( + wrapper = mountWithContexts( {}} itemsToDelete={[itemB]} /> ); expect(wrapper.find('button[disabled]')).toHaveLength(1); }); test('should render tooltip', () => { - const wrapper = mountWithContexts( + wrapper = mountWithContexts( {}} itemsToDelete={[itemA]} /> ); expect(wrapper.find('Tooltip')).toHaveLength(1); @@ -142,7 +228,7 @@ describe('', () => { }); test('should render tooltip for username', () => { - const wrapper = mountWithContexts( + wrapper = mountWithContexts( {}} itemsToDelete={[itemC]} /> ); expect(wrapper.find('Tooltip')).toHaveLength(1); diff --git a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/DeleteRoleConfirmationModal.test.jsx.snap b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/DeleteRoleConfirmationModal.test.jsx.snap index b6a72aee71..a1dd54a0a3 100644 --- a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/DeleteRoleConfirmationModal.test.jsx.snap +++ b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/DeleteRoleConfirmationModal.test.jsx.snap @@ -119,7 +119,7 @@ exports[` should render initially 1`] = ` title="Remove Team Access" titleIconVariant={null} titleLabel="" - variant="medium" + variant="small" > should render initially 1`] = ` aria-label="Alert modal" aria-labelledby="pf-modal-part-0 alert-modal-header-label pf-modal-part-1" aria-modal="true" - class="pf-c-modal-box pf-m-md" - data-ouia-component-id="OUIA-Generated-Modal-medium-1" + class="pf-c-modal-box pf-m-sm" + data-ouia-component-id="OUIA-Generated-Modal-small-1" data-ouia-component-type="PF4/ModalContent" data-ouia-safe="true" id="pf-modal-part-0" @@ -277,13 +277,13 @@ exports[` should render initially 1`] = ` isOpen={true} labelId="pf-modal-part-1" onClose={[Function]} - ouiaId="OUIA-Generated-Modal-medium-1" + ouiaId="OUIA-Generated-Modal-small-1" ouiaSafe={true} showClose={true} title="Remove Team Access" titleIconVariant={null} titleLabel="" - variant="medium" + variant="small" >
should render initially 1`] = ` aria-label="Alert modal" aria-labelledby="pf-modal-part-0 alert-modal-header-label pf-modal-part-1" className="" - data-ouia-component-id="OUIA-Generated-Modal-medium-1" + data-ouia-component-id="OUIA-Generated-Modal-small-1" data-ouia-component-type="PF4/ModalContent" data-ouia-safe={true} id="pf-modal-part-0" style={Object {}} - variant="medium" + variant="small" >
', () => { test('should have proper number of delete detail requests', () => { expect( wrapper.find('DeleteButton').prop('deleteDetailsRequests') - ).toHaveLength(4); + ).toHaveLength(6); }); test('should render details', () => { diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx index 6883a9ac26..e07ab770c5 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.test.jsx @@ -43,7 +43,7 @@ describe('', () => { test('should have proper number of delete detail requests', () => { expect( wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests') - ).toHaveLength(4); + ).toHaveLength(6); }); test('should fetch credentials from api and render the in the list', () => { diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx index 0d0cbd9643..087757d6aa 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; @@ -16,7 +16,11 @@ import { import useRequest, { useDismissableError } from '../../../util/useRequest'; import { CredentialTypesAPI } from '../../../api'; import { jsonToYaml } from '../../../util/yaml'; -import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; +import { + relatedResourceDeleteRequests, + getRelatedResourceDeleteCounts, +} from '../../../util/getRelatedResourceDeleteDetails'; +import ErrorDetail from '../../../components/ErrorDetail'; function CredentialTypeDetails({ credentialType, i18n }) { const { id, name, description, injectors, inputs } = credentialType; @@ -33,12 +37,35 @@ function CredentialTypeDetails({ credentialType, i18n }) { }, [id, history]) ); - const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType( - credentialType, - i18n + const { + result: { isDeleteDisabled }, + error: deleteDetailsError, + request: fetchDeleteDetails, + } = useRequest( + useCallback(async () => { + const { + results: deleteDetails, + error, + } = await getRelatedResourceDeleteCounts( + relatedResourceDeleteRequests.credentialType(credentialType, i18n) + ); + if (error) { + throw new Error(error); + } + if (deleteDetails) { + return { isDeleteDisabled: true }; + } + return { isDeleteDisabled: false }; + }, [credentialType, i18n]), + { isDeleteDisabled: false } ); - const { error, dismissError } = useDismissableError(deleteError); + useEffect(() => { + fetchDeleteDetails(); + }, [fetchDeleteDetails]); + const { error, dismissError } = useDismissableError( + deleteError || deleteDetailsError + ); return ( @@ -88,11 +115,13 @@ function CredentialTypeDetails({ credentialType, i18n }) { name={name} modalTitle={i18n._(t`Delete credential type`)} onConfirm={deleteCredentialType} - isDisabled={isLoading} - deleteDetailsRequests={deleteDetailsRequests} - deleteMessage={i18n._( - t`This credential type is currently being used by some credentials. Are you sure you want to delete it?` - )} + isDisabled={isLoading || isDeleteDisabled} + disabledTooltip={ + isDeleteDisabled && + i18n._( + t`This credential type is currently being used by some credentials and cannot be deleted` + ) + } > {i18n._(t`Delete`)} @@ -105,7 +134,9 @@ function CredentialTypeDetails({ credentialType, i18n }) { onClose={dismissError} title={i18n._(t`Error`)} variant="error" - /> + > + + )} ); diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.test.jsx index 4ba649dd99..45d7a4feb8 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.test.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeDetails/CredentialTypeDetails.test.jsx @@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import { CredentialTypesAPI } from '../../../api'; +import { CredentialTypesAPI, CredentialsAPI } from '../../../api'; import { jsonToYaml } from '../../../util/yaml'; import CredentialTypeDetails from './CredentialTypeDetails'; @@ -66,6 +66,10 @@ function expectDetailToMatch(wrapper, label, value) { describe('', () => { let wrapper; + afterEach(() => { + wrapper.unmount(); + jest.clearAllMocks(); + }); test('should render details properly', async () => { await act(async () => { wrapper = mountWithContexts( @@ -92,10 +96,36 @@ describe('', () => { ); }); - test('should have proper number of delete detail requests', () => { - expect( - wrapper.find('DeleteButton').prop('deleteDetailsRequests') - ).toHaveLength(1); + test('should disabled delete and show proper tooltip requests', async () => { + CredentialsAPI.read.mockResolvedValue({ data: { count: 15 } }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + + expect(wrapper.find('DeleteButton').prop('disabledTooltip')).toBe( + 'This credential type is currently being used by some credentials and cannot be deleted' + ); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + expect(wrapper.find('Tooltip').length).toBe(1); + expect(wrapper.find('Tooltip').prop('content')).toBe( + 'This credential type is currently being used by some credentials and cannot be deleted' + ); + }); + + test('should throw error', async () => { + CredentialsAPI.read.mockRejectedValue(new Error('error')); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); }); test('expected api call is made for delete', async () => { diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx index 71452c10b6..e702ee8768 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx @@ -169,7 +169,7 @@ function CredentialTypeList({ i18n }) { pluralizedItemName={i18n._(t`Credential Types`)} deleteDetailsRequests={deleteDetailsRequests} deleteMessage={i18n._( - '{numItemsToDelete, plural, one {This credential type is currently being used by some credentials. Are you sure you want to delete it?} other {Deleting these credential types could impact other credentials that rely on them. Are you sure you want to delete anyway?}}', + '{numItemsToDelete, plural, one {This credential type is currently being used by some credentials and cannot be deleted.} other {Credential types that are being used by credentials cannot be deleted. Are you sure you want to delete anyway?}}', { numItemsToDelete: selected.length } )} />, diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index 5357b00ca9..a5c89f8d89 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -15,6 +15,7 @@ import { import useRequest, { useDismissableError } from '../../../util/useRequest'; import { toTitleCase } from '../../../util/strings'; import { ExecutionEnvironmentsAPI } from '../../../api'; +import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { const history = useHistory(); @@ -41,7 +42,10 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { ); const { error, dismissError } = useDismissableError(deleteError); - + const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment( + executionEnvironment, + i18n + ); return ( @@ -120,6 +124,10 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { onConfirm={deleteExecutionEnvironment} isDisabled={isLoading} ouiaId="delete-button" + deleteDetailsRequests={deleteDetailsRequests} + deleteMessage={i18n._( + t`This execution environment is currently being used by other resources. Are you sure you want to delete it?` + )} > {i18n._(t`Delete`)} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx index abfb08e094..90c57e1cc8 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx @@ -175,4 +175,22 @@ describe('', () => { expect(wrapper.find('Button[aria-label="Delete"]')).toHaveLength(0); }); + test('should have proper number of delete detail requests', async () => { + const history = createMemoryHistory({ + initialEntries: ['/execution_environments/42/details'], + }); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + expect( + wrapper.find('DeleteButton').prop('deleteDetailsRequests') + ).toHaveLength(2); + }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx index 1490ff49e3..3a4fa01824 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx @@ -9,7 +9,7 @@ import { import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentList from './ExecutionEnvironmentList'; -jest.mock('../../../api/models/ExecutionEnvironments'); +jest.mock('../../../api'); const executionEnvironments = { data: { @@ -143,7 +143,6 @@ describe('', () => { wrapper.find('Button[aria-label="Delete"]').prop('onClick')() ); wrapper.update(); - await act(async () => wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')() ); @@ -185,4 +184,17 @@ describe('', () => { waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); expect(wrapper.find('ToolbarAddButton').length).toBe(0); }); + + test('should have proper number of delete detail requests', async () => { + ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: false } }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + expect( + wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests') + ).toHaveLength(2); + }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx index 914b884f1f..40c4e5d38d 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.jsx @@ -19,7 +19,7 @@ import PaginatedTable, { import ErrorDetail from '../../../components/ErrorDetail'; import AlertModal from '../../../components/AlertModal'; import DatalistToolbar from '../../../components/DataListToolbar'; - +import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; const QS_CONFIG = getQSConfig('execution_environments', { @@ -105,7 +105,10 @@ function ExecutionEnvironmentList({ i18n }) { }; const canAdd = actions && actions.POST; - + const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment( + selected[0], + i18n + ); return ( <> @@ -181,6 +184,11 @@ function ExecutionEnvironmentList({ i18n }) { onDelete={handleDelete} itemsToDelete={selected} pluralizedItemName={i18n._(t`Execution Environments`)} + deleteDetailsRequests={deleteDetailsRequests} + deleteMessage={i18n._( + '{numItemsToDelete, plural, one {This execution environment is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these execution environemnts could impact other resources that rely on them. Are you sure you want to delete anyway?}}', + { numItemsToDelete: selected.length } + )} />, ]} /> diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index 21dd1e2f14..9369eaebfb 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -165,7 +165,7 @@ function OrganizationDetail({ i18n, organization }) { isDisabled={isLoading} deleteDetailsRequests={deleteDetailsRequests} deleteMessage={i18n._( - t`This organization is currently being used some credentials. Are you sure you want to delete it?` + t`This organization is currently being by other resources. Are you sure you want to delete it?` )} > {i18n._(t`Delete`)} diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx index 2b5f60a44b..62ea8d3f74 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx @@ -77,7 +77,7 @@ describe('', () => { expect( component.find('DeleteButton').prop('deleteDetailsRequests') - ).toHaveLength(1); + ).toHaveLength(4); }); test('should render the expected instance group', async () => { diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index d88cff0d6e..d5dce4dcac 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -180,7 +180,7 @@ function OrganizationsList({ i18n }) { pluralizedItemName={i18n._(t`Organizations`)} deleteDetailsRequests={deleteDetailsRequests} deleteMessage={i18n._( - '{numItemsToDelete, plural, one {This organization is currently being used some credentials. Are you sure you want to delete it?} other {Deleting these organizations could impact some credentials that rely on them. Are you sure you want to delete anyway?}}', + '{numItemsToDelete, plural, one {This organization is currently being by other resources. Are you sure you want to delete it?} other {Deleting these organizations could impact other resources that rely on them. Are you sure you want to delete anyway?}}', { numItemsToDelete: selected.length } )} />, diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx index 7655bfdd1b..6e3d2003cc 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx @@ -102,7 +102,7 @@ describe('', () => { ); expect( wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests') - ).toHaveLength(1); + ).toHaveLength(4); }); test('Items are rendered after loading', async () => { diff --git a/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js b/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js index 27ceccfbb2..32f795704a 100644 --- a/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js +++ b/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js @@ -54,13 +54,13 @@ describe('delete details', () => { getRelatedResourceDeleteCounts( relatedResourceDeleteRequests.project({ id: 1 }, i18n) ); - expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({ - credentials: 1, + expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({ + unified_job_template: 1, }); expect(InventorySourcesAPI.read).toBeCalledWith({ - credentials__id: 1, + source_project: 1, }); - expect(JobTemplatesAPI.read).toBeCalledWith({ credentials: 1 }); + expect(JobTemplatesAPI.read).toBeCalledWith({ project: 1 }); }); test('should call api for templates list', () => { diff --git a/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js b/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js index 1a3a4bbbb7..76af4be6ee 100644 --- a/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js +++ b/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js @@ -1,6 +1,7 @@ import { t } from '@lingui/macro'; import { + UnifiedJobTemplatesAPI, CredentialsAPI, InventoriesAPI, InventorySourcesAPI, @@ -8,6 +9,12 @@ import { ProjectsAPI, WorkflowJobTemplateNodesAPI, WorkflowJobTemplatesAPI, + CredentialInputSourcesAPI, + TeamsAPI, + NotificationTemplatesAPI, + ExecutionEnvironmentsAPI, + ApplicationsAPI, + OrganizationsAPI, } from '../api'; export async function getRelatedResourceDeleteCounts(requests) { @@ -65,6 +72,20 @@ export const relatedResourceDeleteRequests = { }), label: i18n._(t`Inventory Sources`), }, + { + request: () => + CredentialInputSourcesAPI.read({ + source_credential: selected.id, + }), + label: i18n._(t`Credential`), + }, + { + request: () => + ExecutionEnvironmentsAPI.read({ + credential: selected.id, + }), + label: i18n._(t`Execution Environments`), + }, ], credentialType: (selected, i18n) => [ @@ -111,18 +132,21 @@ export const relatedResourceDeleteRequests = { { request: () => JobTemplatesAPI.read({ - credentials: selected.id, + project: selected.id, }), label: i18n._(t`Job Templates`), }, { - request: () => WorkflowJobTemplatesAPI.read({ credentials: selected.id }), + request: () => + WorkflowJobTemplateNodesAPI.read({ + unified_job_template: selected.id, + }), label: i18n._(t`Workflow Job Templates`), }, { request: () => InventorySourcesAPI.read({ - credentials__id: selected.id, + source_project: selected.id, }), label: i18n._(t`Inventory Sources`), }, @@ -134,7 +158,7 @@ export const relatedResourceDeleteRequests = { WorkflowJobTemplateNodesAPI.read({ unified_job_template: selected.id, }), - label: [i18n._(t`Workflow Job Template Node`)], + label: [i18n._(t`Workflow Job Template Nodes`)], }, ], @@ -146,5 +170,87 @@ export const relatedResourceDeleteRequests = { }), label: i18n._(t`Credential`), }, + { + request: async () => + TeamsAPI.read({ + organization: selected.id, + }), + label: i18n._(t`Teams`), + }, + { + request: async () => + NotificationTemplatesAPI.read({ + organization: selected.id, + }), + label: i18n._(t`Notification Templates`), + }, + { + request: () => + ExecutionEnvironmentsAPI.read({ + organization: selected.id, + }), + label: i18n._(t`Execution Environments`), + }, + { + request: async () => + ProjectsAPI.read({ + organization: selected.id, + }), + label: [i18n._(t`Projects`)], + }, + { + request: () => + InventoriesAPI.read({ + organization: selected.id, + }), + label: i18n._(t`Inventories`), + }, + { + request: () => + ApplicationsAPI.read({ + organization: selected.id, + }), + label: i18n._(t`Applications`), + }, + ], + executionEnvironment: (selected, i18n) => [ + { + request: async () => + UnifiedJobTemplatesAPI.read({ + execution_environment: selected.id, + }), + label: [i18n._(t`Templates`)], + }, + { + request: async () => + ProjectsAPI.read({ + default_environment: selected.id, + }), + label: [i18n._(t`Projects`)], + }, + { + request: async () => + OrganizationsAPI.read({ + execution_environment: selected.id, + }), + label: [i18n._(t`Organizations`)], + }, + { + request: async () => { + try { + const { data } = await WorkflowJobTemplateNodesAPI.read({ + execution_environment: selected.id, + }); + if ( + data.summary_fields.unified_job_template.unified_job_type === + 'inventory_update' + ) { + await InventorySourcesAPI.read(); + } + } catch {} + }, + + label: [i18n._(t`Organizations`)], + }, ], };