fixes credential type delete warnings

This commit is contained in:
Alex Corey 2021-03-04 17:54:24 -05:00
parent c2e224bb86
commit 6e401fa02f
21 changed files with 439 additions and 114 deletions

View File

@ -0,0 +1,9 @@
import Base from '../Base';
class Metrics extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/inventories/';
}
}
export default Metrics;

View File

@ -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}
>

View File

@ -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 (
<>
<Button
variant={variant || 'secondary'}
aria-label={i18n._(t`Delete`)}
isDisabled={isDisabled}
onClick={() => toggleModal(true)}
>
{children || i18n._(t`Delete`)}
</Button>
{disabledTooltip ? (
<Tooltip content={disabledTooltip} position="top">
<div>
<Button
variant={variant || 'secondary'}
aria-label={i18n._(t`Delete`)}
isDisabled={isDisabled}
onClick={() => toggleModal(true)}
>
{children || i18n._(t`Delete`)}
</Button>
</div>
</Tooltip>
) : (
<Button
variant={variant || 'secondary'}
aria-label={i18n._(t`Delete`)}
isDisabled={isDisabled}
onClick={() => toggleModal(true)}
>
{children || i18n._(t`Delete`)}
</Button>
)}
<AlertModal
isOpen={isOpen}
title={modalTitle}
@ -83,7 +97,10 @@ function DeleteButton({
variant="danger"
aria-label={i18n._(t`Confirm Delete`)}
isDisabled={isDisabled}
onClick={onConfirm}
onClick={() => {
onConfirm();
toggleModal(false);
}}
>
{i18n._(t`Delete`)}
</Button>,
@ -110,9 +127,9 @@ function DeleteButton({
<div aria-label={deleteMessage}>{deleteMessage}</div>
<br />
{Object.entries(deleteDetails).map(([key, value]) => (
<DetailsWrapper aria-label={`${key}: ${value}`} key={key}>
<span>{key}</span> <Badge>{value}</Badge>
</DetailsWrapper>
<div aria-label={`${key}: ${value}`} key={key}>
<Label>{key}</Label> <Badge>{value}</Badge>
</div>
))}
</div>
}

View File

@ -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 (
<div>
{deleteMessages.map(message => (
<div aria-label={message} key={message}>
{message}
</div>
))}
{deleteDetails &&
Object.entries(deleteDetails).map(([key, value]) => (
<div key={key} aria-label={`${key}: ${value}`}>
<Label>{key}</Label>
<Badge>{value}</Badge>
</div>
))}
</div>
);
};
if (deleteMessageError) {
return (
<AlertModal
@ -218,6 +246,9 @@ function ToolbarDeleteButton({
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
isDisabled={Boolean(
deleteDetails && itemsToDelete[0]?.type === 'credential_type'
)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
@ -239,42 +270,11 @@ function ToolbarDeleteButton({
<br />
</span>
))}
{itemsToDelete.length === 1 &&
Object.values(deleteDetails).length > 0 && (
<WarningMessage
variant="warning"
isInline
title={
<div>
{deleteMessages.map(message => (
<div aria-label={message} key={message}>
{message}
</div>
))}
{itemsToDelete.length === 1 && (
<>
<br />
{Object.entries(deleteDetails).map(([key, value]) => (
<DetailsWrapper
key={key}
aria-label={`${key}: ${value}`}
>
<span>{key}</span> <Badge>{value}</Badge>
</DetailsWrapper>
))}
</>
)}
</div>
}
/>
)}
{itemsToDelete.length > 1 && (
{(deleteDetails || deleteMessage || warningMessage) && (
<WarningMessage
variant="warning"
isInline
title={deleteMessages.map(message => (
<div>{message}</div>
))}
title={buildDeleteWarning()}
/>
)}
</AlertModal>

View File

@ -27,6 +27,7 @@ const itemC = {
describe('<ToolbarDeleteButton />', () => {
let deleteDetailsRequests;
let wrapper;
beforeEach(() => {
deleteDetailsRequests = [
{
@ -35,8 +36,13 @@ describe('<ToolbarDeleteButton />', () => {
},
];
});
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
test('should render button', () => {
const wrapper = mountWithContexts(
wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[]} />
);
expect(wrapper.find('button')).toHaveLength(1);
@ -44,7 +50,6 @@ describe('<ToolbarDeleteButton />', () => {
});
test('should open confirmation modal', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<ToolbarDeleteButton
@ -65,8 +70,89 @@ describe('<ToolbarDeleteButton />', () => {
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(
<ToolbarDeleteButton
onDelete={() => {}}
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(
<ToolbarDeleteButton
onDelete={() => {}}
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('<ToolbarDeleteButton />', () => {
),
},
];
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<ToolbarDeleteButton
@ -113,7 +199,7 @@ describe('<ToolbarDeleteButton />', () => {
test('should invoke onDelete prop', () => {
const onDelete = jest.fn();
const wrapper = mountWithContexts(
wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={onDelete} itemsToDelete={[itemA]} />
);
wrapper.find('button').simulate('click');
@ -127,14 +213,14 @@ describe('<ToolbarDeleteButton />', () => {
});
test('should disable button when no delete permissions', () => {
const wrapper = mountWithContexts(
wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemB]} />
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
test('should render tooltip', () => {
const wrapper = mountWithContexts(
wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
);
expect(wrapper.find('Tooltip')).toHaveLength(1);
@ -142,7 +228,7 @@ describe('<ToolbarDeleteButton />', () => {
});
test('should render tooltip for username', () => {
const wrapper = mountWithContexts(
wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemC]} />
);
expect(wrapper.find('Tooltip')).toHaveLength(1);

View File

@ -119,7 +119,7 @@ exports[`<DeleteRoleConfirmationModal /> should render initially 1`] = `
title="Remove Team Access"
titleIconVariant={null}
titleLabel=""
variant="medium"
variant="small"
>
<Portal
containerInfo={
@ -135,8 +135,8 @@ exports[`<DeleteRoleConfirmationModal /> 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[`<DeleteRoleConfirmationModal /> 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"
>
<Backdrop>
<div
@ -308,20 +308,20 @@ exports[`<DeleteRoleConfirmationModal /> 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"
>
<div
aria-describedby="pf-modal-part-2"
aria-label="Alert modal"
aria-labelledby="pf-modal-part-0 alert-modal-header-label pf-modal-part-1"
aria-modal="true"
className="pf-c-modal-box pf-m-md"
data-ouia-component-id="OUIA-Generated-Modal-medium-1"
className="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"

View File

@ -68,7 +68,7 @@ describe('<CredentialDetail />', () => {
test('should have proper number of delete detail requests', () => {
expect(
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
).toHaveLength(4);
).toHaveLength(6);
});
test('should render details', () => {

View File

@ -43,7 +43,7 @@ describe('<CredentialList />', () => {
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', () => {

View File

@ -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 (
<CardBody>
@ -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`)}
</DeleteButton>
@ -105,7 +134,9 @@ function CredentialTypeDetails({ credentialType, i18n }) {
onClose={dismissError}
title={i18n._(t`Error`)}
variant="error"
/>
>
<ErrorDetail error={error} />
</AlertModal>
)}
</CardBody>
);

View File

@ -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('<CredentialTypeDetails/>', () => {
let wrapper;
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('should render details properly', async () => {
await act(async () => {
wrapper = mountWithContexts(
@ -92,10 +96,36 @@ describe('<CredentialTypeDetails/>', () => {
);
});
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(
<CredentialTypeDetails credentialType={credentialTypeData} />
);
});
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(
<CredentialTypeDetails credentialType={credentialTypeData} />
);
});
wrapper.update();
expect(wrapper.find('ErrorDetail').length).toBe(1);
});
test('expected api call is made for delete', async () => {

View File

@ -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 }
)}
/>,

View File

@ -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 (
<CardBody>
<DetailList>
@ -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`)}
</DeleteButton>

View File

@ -175,4 +175,22 @@ describe('<ExecutionEnvironmentDetails/>', () => {
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(
<ExecutionEnvironmentDetails
executionEnvironment={executionEnvironment}
/>,
{
context: { router: { history } },
}
);
});
expect(
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
).toHaveLength(2);
});
});

View File

@ -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('<ExecutionEnvironmentList/>', () => {
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('<ExecutionEnvironmentList/>', () => {
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(<ExecutionEnvironmentList />);
});
expect(
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
).toHaveLength(2);
});
});

View File

@ -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 (
<>
<PageSection>
@ -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 }
)}
/>,
]}
/>

View File

@ -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`)}

View File

@ -77,7 +77,7 @@ describe('<OrganizationDetail />', () => {
expect(
component.find('DeleteButton').prop('deleteDetailsRequests')
).toHaveLength(1);
).toHaveLength(4);
});
test('should render the expected instance group', async () => {

View File

@ -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 }
)}
/>,

View File

@ -102,7 +102,7 @@ describe('<OrganizationsList />', () => {
);
expect(
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
).toHaveLength(1);
).toHaveLength(4);
});
test('Items are rendered after loading', async () => {

View File

@ -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', () => {

View File

@ -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`)],
},
],
};