mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
fixes credential type delete warnings
This commit is contained in:
parent
c2e224bb86
commit
6e401fa02f
9
awx/ui_next/src/api/models/Metrics.js
Normal file
9
awx/ui_next/src/api/models/Metrics.js
Normal 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;
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 }
|
||||
)}
|
||||
/>,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 }
|
||||
)}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -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`)}
|
||||
|
||||
@ -77,7 +77,7 @@ describe('<OrganizationDetail />', () => {
|
||||
|
||||
expect(
|
||||
component.find('DeleteButton').prop('deleteDetailsRequests')
|
||||
).toHaveLength(1);
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('should render the expected instance group', async () => {
|
||||
|
||||
@ -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 }
|
||||
)}
|
||||
/>,
|
||||
|
||||
@ -102,7 +102,7 @@ describe('<OrganizationsList />', () => {
|
||||
);
|
||||
expect(
|
||||
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||
).toHaveLength(1);
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('Items are rendered after loading', async () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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`)],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user