around the
below.
- // See: https://github.com/patternfly/patternfly-react/issues/1894
+ const buildDeleteWarning = () => {
+ const deleteMessages = [];
+ if (warningMessage) {
+ deleteMessages.push(warningMessage);
+ }
+ if (deleteMessage) {
+ if (
+ itemsToDelete[0]?.type !== 'inventory' &&
+ (itemsToDelete.length > 1 || deleteDetails)
+ ) {
+ 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 (
+ {
+ toggleModal(false);
+ setDeleteMessageError();
+ }}
+ >
+
+
+ );
+ }
+ const shouldShowDeleteWarning =
+ warningMessage ||
+ (itemsToDelete.length === 1 && deleteDetails) ||
+ (itemsToDelete.length > 1 && deleteMessage);
+
return (
<>
{isKebabified ? (
-
- {i18n._(t`Delete`)}
-
+
+ {
+ toggleModal(true);
+ }}
+ >
+ {i18n._(t`Delete`)}
+
+
) : (
)}
+
{isModalOpen && (
toggleModal(false)}
actions={[
,
@@ -186,8 +286,12 @@ function ToolbarDeleteButton({
))}
- {warningMessage && (
-
+ {shouldShowDeleteWarning && (
+
)}
)}
diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx
index 487f2c17f0..e4366a16eb 100644
--- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx
+++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.test.jsx
@@ -1,7 +1,14 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { act } from 'react-dom/test-utils';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../testUtils/enzymeHelpers';
+import { CredentialsAPI } from '../../api';
import ToolbarDeleteButton from './ToolbarDeleteButton';
+jest.mock('../../api');
+
const itemA = {
id: 1,
name: 'Foo',
@@ -19,27 +26,180 @@ const itemC = {
};
describe('', () => {
+ let deleteDetailsRequests;
+ let wrapper;
+ beforeEach(() => {
+ deleteDetailsRequests = [
+ {
+ label: 'Workflow Job Template Node',
+ request: CredentialsAPI.read.mockResolvedValue({ data: { count: 1 } }),
+ },
+ ];
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ wrapper.unmount();
+ });
test('should render button', () => {
- const wrapper = mountWithContexts(
+ wrapper = mountWithContexts(
{}} itemsToDelete={[]} />
);
expect(wrapper.find('button')).toHaveLength(1);
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
});
- test('should open confirmation modal', () => {
- const wrapper = mountWithContexts(
- {}} itemsToDelete={[itemA]} />
- );
+ test('should open confirmation modal', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ itemsToDelete={[itemA]}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage="Delete this?"
+ warningMessage="Are you sure to want to delete this"
+ />
+ );
+ });
+
expect(wrapper.find('Modal')).toHaveLength(0);
- wrapper.find('button').simulate('click');
- wrapper.update();
+ 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('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);
+ });
+
+ test('should open delete error modal', async () => {
+ const request = [
+ {
+ label: 'Workflow Job Template Node',
+ request: CredentialsAPI.read.mockRejectedValue(
+ new Error({
+ response: {
+ config: {
+ method: 'get',
+ url: '/api/v2/credentals',
+ },
+ data: 'An error occurred',
+ status: 403,
+ },
+ })
+ ),
+ },
+ ];
+
+ await act(async () => {
+ wrapper = mountWithContexts(
+ {}}
+ itemsToDelete={[itemA]}
+ 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').simulate('click'));
+ await waitForElement(wrapper, 'Modal', el => el.length > 0);
+ expect(CredentialsAPI.read).toBeCalled();
+
+ wrapper.update();
+
+ expect(wrapper.find('AlertModal[title="Error!"]')).toHaveLength(1);
});
test('should invoke onDelete prop', () => {
const onDelete = jest.fn();
- const wrapper = mountWithContexts(
+ wrapper = mountWithContexts(
);
wrapper.find('button').simulate('click');
@@ -53,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);
@@ -68,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/PaginatedDataList/__snapshots__/ToolbarDeleteButton.test.jsx.snap b/awx/ui_next/src/components/PaginatedDataList/__snapshots__/ToolbarDeleteButton.test.jsx.snap
index fc24195951..eb410530a8 100644
--- a/awx/ui_next/src/components/PaginatedDataList/__snapshots__/ToolbarDeleteButton.test.jsx.snap
+++ b/awx/ui_next/src/components/PaginatedDataList/__snapshots__/ToolbarDeleteButton.test.jsx.snap
@@ -75,6 +75,7 @@ exports[` should render button 1`] = `
@@ -95,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 92cad273f9..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,6 +96,38 @@ describe('
', () => {
);
});
+ 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 () => {
const history = createMemoryHistory({
initialEntries: ['/credential_types/42/details'],
diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx
index 7c491bd36a..e702ee8768 100644
--- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.jsx
+++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.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 CredentialTypeListItem from './CredentialTypeListItem';
const QS_CONFIG = getQSConfig('credential-type', {
@@ -106,6 +106,11 @@ function CredentialTypeList({ i18n }) {
const canAdd = actions && actions.POST;
+ const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType(
+ selected[0],
+ i18n
+ );
+
return (
<>
@@ -162,6 +167,11 @@ function CredentialTypeList({ i18n }) {
onDelete={handleDelete}
itemsToDelete={selected}
pluralizedItemName={i18n._(t`Credential Types`)}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ '{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/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx
index 16f5f4daf7..7a289b99f1 100644
--- a/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx
+++ b/awx/ui_next/src/screens/CredentialType/CredentialTypeList/CredentialTypeList.test.jsx
@@ -6,10 +6,11 @@ import {
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { CredentialTypesAPI } from '../../../api';
+import { CredentialTypesAPI, CredentialsAPI } from '../../../api';
import CredentialTypeList from './CredentialTypeList';
jest.mock('../../../api/models/CredentialTypes');
+jest.mock('../../../api/models/Credentials');
const credentialTypes = {
data: {
@@ -49,6 +50,12 @@ describe(' {
await waitForElement(wrapper, 'CredentialTypeList', el => el.length > 0);
});
+ test('should have proper number of delete detail requests', () => {
+ expect(
+ wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(1);
+ });
+
test('should have data fetched and render 2 rows', async () => {
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
CredentialTypesAPI.readOptions.mockResolvedValue(options);
@@ -65,6 +72,7 @@ describe(' {
test('should delete item successfully', async () => {
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
CredentialTypesAPI.readOptions.mockResolvedValue(options);
+ CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
await act(async () => {
wrapper = mountWithContexts();
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..ce0bf830ed 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(4);
+ });
});
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..fa7e766232 100644
--- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx
+++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnviromentList.test.jsx
@@ -6,10 +6,22 @@ import {
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { ExecutionEnvironmentsAPI } from '../../../api';
+import {
+ ExecutionEnvironmentsAPI,
+ InventorySourcesAPI,
+ WorkflowJobTemplateNodesAPI,
+ OrganizationsAPI,
+ ProjectsAPI,
+ UnifiedJobTemplatesAPI,
+} from '../../../api';
import ExecutionEnvironmentList from './ExecutionEnvironmentList';
jest.mock('../../../api/models/ExecutionEnvironments');
+jest.mock('../../../api/models/UnifiedJobTemplates');
+jest.mock('../../../api/models/Projects');
+jest.mock('../../../api/models/Organizations');
+jest.mock('../../../api/models/InventorySources');
+jest.mock('../../../api/models/WorkflowJobTemplateNodes');
const executionEnvironments = {
data: {
@@ -43,6 +55,16 @@ describe('', () => {
beforeEach(() => {
ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments);
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options);
+ InventorySourcesAPI.read.mockResolvedValue({
+ data: { results: [{ id: 10000000 }] },
+ });
+ WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
+
+ OrganizationsAPI.read.mockResolvedValue({ data: { count: 0 } });
+
+ UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+
+ ProjectsAPI.read.mockResolvedValue({ data: { count: 0 } });
});
afterEach(() => {
@@ -144,6 +166,11 @@ describe('', () => {
);
wrapper.update();
+ await waitForElement(
+ wrapper,
+ 'Button[aria-label="confirm delete"]',
+ el => el.length > 0
+ );
await act(async () =>
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
);
@@ -185,4 +212,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(4);
+ });
});
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/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
index 96845ea500..c04024f734 100644
--- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupDetails/ContainerGroupDetails.jsx
@@ -16,6 +16,7 @@ import {
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { jsonToYaml, isJsonString } from '../../../util/yaml';
import { InstanceGroupsAPI } from '../../../api';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function ContainerGroupDetails({ instanceGroup, i18n }) {
const { id, name } = instanceGroup;
@@ -34,7 +35,10 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
);
const { error, dismissError } = useDismissableError(deleteError);
-
+ const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
+ instanceGroup,
+ i18n
+ );
return (
@@ -101,6 +105,10 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
modalTitle={i18n._(t`Delete instance group`)}
onConfirm={deleteInstanceGroup}
isDisabled={isLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This container group 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/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx
index 62b723f294..c5d435320c 100644
--- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx
@@ -16,6 +16,7 @@ import {
} from '../../../components/DetailList';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { InstanceGroupsAPI } from '../../../api';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
const Unavailable = styled.span`
color: var(--pf-global--danger-color--200);
@@ -38,7 +39,10 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
);
const { error, dismissError } = useDismissableError(deleteError);
-
+ const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
+ instanceGroup,
+ i18n
+ );
const verifyInstanceGroup = item => {
if (item.is_isolated) {
return (
@@ -142,6 +146,10 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
modalTitle={i18n._(t`Delete instance group`)}
onConfirm={deleteInstanceGroup}
isDisabled={isLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This instance group 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/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx
index 4cdc2dc8b7..4abc825f4c 100644
--- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.jsx
@@ -17,7 +17,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
import AlertModal from '../../../components/AlertModal';
import DatalistToolbar from '../../../components/DataListToolbar';
import AddDropDownButton from '../../../components/AddDropDownButton';
-
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import InstanceGroupListItem from './InstanceGroupListItem';
const QS_CONFIG = getQSConfig('instance-group', {
@@ -186,7 +186,10 @@ function InstanceGroupList({ i18n }) {
? `${match.url}/container_group/${item.id}/details`
: `${match.url}/${item.id}/details`;
};
-
+ const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
+ selected[0],
+ i18n
+ );
return (
<>
@@ -218,6 +221,11 @@ function InstanceGroupList({ i18n }) {
itemsToDelete={modifiedSelected}
pluralizedItemName={i18n._(t`Instance Groups`)}
errorMessage={errorMessageDelete}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ '{numItemsToDelete, plural, one {This instance group is currently being by other resources. Are you sure you want to delete it?} other {Deleting these instance groups 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/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx
index 74f397dc1c..b4c702bef2 100644
--- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx
+++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupList.test.jsx
@@ -6,10 +6,18 @@ import {
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { InstanceGroupsAPI } from '../../../api';
+import {
+ InstanceGroupsAPI,
+ OrganizationsAPI,
+ InventoriesAPI,
+ UnifiedJobTemplatesAPI,
+} from '../../../api';
import InstanceGroupList from './InstanceGroupList';
jest.mock('../../../api/models/InstanceGroups');
+jest.mock('../../../api/models/Organizations');
+jest.mock('../../../api/models/Inventories');
+jest.mock('../../../api/models/UnifiedJobTemplates');
const instanceGroups = {
data: {
@@ -44,6 +52,9 @@ const instanceGroups = {
};
const options = { data: { actions: { POST: true } } };
+OrganizationsAPI.read.mockResolvedValue({ data: { count: 0 } });
+InventoriesAPI.read.mockResolvedValue({ data: { count: 0 } });
+UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
describe('', () => {
let wrapper;
diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
index 32d9eda3e5..f294e5c696 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
@@ -19,6 +19,7 @@ import ChipGroup from '../../../components/ChipGroup';
import { InventoriesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { Inventory } from '../../../types';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function InventoryDetail({ inventory, i18n }) {
const history = useHistory();
@@ -54,6 +55,11 @@ function InventoryDetail({ inventory, i18n }) {
user_capabilities: userCapabilities,
} = inventory.summary_fields;
+ const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
+ inventory,
+ i18n
+ );
+
if (isLoading) {
return ;
}
@@ -126,6 +132,10 @@ function InventoryDetail({ inventory, i18n }) {
name={inventory.name}
modalTitle={i18n._(t`Delete Inventory`)}
onConfirm={deleteInventory}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This inventory 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/Inventory/InventoryDetail/InventoryDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx
index 426a359289..61fb97997a 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx
@@ -105,6 +105,18 @@ describe('', () => {
expect(dates.at(1).prop('date')).toEqual(mockInventory.modified);
});
+ test('should have proper number of delete detail requests', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(
+ wrapper.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(2);
+ });
+
test('should load instance groups', async () => {
InventoriesAPI.readInstanceGroups.mockResolvedValue({
data: {
diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx
index e152e7cde8..480e9ef7a9 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx
@@ -17,6 +17,7 @@ import { getQSConfig, parseQueryString } from '../../../util/qs';
import useWsInventories from './useWsInventories';
import AddDropDownButton from '../../../components/AddDropDownButton';
import InventoryListItem from './InventoryListItem';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
const QS_CONFIG = getQSConfig('inventory', {
page: 1,
@@ -126,6 +127,12 @@ function InventoryList({ i18n }) {
}
}
};
+
+ const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
+ selected[0],
+ i18n
+ );
+
const addInventory = i18n._(t`Add inventory`);
const addSmartInventory = i18n._(t`Add smart inventory`);
const addButton = (
@@ -216,6 +223,11 @@ function InventoryList({ i18n }) {
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
{ numItemsToDelete: selected.length }
)}
+ deleteMessage={i18n._(
+ '{numItemsToDelete, plural, one {This inventory is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these inventories could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
+ { numItemsToDelete: selected.length }
+ )}
+ deleteDetailsRequests={deleteDetailsRequests}
/>,
]}
/>
diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx
index e9b256e05e..6d4f9734a4 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx
@@ -1,11 +1,17 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { InventoriesAPI } from '../../../api';
+import {
+ InventoriesAPI,
+ JobTemplatesAPI,
+ WorkflowJobTemplatesAPI,
+} from '../../../api';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import InventoryList from './InventoryList';
-jest.mock('../../../api');
+jest.mock('../../../api/models/Inventories');
+jest.mock('../../../api/models/JobTemplates');
+jest.mock('../../../api/models/WorkflowJobTemplates');
const mockInventories = [
{
@@ -136,6 +142,8 @@ describe('', () => {
},
},
});
+ JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
debug = global.console.debug; // eslint-disable-line prefer-destructuring
global.console.debug = () => {};
});
@@ -155,6 +163,16 @@ describe('', () => {
expect(wrapper.find('InventoryListItem')).toHaveLength(3);
});
+ test('should have proper number of delete detail requests', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ expect(
+ wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(2);
+ });
+
test('should select inventory when checked', async () => {
let wrapper;
await act(async () => {
diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
index 4eda04831d..c338c6eb92 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx
@@ -22,6 +22,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
import Popover from '../../../components/Popover';
import useRequest from '../../../util/useRequest';
import { InventorySourcesAPI } from '../../../api';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function InventorySourceDetail({ inventorySource, i18n }) {
const {
@@ -96,6 +97,12 @@ function InventorySourceDetail({ inventorySource, i18n }) {
}
};
+ const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
+ inventorySource.inventory,
+ i18n,
+ inventorySource
+ );
+
const VERBOSITY = {
0: i18n._(t`0 (Warning)`),
1: i18n._(t`1 (Info)`),
@@ -281,6 +288,10 @@ function InventorySourceDetail({ inventorySource, i18n }) {
name={name}
modalTitle={i18n._(t`Delete inventory source`)}
onConfirm={handleDelete}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This inventory source is currently being used by other resources that rely on it. Are you sure you want to delete it?`
+ )}
>
{i18n._(t`Delete`)}
diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx
index 9a33c1ab50..edb05771de 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx
@@ -7,9 +7,20 @@ import {
} from '../../../../testUtils/enzymeHelpers';
import InventorySourceDetail from './InventorySourceDetail';
import mockInvSource from '../shared/data.inventory_source.json';
-import { InventorySourcesAPI } from '../../../api';
+import {
+ InventorySourcesAPI,
+ InventoriesAPI,
+ WorkflowJobTemplateNodesAPI,
+} from '../../../api';
jest.mock('../../../api/models/InventorySources');
+jest.mock('../../../api/models/Inventories');
+jest.mock('../../../api/models/WorkflowJobTemplateNodes');
+
+InventoriesAPI.updateSources.mockResolvedValue({
+ data: [{ inventory_source: 1 }],
+});
+WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
InventorySourcesAPI.readOptions.mockResolvedValue({
data: {
actions: {
@@ -101,6 +112,17 @@ describe('InventorySourceDetail', () => {
expect(wrapper.find('InventorySourceSyncButton')).toHaveLength(1);
});
+ test('should have proper number of delete detail requests', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(
+ wrapper.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(3);
+ });
+
test('should hide expected action buttons for users without permissions', async () => {
const userCapabilities = {
edit: false,
diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
index 4013d9a53b..9a19304d59 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
@@ -20,6 +20,7 @@ import AlertModal from '../../../components/AlertModal/AlertModal';
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
import InventorySourceListItem from './InventorySourceListItem';
import useWsInventorySources from './useWsInventorySources';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
const QS_CONFIG = getQSConfig('inventory', {
not__source: '',
@@ -142,6 +143,12 @@ function InventorySourceList({ i18n }) {
sourceChoicesOptions &&
Object.prototype.hasOwnProperty.call(sourceChoicesOptions, 'POST');
const listUrl = `/inventories/${inventoryType}/${id}/sources/`;
+
+ const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
+ id,
+ i18n,
+ selected[0]
+ );
return (
<>
,
...(canSyncSources
? [
diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx
index 5f4115b1fd..1dbda4ee83 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx
@@ -2,7 +2,11 @@ import React from 'react';
import { Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
-import { InventoriesAPI, InventorySourcesAPI } from '../../../api';
+import {
+ InventoriesAPI,
+ InventorySourcesAPI,
+ WorkflowJobTemplateNodesAPI,
+} from '../../../api';
import {
mountWithContexts,
waitForElement,
@@ -13,6 +17,7 @@ import InventorySourceList from './InventorySourceList';
jest.mock('../../../api/models/InventorySources');
jest.mock('../../../api/models/Inventories');
jest.mock('../../../api/models/InventoryUpdates');
+jest.mock('../../../api/models/WorkflowJobTemplateNodes');
const sources = {
data: {
@@ -61,6 +66,12 @@ describe('', () => {
debug = global.console.debug; // eslint-disable-line prefer-destructuring
global.console.debug = () => {};
InventoriesAPI.readSources.mockResolvedValue(sources);
+ InventoriesAPI.updateSources.mockResolvedValue({
+ data: [{ inventory_source: 1 }],
+ });
+ InventorySourcesAPI.readGroups.mockResolvedValue({ data: { count: 0 } });
+ InventorySourcesAPI.readHosts.mockResolvedValue({ data: { count: 0 } });
+ WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
InventorySourcesAPI.readOptions.mockResolvedValue({
data: {
actions: {
@@ -119,6 +130,12 @@ describe('', () => {
expect(InventorySourcesAPI.readOptions).toHaveBeenCalled();
});
+ test('should have proper number of delete detail requests', async () => {
+ expect(
+ wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(3);
+ });
+
test('source data should render properly', async () => {
await waitForElement(wrapper, 'InventorySourceList', el => el.length > 0);
expect(
diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
index bee0feb773..807e24a640 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
@@ -116,7 +116,7 @@ describe('', () => {
wrapper.update();
const modal = wrapper.find('Modal');
expect(modal.length).toBe(1);
- modal.find('button[aria-label="Delete"]').simulate('click');
+ modal.find('button[aria-label="Confirm Delete"]').simulate('click');
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
});
@@ -138,7 +138,7 @@ describe('', () => {
const modal = wrapper.find('Modal');
expect(modal.length).toBe(1);
await act(async () => {
- modal.find('button[aria-label="Delete"]').simulate('click');
+ modal.find('button[aria-label="Confirm Delete"]').simulate('click');
});
wrapper.update();
diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
index ef2fa35190..59efdfe323 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
@@ -188,9 +188,19 @@ describe('', () => {
wrapper = mountWithContexts();
});
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
- await act(async () => {
- wrapper.find('DeleteButton').invoke('onConfirm')();
- });
+ await act(async () =>
+ wrapper.find('button[aria-label="Delete"]').simulate('click')
+ );
+ await waitForElement(
+ wrapper,
+ 'Modal',
+ el => el.props().isOpen === true && el.props().title === 'Delete Job'
+ );
+ await act(async () =>
+ wrapper
+ .find('Modal button[aria-label="Confirm Delete"]')
+ .simulate('click')
+ );
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
});
diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
index db282620a5..9369eaebfb 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx
@@ -20,6 +20,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import { useConfig } from '../../../contexts/Config';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function OrganizationDetail({ i18n, organization }) {
const {
@@ -71,6 +72,11 @@ function OrganizationDetail({ i18n, organization }) {
const { error, dismissError } = useDismissableError(deleteError);
+ const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
+ organization,
+ i18n
+ );
+
if (hasContentLoading) {
return ;
}
@@ -157,6 +163,10 @@ function OrganizationDetail({ i18n, organization }) {
modalTitle={i18n._(t`Delete Organization`)}
onConfirm={deleteOrganization}
isDisabled={isLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ 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 a33ddc1c61..d91b38c85d 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { OrganizationsAPI } from '../../../api';
+import { OrganizationsAPI, CredentialsAPI } from '../../../api';
import {
mountWithContexts,
waitForElement,
@@ -44,6 +44,8 @@ describe('', () => {
};
beforeEach(() => {
+ CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
+
OrganizationsAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
});
@@ -64,6 +66,20 @@ describe('', () => {
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
+ test('should have proper number of delete detail requests', async () => {
+ let component;
+ await act(async () => {
+ component = mountWithContexts(
+
+ );
+ });
+ await waitForElement(component, 'ContentLoading', el => el.length === 0);
+
+ expect(
+ component.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(7);
+ });
+
test('should render the expected instance group', async () => {
let component;
await act(async () => {
diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
index 03b062a83d..d5dce4dcac 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx
@@ -19,6 +19,7 @@ import PaginatedTable, {
} from '../../../components/PaginatedTable';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import OrganizationListItem from './OrganizationListItem';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
const QS_CONFIG = getQSConfig('organization', {
page: 1,
@@ -116,6 +117,10 @@ function OrganizationsList({ i18n }) {
setSelected(selected.concat(row));
}
};
+ const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
+ selected[0],
+ i18n
+ );
return (
<>
@@ -173,6 +178,11 @@ function OrganizationsList({ i18n }) {
onDelete={handleOrgDelete}
itemsToDelete={selected}
pluralizedItemName={i18n._(t`Organizations`)}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ '{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 34e9b93e33..02c6c65b0e 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.test.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { OrganizationsAPI } from '../../../api';
+import { OrganizationsAPI, CredentialsAPI } from '../../../api';
import {
mountWithContexts,
waitForElement,
@@ -70,6 +70,7 @@ const mockOrganizations = {
describe('', () => {
let wrapper;
beforeEach(() => {
+ CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
OrganizationsAPI.read.mockResolvedValue(mockOrganizations);
OrganizationsAPI.readOptions.mockResolvedValue({
data: {
@@ -90,6 +91,20 @@ describe('', () => {
});
});
+ test('should have proper number of delete detail requests', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ await waitForElement(
+ wrapper,
+ 'OrganizationsList',
+ el => el.find('ContentLoading').length === 0
+ );
+ expect(
+ wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(7);
+ });
+
test('Items are rendered after loading', async () => {
await act(async () => {
wrapper = mountWithContexts();
diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
index d00ab5da57..f8bcd351b7 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx
@@ -20,6 +20,7 @@ import CredentialChip from '../../../components/CredentialChip';
import { ProjectsAPI } from '../../../api';
import { toTitleCase } from '../../../util/strings';
import useRequest, { useDismissableError } from '../../../util/useRequest';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import ProjectSyncButton from '../shared/ProjectSyncButton';
function ProjectDetail({ project, i18n }) {
@@ -52,7 +53,10 @@ function ProjectDetail({ project, i18n }) {
);
const { error, dismissError } = useDismissableError(deleteError);
-
+ const deleteDetailsRequests = relatedResourceDeleteRequests.project(
+ project,
+ i18n
+ );
let optionsList = '';
if (
scm_clean ||
@@ -171,6 +175,10 @@ function ProjectDetail({ project, i18n }) {
modalTitle={i18n._(t`Delete Project`)}
onConfirm={deleteProject}
isDisabled={isLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This project 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/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
index 36caa48a12..6f65e6b7f7 100644
--- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx
@@ -5,7 +5,12 @@ import {
mountWithContexts,
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
-import { ProjectsAPI } from '../../../api';
+import {
+ ProjectsAPI,
+ JobTemplatesAPI,
+ WorkflowJobTemplatesAPI,
+ InventorySourcesAPI,
+} from '../../../api';
import ProjectDetail from './ProjectDetail';
jest.mock('../../../api');
@@ -147,6 +152,27 @@ describe('', () => {
expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
});
+ test('should have proper number of delete detail requests', () => {
+ JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ const mockOptions = {
+ scm_type: '',
+ scm_clean: false,
+ scm_delete_on_update: false,
+ scm_update_on_launch: false,
+ allow_override: false,
+ created: '',
+ modified: '',
+ };
+ const wrapper = mountWithContexts(
+
+ );
+ expect(
+ wrapper.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(3);
+ });
+
test('should render with missing summary fields', async () => {
const wrapper = mountWithContexts(
diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx
index acb473a34d..205628ca27 100644
--- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx
@@ -18,6 +18,7 @@ import PaginatedTable, {
HeaderCell,
} from '../../../components/PaginatedTable';
import useWsProjects from './useWsProjects';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import ProjectListItem from './ProjectListItem';
@@ -116,6 +117,11 @@ function ProjectList({ i18n }) {
}
};
+ const deleteDetailsRequests = relatedResourceDeleteRequests.project(
+ selected[0],
+ i18n
+ );
+
return (
@@ -194,6 +200,11 @@ function ProjectList({ i18n }) {
onDelete={handleProjectDelete}
itemsToDelete={selected}
pluralizedItemName={i18n._(t`Projects`)}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ '{numItemsToDelete, plural, one {This project is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these projects 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/Project/ProjectList/ProjectList.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx
index 5a6945d892..6b6f913e79 100644
--- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx
@@ -1,7 +1,15 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { ProjectsAPI } from '../../../api';
-import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import {
+ ProjectsAPI,
+ JobTemplatesAPI,
+ WorkflowJobTemplatesAPI,
+ InventorySourcesAPI,
+} from '../../../api';
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../../testUtils/enzymeHelpers';
import ProjectList from './ProjectList';
jest.mock('../../../api');
@@ -83,6 +91,9 @@ const mockProjects = [
describe('', () => {
beforeEach(() => {
+ JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
ProjectsAPI.read.mockResolvedValue({
data: {
count: mockProjects.length,
@@ -138,6 +149,17 @@ describe('', () => {
).toEqual(true);
});
+ test('should have proper number of delete detail requests', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(
+ wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(3);
+ });
+
test('should select all', async () => {
let wrapper;
await act(async () => {
@@ -177,10 +199,11 @@ describe('', () => {
.at(2)
.invoke('onSelect')();
});
- wrapper.update();
- expect(wrapper.find('ToolbarDeleteButton button').prop('disabled')).toEqual(
- true
+ waitForElement(
+ wrapper,
+ 'ToolbarDeleteButton button',
+ el => el.prop('disabled') === true
);
});
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index abd0078571..27276ec543 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -31,6 +31,7 @@ import { VariablesDetail } from '../../../components/CodeEditor';
import { JobTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function JobTemplateDetail({ i18n, template }) {
const {
@@ -96,6 +97,10 @@ function JobTemplateDetail({ i18n, template }) {
const { error, dismissError } = useDismissableError(deleteError);
+ const deleteDetailsRequests = relatedResourceDeleteRequests.template(
+ template,
+ i18n
+ );
const canLaunch =
summary_fields.user_capabilities && summary_fields.user_capabilities.start;
const verbosityOptions = [
@@ -401,6 +406,10 @@ function JobTemplateDetail({ i18n, template }) {
modalTitle={i18n._(t`Delete Job Template`)}
onConfirm={deleteJobTemplate}
isDisabled={isDeleteLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This job template 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/Template/JobTemplateDetail/JobTemplateDetail.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
index 775dc5f50a..ccd698da10 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx
@@ -5,7 +5,7 @@ import {
waitForElement,
} from '../../../../testUtils/enzymeHelpers';
import JobTemplateDetail from './JobTemplateDetail';
-import { JobTemplatesAPI } from '../../../api';
+import { JobTemplatesAPI, WorkflowJobTemplateNodesAPI } from '../../../api';
import mockTemplate from '../shared/data.job_template.json';
jest.mock('../../../api');
@@ -25,6 +25,7 @@ describe('', () => {
beforeEach(async () => {
JobTemplatesAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
+ WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
await act(async () => {
wrapper = mountWithContexts(
@@ -56,6 +57,23 @@ describe('', () => {
);
});
+ test('should have proper number of delete detail requests', async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ expect(
+ wrapper.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(1);
+ });
+
test('should request instance groups from api', async () => {
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
index 7d326d649d..91e5016e89 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
@@ -27,6 +27,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
import { LaunchButton } from '../../../components/LaunchButton';
import Sparkline from '../../../components/Sparkline';
import { toTitleCase } from '../../../util/strings';
+import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import useRequest, { useDismissableError } from '../../../util/useRequest';
function WorkflowJobTemplateDetail({ template, i18n }) {
@@ -102,6 +103,11 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
type: 'workflow_job',
}));
+ const deleteDetailsRequests = relatedResourceDeleteRequests.template(
+ template,
+ i18n
+ );
+
return (
@@ -241,6 +247,10 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
modalTitle={i18n._(t`Delete Workflow Job Template`)}
onConfirm={deleteWorkflowJobTemplate}
isDisabled={isLoading}
+ deleteDetailsRequests={deleteDetailsRequests}
+ deleteMessage={i18n._(
+ t`This workflow job template 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/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx
index 0c1be1e8e9..71b2eb1158 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.test.jsx
@@ -5,6 +5,9 @@ import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
+import { WorkflowJobTemplateNodesAPI } from '../../../api';
+
+jest.mock('../../../api');
describe('', () => {
let wrapper;
@@ -50,6 +53,7 @@ describe('', () => {
};
beforeEach(async () => {
+ WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
history = createMemoryHistory({
initialEntries: ['/templates/workflow_job_template/1/details'],
});
@@ -86,6 +90,7 @@ describe('', () => {
afterEach(() => {
wrapper.unmount();
+ jest.clearAllMocks();
});
test('renders successfully', () => {
@@ -163,6 +168,12 @@ describe('', () => {
).toBe('Demo EE');
});
+ test('should have proper number of delete detail requests', async () => {
+ expect(
+ wrapper.find('DeleteButton').prop('deleteDetailsRequests')
+ ).toHaveLength(1);
+ });
+
test('link out resource have the correct url', () => {
const inventory = wrapper.find('Detail[label="Inventory"]').find('Link');
const organization = wrapper
diff --git a/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js b/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js
new file mode 100644
index 0000000000..32f795704a
--- /dev/null
+++ b/awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js
@@ -0,0 +1,144 @@
+import {
+ getRelatedResourceDeleteCounts,
+ relatedResourceDeleteRequests,
+} from './getRelatedResourceDeleteDetails';
+import {
+ InventoriesAPI,
+ InventorySourcesAPI,
+ JobTemplatesAPI,
+ ProjectsAPI,
+ WorkflowJobTemplatesAPI,
+ WorkflowJobTemplateNodesAPI,
+ CredentialsAPI,
+} from '../api';
+
+jest.mock('../api/models/Credentials');
+jest.mock('../api/models/Inventories');
+jest.mock('../api/models/InventorySources');
+jest.mock('../api/models/JobTemplates');
+jest.mock('../api/models/Projects');
+jest.mock('../api/models/WorkflowJobTemplates');
+jest.mock('../api/models/WorkflowJobTemplateNodes');
+
+const i18n = {
+ _: key => {
+ if (key.values) {
+ Object.entries(key.values).forEach(([k, v]) => {
+ key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
+ });
+ }
+ return key.id;
+ },
+};
+
+describe('delete details', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should call api for credentials list', () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
+ );
+ expect(InventoriesAPI.read).toBeCalledWith({
+ insights_credential: 1,
+ });
+ expect(InventorySourcesAPI.read).toBeCalledWith({
+ credentials__id: 1,
+ });
+ expect(JobTemplatesAPI.read).toBeCalledWith({ credentials: 1 });
+ expect(ProjectsAPI.read).toBeCalledWith({ credentials: 1 });
+ });
+
+ test('should call api for projects list', () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.project({ id: 1 }, i18n)
+ );
+ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
+ unified_job_template: 1,
+ });
+ expect(InventorySourcesAPI.read).toBeCalledWith({
+ source_project: 1,
+ });
+ expect(JobTemplatesAPI.read).toBeCalledWith({ project: 1 });
+ });
+
+ test('should call api for templates list', () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.template({ id: 1 }, i18n)
+ );
+ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
+ unified_job_template: 1,
+ });
+ });
+
+ test('should call api for credential type list', () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.credentialType({ id: 1 }, i18n)
+ );
+ expect(CredentialsAPI.read).toBeCalledWith({
+ credential_type__id: 1,
+ });
+ });
+
+ test('should call api for inventory list', () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.inventory({ id: 1 }, i18n)
+ );
+ expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 });
+ expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({
+ inventory: 1,
+ });
+ });
+
+ test('should call api for inventory source list', async () => {
+ InventoriesAPI.updateSources.mockResolvedValue({
+ data: [{ inventory_source: 2 }],
+ });
+ await getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.inventorySource(1, i18n)
+ );
+ expect(InventoriesAPI.updateSources).toBeCalledWith(1);
+ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
+ unified_job_template: 2,
+ });
+ });
+
+ test('should call api for organization list', async () => {
+ getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.organization({ id: 1 }, i18n)
+ );
+ expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 });
+ });
+
+ test('should call return error for inventory source list', async () => {
+ InventoriesAPI.updateSources.mockRejectedValue({
+ response: {
+ config: {
+ method: 'post',
+ url: '/api/v2/inventories/1/ad_hoc_commands',
+ },
+ data: 'An error occurred',
+ status: 403,
+ },
+ });
+ const { error } = await getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.inventorySource(1, i18n)
+ );
+
+ expect(InventoriesAPI.updateSources).toBeCalledWith(1);
+ expect(error).toBeDefined();
+ });
+
+ test('should return proper results', async () => {
+ JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
+ ProjectsAPI.read.mockResolvedValue({ data: { count: 2 } });
+ InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } });
+ InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
+
+ const { results } = await getRelatedResourceDeleteCounts(
+ relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
+ );
+ expect(results).toEqual({ Projects: 2, Inventories: 3 });
+ });
+});
diff --git a/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js b/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js
new file mode 100644
index 0000000000..c4d9c12ad1
--- /dev/null
+++ b/awx/ui_next/src/util/getRelatedResourceDeleteDetails.js
@@ -0,0 +1,301 @@
+import { t } from '@lingui/macro';
+
+import {
+ UnifiedJobTemplatesAPI,
+ CredentialsAPI,
+ InventoriesAPI,
+ InventorySourcesAPI,
+ JobTemplatesAPI,
+ ProjectsAPI,
+ WorkflowJobTemplateNodesAPI,
+ WorkflowJobTemplatesAPI,
+ CredentialInputSourcesAPI,
+ TeamsAPI,
+ NotificationTemplatesAPI,
+ ExecutionEnvironmentsAPI,
+ ApplicationsAPI,
+ OrganizationsAPI,
+} from '../api';
+
+export async function getRelatedResourceDeleteCounts(requests) {
+ const results = {};
+ let error = null;
+ let hasCount = false;
+
+ try {
+ await Promise.all(
+ requests.map(async ({ request, label }) => {
+ const {
+ data: { count },
+ } = await request();
+
+ if (count > 0) {
+ results[label] = count;
+ hasCount = true;
+ }
+ })
+ );
+ } catch (err) {
+ error = err;
+ }
+
+ return {
+ results: hasCount && results,
+ error,
+ };
+}
+
+export const relatedResourceDeleteRequests = {
+ credential: (selected, i18n) => [
+ {
+ request: () =>
+ JobTemplatesAPI.read({
+ credentials: selected.id,
+ }),
+ label: i18n._(t`Job Templates`),
+ },
+ {
+ request: () => ProjectsAPI.read({ credentials: selected.id }),
+ label: i18n._(t`Projects`),
+ },
+ {
+ request: () =>
+ InventoriesAPI.read({
+ insights_credential: selected.id,
+ }),
+ label: i18n._(t`Inventories`),
+ },
+ {
+ request: () =>
+ InventorySourcesAPI.read({
+ credentials__id: selected.id,
+ }),
+ 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) => [
+ {
+ request: async () =>
+ CredentialsAPI.read({
+ credential_type__id: selected.id,
+ }),
+ label: i18n._(t`Credentials`),
+ },
+ ],
+
+ inventory: (selected, i18n) => [
+ {
+ request: async () =>
+ JobTemplatesAPI.read({
+ inventory: selected.id,
+ }),
+ label: i18n._(t`Job Templates`),
+ },
+ {
+ request: () => WorkflowJobTemplatesAPI.read({ inventory: selected.id }),
+ label: i18n._(t`Workflow Job Template`),
+ },
+ ],
+
+ inventorySource: (inventoryId, i18n, inventorySource) => [
+ {
+ request: async () => {
+ try {
+ const { data } = await InventoriesAPI.updateSources(inventoryId);
+
+ const results = await Promise.all(
+ data.map(async datum =>
+ WorkflowJobTemplateNodesAPI.read({
+ unified_job_template: datum.inventory_source,
+ })
+ )
+ );
+ const total = results.reduce(
+ ({ data: { count: acc } }, { data: { count: cur } }) => acc + cur,
+ { data: { count: 0 } }
+ );
+
+ return { data: { count: total } };
+ } catch (err) {
+ throw new Error(err);
+ }
+ },
+ label: i18n._(t`Workflow Job Template Nodes`),
+ },
+ {
+ request: async () => InventorySourcesAPI.readGroups(inventorySource.id),
+ label: i18n._(t`Groups`),
+ },
+ {
+ request: async () => InventorySourcesAPI.readHosts(inventorySource.id),
+ label: i18n._(t`Hosts`),
+ },
+ ],
+
+ project: (selected, i18n) => [
+ {
+ request: () =>
+ JobTemplatesAPI.read({
+ project: selected.id,
+ }),
+ label: i18n._(t`Job Templates`),
+ },
+ {
+ request: () =>
+ WorkflowJobTemplateNodesAPI.read({
+ unified_job_template: selected.id,
+ }),
+ label: i18n._(t`Workflow Job Templates`),
+ },
+ {
+ request: () =>
+ InventorySourcesAPI.read({
+ source_project: selected.id,
+ }),
+ label: i18n._(t`Inventory Sources`),
+ },
+ ],
+
+ template: (selected, i18n) => [
+ {
+ request: async () =>
+ WorkflowJobTemplateNodesAPI.read({
+ unified_job_template: selected.id,
+ }),
+ label: [i18n._(t`Workflow Job Template Nodes`)],
+ },
+ ],
+
+ organization: (selected, i18n) => [
+ {
+ request: async () =>
+ CredentialsAPI.read({
+ organization: selected.id,
+ }),
+ 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({
+ default_environment: selected.id,
+ }),
+ label: [i18n._(t`Organizations`)],
+ },
+ {
+ request: async () => {
+ try {
+ const {
+ data: { results },
+ } = await InventorySourcesAPI.read({
+ execution_environment: selected.id,
+ });
+
+ const responses = await Promise.all(
+ results.map(result =>
+ WorkflowJobTemplateNodesAPI.read({
+ unified_job_template: result.id,
+ })
+ )
+ );
+
+ const total = responses.reduce(
+ ({ data: { count: acc } }, { data: { count: cur } }) => acc + cur,
+ { data: { count: 0 } }
+ );
+ return { data: { count: total } };
+ } catch (err) {
+ throw new Error(err);
+ }
+ },
+ label: [i18n._(t`Workflow Job Template Nodes`)],
+ },
+ ],
+ instanceGroup: (selected, i18n) => [
+ {
+ request: () => OrganizationsAPI.read({ instance_groups: selected.id }),
+ label: i18n._(t`Organizations`),
+ },
+ {
+ request: () => InventoriesAPI.read({ instance_groups: selected.id }),
+ label: i18n._(t`Inventories`),
+ },
+ {
+ request: () =>
+ UnifiedJobTemplatesAPI.read({ instance_groups: selected.id }),
+ label: i18n._(t`Templates`),
+ },
+ ],
+};