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-label={label || i18n._(t`Alert modal`)}
aria-labelledby="alert-modal-header-label" aria-labelledby="alert-modal-header-label"
isOpen={Boolean(isOpen)} isOpen={Boolean(isOpen)}
variant="medium" variant="small"
title={title} title={title}
{...props} {...props}
> >

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import styled from 'styled-components'; 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 AlertModal from '../AlertModal';
import { getRelatedResourceDeleteCounts } from '../../util/getRelatedResourceDeleteDetails'; import { getRelatedResourceDeleteCounts } from '../../util/getRelatedResourceDeleteDetails';
import ErrorDetail from '../ErrorDetail'; import ErrorDetail from '../ErrorDetail';
@@ -11,9 +11,9 @@ import ErrorDetail from '../ErrorDetail';
const WarningMessage = styled(Alert)` const WarningMessage = styled(Alert)`
margin-top: 10px; margin-top: 10px;
`; `;
const DetailsWrapper = styled.span` const Label = styled.span`
:not(:first-of-type) { && {
padding-left: 10px; margin-right: 10px;
} }
`; `;
function DeleteButton({ function DeleteButton({
@@ -27,11 +27,11 @@ function DeleteButton({
ouiaId, ouiaId,
deleteMessage, deleteMessage,
deleteDetailsRequests, deleteDetailsRequests,
disabledTooltip,
}) { }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [deleteMessageError, setDeleteMessageError] = useState(); const [deleteMessageError, setDeleteMessageError] = useState();
const [deleteDetails, setDeleteDetails] = useState({}); const [deleteDetails, setDeleteDetails] = useState({});
const toggleModal = async isModalOpen => { const toggleModal = async isModalOpen => {
if (deleteDetailsRequests?.length && isModalOpen) { if (deleteDetailsRequests?.length && isModalOpen) {
const { results, error } = await getRelatedResourceDeleteCounts( const { results, error } = await getRelatedResourceDeleteCounts(
@@ -62,15 +62,29 @@ function DeleteButton({
} }
return ( return (
<> <>
<Button {disabledTooltip ? (
variant={variant || 'secondary'} <Tooltip content={disabledTooltip} position="top">
aria-label={i18n._(t`Delete`)} <div>
isDisabled={isDisabled} <Button
onClick={() => toggleModal(true)} variant={variant || 'secondary'}
> aria-label={i18n._(t`Delete`)}
{children || i18n._(t`Delete`)} isDisabled={isDisabled}
</Button> 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 <AlertModal
isOpen={isOpen} isOpen={isOpen}
title={modalTitle} title={modalTitle}
@@ -83,7 +97,10 @@ function DeleteButton({
variant="danger" variant="danger"
aria-label={i18n._(t`Confirm Delete`)} aria-label={i18n._(t`Confirm Delete`)}
isDisabled={isDisabled} isDisabled={isDisabled}
onClick={onConfirm} onClick={() => {
onConfirm();
toggleModal(false);
}}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</Button>, </Button>,
@@ -110,9 +127,9 @@ function DeleteButton({
<div aria-label={deleteMessage}>{deleteMessage}</div> <div aria-label={deleteMessage}>{deleteMessage}</div>
<br /> <br />
{Object.entries(deleteDetails).map(([key, value]) => ( {Object.entries(deleteDetails).map(([key, value]) => (
<DetailsWrapper aria-label={`${key}: ${value}`} key={key}> <div aria-label={`${key}: ${value}`} key={key}>
<span>{key}</span> <Badge>{value}</Badge> <Label>{key}</Label> <Badge>{value}</Badge>
</DetailsWrapper> </div>
))} ))}
</div> </div>
} }

View File

@@ -28,9 +28,10 @@ import ErrorDetail from '../ErrorDetail';
const WarningMessage = styled(Alert)` const WarningMessage = styled(Alert)`
margin-top: 10px; margin-top: 10px;
`; `;
const DetailsWrapper = styled.span`
:not(:first-of-type) { const Label = styled.span`
padding-left: 10px; && {
margin-right: 10px;
} }
`; `;
@@ -99,11 +100,7 @@ function ToolbarDeleteButton({
}) { }) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext); const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteDetails, setDeleteDetails] = useState({}); const [deleteDetails, setDeleteDetails] = useState(null);
const deleteMessages = [warningMessage, deleteMessage].filter(
message => message
);
const [deleteMessageError, setDeleteMessageError] = useState(); const [deleteMessageError, setDeleteMessageError] = useState();
const handleDelete = () => { const handleDelete = () => {
@@ -112,6 +109,7 @@ function ToolbarDeleteButton({
}; };
const toggleModal = async isOpen => { const toggleModal = async isOpen => {
setDeleteDetails(null);
if ( if (
isOpen && isOpen &&
itemsToDelete.length === 1 && itemsToDelete.length === 1 &&
@@ -164,6 +162,36 @@ function ToolbarDeleteButton({
const isDisabled = const isDisabled =
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete); 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) { if (deleteMessageError) {
return ( return (
<AlertModal <AlertModal
@@ -218,6 +246,9 @@ function ToolbarDeleteButton({
key="delete" key="delete"
variant="danger" variant="danger"
aria-label={i18n._(t`confirm delete`)} aria-label={i18n._(t`confirm delete`)}
isDisabled={Boolean(
deleteDetails && itemsToDelete[0]?.type === 'credential_type'
)}
onClick={handleDelete} onClick={handleDelete}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
@@ -239,42 +270,11 @@ function ToolbarDeleteButton({
<br /> <br />
</span> </span>
))} ))}
{itemsToDelete.length === 1 && {(deleteDetails || deleteMessage || warningMessage) && (
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 && (
<WarningMessage <WarningMessage
variant="warning" variant="warning"
isInline isInline
title={deleteMessages.map(message => ( title={buildDeleteWarning()}
<div>{message}</div>
))}
/> />
)} )}
</AlertModal> </AlertModal>

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ describe('<CredentialList />', () => {
test('should have proper number of delete detail requests', () => { test('should have proper number of delete detail requests', () => {
expect( expect(
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests') wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
).toHaveLength(4); ).toHaveLength(6);
}); });
test('should fetch credentials from api and render the in the list', () => { 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 { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
@@ -16,7 +16,11 @@ import {
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';
import { CredentialTypesAPI } from '../../../api'; import { CredentialTypesAPI } from '../../../api';
import { jsonToYaml } from '../../../util/yaml'; 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 }) { function CredentialTypeDetails({ credentialType, i18n }) {
const { id, name, description, injectors, inputs } = credentialType; const { id, name, description, injectors, inputs } = credentialType;
@@ -33,12 +37,35 @@ function CredentialTypeDetails({ credentialType, i18n }) {
}, [id, history]) }, [id, history])
); );
const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType( const {
credentialType, result: { isDeleteDisabled },
i18n 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 ( return (
<CardBody> <CardBody>
@@ -88,11 +115,13 @@ function CredentialTypeDetails({ credentialType, i18n }) {
name={name} name={name}
modalTitle={i18n._(t`Delete credential type`)} modalTitle={i18n._(t`Delete credential type`)}
onConfirm={deleteCredentialType} onConfirm={deleteCredentialType}
isDisabled={isLoading} isDisabled={isLoading || isDeleteDisabled}
deleteDetailsRequests={deleteDetailsRequests} disabledTooltip={
deleteMessage={i18n._( isDeleteDisabled &&
t`This credential type is currently being used by some credentials. Are you sure you want to delete it?` i18n._(
)} t`This credential type is currently being used by some credentials and cannot be deleted`
)
}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</DeleteButton> </DeleteButton>
@@ -105,7 +134,9 @@ function CredentialTypeDetails({ credentialType, i18n }) {
onClose={dismissError} onClose={dismissError}
title={i18n._(t`Error`)} title={i18n._(t`Error`)}
variant="error" variant="error"
/> >
<ErrorDetail error={error} />
</AlertModal>
)} )}
</CardBody> </CardBody>
); );

View File

@@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import { CredentialTypesAPI } from '../../../api'; import { CredentialTypesAPI, CredentialsAPI } from '../../../api';
import { jsonToYaml } from '../../../util/yaml'; import { jsonToYaml } from '../../../util/yaml';
import CredentialTypeDetails from './CredentialTypeDetails'; import CredentialTypeDetails from './CredentialTypeDetails';
@@ -66,6 +66,10 @@ function expectDetailToMatch(wrapper, label, value) {
describe('<CredentialTypeDetails/>', () => { describe('<CredentialTypeDetails/>', () => {
let wrapper; let wrapper;
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
});
test('should render details properly', async () => { test('should render details properly', async () => {
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
@@ -92,10 +96,36 @@ describe('<CredentialTypeDetails/>', () => {
); );
}); });
test('should have proper number of delete detail requests', () => { test('should disabled delete and show proper tooltip requests', async () => {
expect( CredentialsAPI.read.mockResolvedValue({ data: { count: 15 } });
wrapper.find('DeleteButton').prop('deleteDetailsRequests') await act(async () => {
).toHaveLength(1); 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 () => { test('expected api call is made for delete', async () => {

View File

@@ -169,7 +169,7 @@ function CredentialTypeList({ i18n }) {
pluralizedItemName={i18n._(t`Credential Types`)} pluralizedItemName={i18n._(t`Credential Types`)}
deleteDetailsRequests={deleteDetailsRequests} deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={i18n._( 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 } { numItemsToDelete: selected.length }
)} )}
/>, />,

View File

@@ -15,6 +15,7 @@ import {
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';
import { ExecutionEnvironmentsAPI } from '../../../api'; import { ExecutionEnvironmentsAPI } from '../../../api';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
const history = useHistory(); const history = useHistory();
@@ -41,7 +42,10 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
); );
const { error, dismissError } = useDismissableError(deleteError); const { error, dismissError } = useDismissableError(deleteError);
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
executionEnvironment,
i18n
);
return ( return (
<CardBody> <CardBody>
<DetailList> <DetailList>
@@ -120,6 +124,10 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
onConfirm={deleteExecutionEnvironment} onConfirm={deleteExecutionEnvironment}
isDisabled={isLoading} isDisabled={isLoading}
ouiaId="delete-button" 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`)} {i18n._(t`Delete`)}
</DeleteButton> </DeleteButton>

View File

@@ -175,4 +175,22 @@ describe('<ExecutionEnvironmentDetails/>', () => {
expect(wrapper.find('Button[aria-label="Delete"]')).toHaveLength(0); 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 { ExecutionEnvironmentsAPI } from '../../../api';
import ExecutionEnvironmentList from './ExecutionEnvironmentList'; import ExecutionEnvironmentList from './ExecutionEnvironmentList';
jest.mock('../../../api/models/ExecutionEnvironments'); jest.mock('../../../api');
const executionEnvironments = { const executionEnvironments = {
data: { data: {
@@ -143,7 +143,6 @@ describe('<ExecutionEnvironmentList/>', () => {
wrapper.find('Button[aria-label="Delete"]').prop('onClick')() wrapper.find('Button[aria-label="Delete"]').prop('onClick')()
); );
wrapper.update(); wrapper.update();
await act(async () => await act(async () =>
wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')() wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')()
); );
@@ -185,4 +184,17 @@ describe('<ExecutionEnvironmentList/>', () => {
waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0); waitForElement(wrapper, 'ExecutionEnvironmentList', el => el.length > 0);
expect(wrapper.find('ToolbarAddButton').length).toBe(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 ErrorDetail from '../../../components/ErrorDetail';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import DatalistToolbar from '../../../components/DataListToolbar'; import DatalistToolbar from '../../../components/DataListToolbar';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem'; import ExecutionEnvironmentsListItem from './ExecutionEnvironmentListItem';
const QS_CONFIG = getQSConfig('execution_environments', { const QS_CONFIG = getQSConfig('execution_environments', {
@@ -105,7 +105,10 @@ function ExecutionEnvironmentList({ i18n }) {
}; };
const canAdd = actions && actions.POST; const canAdd = actions && actions.POST;
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
selected[0],
i18n
);
return ( return (
<> <>
<PageSection> <PageSection>
@@ -181,6 +184,11 @@ function ExecutionEnvironmentList({ i18n }) {
onDelete={handleDelete} onDelete={handleDelete}
itemsToDelete={selected} itemsToDelete={selected}
pluralizedItemName={i18n._(t`Execution Environments`)} 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} isDisabled={isLoading}
deleteDetailsRequests={deleteDetailsRequests} deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={i18n._( 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`)} {i18n._(t`Delete`)}

View File

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

View File

@@ -180,7 +180,7 @@ function OrganizationsList({ i18n }) {
pluralizedItemName={i18n._(t`Organizations`)} pluralizedItemName={i18n._(t`Organizations`)}
deleteDetailsRequests={deleteDetailsRequests} deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={i18n._( 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 } { numItemsToDelete: selected.length }
)} )}
/>, />,

View File

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

View File

@@ -54,13 +54,13 @@ describe('delete details', () => {
getRelatedResourceDeleteCounts( getRelatedResourceDeleteCounts(
relatedResourceDeleteRequests.project({ id: 1 }, i18n) relatedResourceDeleteRequests.project({ id: 1 }, i18n)
); );
expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({ expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
credentials: 1, unified_job_template: 1,
}); });
expect(InventorySourcesAPI.read).toBeCalledWith({ 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', () => { test('should call api for templates list', () => {

View File

@@ -1,6 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
UnifiedJobTemplatesAPI,
CredentialsAPI, CredentialsAPI,
InventoriesAPI, InventoriesAPI,
InventorySourcesAPI, InventorySourcesAPI,
@@ -8,6 +9,12 @@ import {
ProjectsAPI, ProjectsAPI,
WorkflowJobTemplateNodesAPI, WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
CredentialInputSourcesAPI,
TeamsAPI,
NotificationTemplatesAPI,
ExecutionEnvironmentsAPI,
ApplicationsAPI,
OrganizationsAPI,
} from '../api'; } from '../api';
export async function getRelatedResourceDeleteCounts(requests) { export async function getRelatedResourceDeleteCounts(requests) {
@@ -65,6 +72,20 @@ export const relatedResourceDeleteRequests = {
}), }),
label: i18n._(t`Inventory Sources`), 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) => [ credentialType: (selected, i18n) => [
@@ -111,18 +132,21 @@ export const relatedResourceDeleteRequests = {
{ {
request: () => request: () =>
JobTemplatesAPI.read({ JobTemplatesAPI.read({
credentials: selected.id, project: selected.id,
}), }),
label: i18n._(t`Job Templates`), 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`), label: i18n._(t`Workflow Job Templates`),
}, },
{ {
request: () => request: () =>
InventorySourcesAPI.read({ InventorySourcesAPI.read({
credentials__id: selected.id, source_project: selected.id,
}), }),
label: i18n._(t`Inventory Sources`), label: i18n._(t`Inventory Sources`),
}, },
@@ -134,7 +158,7 @@ export const relatedResourceDeleteRequests = {
WorkflowJobTemplateNodesAPI.read({ WorkflowJobTemplateNodesAPI.read({
unified_job_template: selected.id, 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`), 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`)],
},
], ],
}; };