mirror of
https://github.com/ansible/awx.git
synced 2026-05-17 06:17:36 -02:30
Merge pull request #9345 from AlexSCorey/5546-DeleteWarnings
Adds delete warnings on lists SUMMARY This addresses #5546. ISSUE TYPE Feature Pull Request COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Keith Grant <None> Reviewed-by: Alex Corey <Alex.swansboro@gmail.com> Reviewed-by: Kersom <None>
This commit is contained in:
@@ -78,6 +78,12 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSources(inventoryId) {
|
||||||
|
return this.http.get(
|
||||||
|
`${this.baseUrl}${inventoryId}/update_inventory_sources/`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async readSourceDetail(inventoryId, sourceId) {
|
async readSourceDetail(inventoryId, sourceId) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ class InventorySources extends LaunchUpdateMixin(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readGroups(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/groups/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
readHosts(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/hosts/`);
|
||||||
|
}
|
||||||
|
|
||||||
destroyGroups(id) {
|
destroyGroups(id) {
|
||||||
return this.http.delete(`${this.baseUrl}${id}/groups/`);
|
return this.http.delete(`${this.baseUrl}${id}/groups/`);
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
@@ -2,9 +2,20 @@ import React, { useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
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 { Button } from '@patternfly/react-core';
|
import styled from 'styled-components';
|
||||||
|
import { Button, Badge, Alert, Tooltip } from '@patternfly/react-core';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
|
import { getRelatedResourceDeleteCounts } from '../../util/getRelatedResourceDeleteDetails';
|
||||||
|
import ErrorDetail from '../ErrorDetail';
|
||||||
|
|
||||||
|
const WarningMessage = styled(Alert)`
|
||||||
|
margin-top: 10px;
|
||||||
|
`;
|
||||||
|
const Label = styled.span`
|
||||||
|
&& {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
function DeleteButton({
|
function DeleteButton({
|
||||||
onConfirm,
|
onConfirm,
|
||||||
modalTitle,
|
modalTitle,
|
||||||
@@ -14,33 +25,91 @@ function DeleteButton({
|
|||||||
children,
|
children,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
ouiaId,
|
ouiaId,
|
||||||
|
deleteMessage,
|
||||||
|
deleteDetailsRequests,
|
||||||
|
disabledTooltip,
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [deleteMessageError, setDeleteMessageError] = useState();
|
||||||
|
const [deleteDetails, setDeleteDetails] = useState({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const toggleModal = async isModalOpen => {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (deleteDetailsRequests?.length && isModalOpen) {
|
||||||
|
const { results, error } = await getRelatedResourceDeleteCounts(
|
||||||
|
deleteDetailsRequests
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
setDeleteMessageError(error);
|
||||||
|
} else {
|
||||||
|
setDeleteDetails(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsOpen(isModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (deleteMessageError) {
|
||||||
|
return (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deleteMessageError}
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => {
|
||||||
|
toggleModal(false);
|
||||||
|
setDeleteMessageError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorDetail error={deleteMessageError} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
{disabledTooltip ? (
|
||||||
variant={variant || 'secondary'}
|
<Tooltip content={disabledTooltip} position="top">
|
||||||
aria-label={i18n._(t`Delete`)}
|
<div>
|
||||||
isDisabled={isDisabled}
|
<Button
|
||||||
onClick={() => setIsOpen(true)}
|
isLoading={isLoading}
|
||||||
ouiaId={ouiaId}
|
spinnerAriaValueText={isLoading ? 'Loading' : undefined}
|
||||||
>
|
variant={variant || 'secondary'}
|
||||||
{children || i18n._(t`Delete`)}
|
aria-label={i18n._(t`Delete`)}
|
||||||
</Button>
|
isDisabled={isDisabled}
|
||||||
|
onClick={() => toggleModal(true)}
|
||||||
|
ouiaId={ouiaId}
|
||||||
|
>
|
||||||
|
{children || i18n._(t`Delete`)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
spinnerAriaValueText={isLoading ? 'Loading' : undefined}
|
||||||
|
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}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClose={() => setIsOpen(false)}
|
onClose={() => toggleModal(false)}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
ouiaId="delete-modal-confirm"
|
ouiaId="delete-modal-confirm"
|
||||||
key="delete"
|
key="delete"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`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>,
|
||||||
@@ -49,7 +118,7 @@ function DeleteButton({
|
|||||||
key="cancel"
|
key="cancel"
|
||||||
variant="link"
|
variant="link"
|
||||||
aria-label={i18n._(t`Cancel`)}
|
aria-label={i18n._(t`Cancel`)}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => toggleModal(false)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -58,6 +127,23 @@ function DeleteButton({
|
|||||||
{i18n._(t`Are you sure you want to delete:`)}
|
{i18n._(t`Are you sure you want to delete:`)}
|
||||||
<br />
|
<br />
|
||||||
<strong>{name}</strong>
|
<strong>{name}</strong>
|
||||||
|
{Object.values(deleteDetails).length > 0 && (
|
||||||
|
<WarningMessage
|
||||||
|
variant="warning"
|
||||||
|
isInline
|
||||||
|
title={
|
||||||
|
<div>
|
||||||
|
<div aria-label={deleteMessage}>{deleteMessage}</div>
|
||||||
|
<br />
|
||||||
|
{Object.entries(deleteDetails).map(([key, value]) => (
|
||||||
|
<div aria-label={`${key}: ${value}`} key={key}>
|
||||||
|
<Label>{key}</Label> <Badge>{value}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
112
awx/ui_next/src/components/DeleteButton/DeleteButton.test.jsx
Normal file
112
awx/ui_next/src/components/DeleteButton/DeleteButton.test.jsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { CredentialsAPI } from '../../api';
|
||||||
|
import DeleteButton from './DeleteButton';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
|
||||||
|
describe('<DeleteButton />', () => {
|
||||||
|
test('should render button', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<DeleteButton onConfirm={() => {}} name="Foo" />
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open confirmation modal', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<DeleteButton
|
||||||
|
onConfirm={() => {}}
|
||||||
|
name="Foo"
|
||||||
|
deleteDetailsRequests={[
|
||||||
|
{
|
||||||
|
label: 'job',
|
||||||
|
request: CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 1 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
deleteMessage="Delete this?"
|
||||||
|
warningMessage="Are you sure to want to delete this"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button').prop('onClick')();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForElement(wrapper, 'Modal', el => el.length > 0);
|
||||||
|
expect(wrapper.find('Modal')).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(wrapper.find('div[aria-label="Delete this?"]')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should invoke onConfirm prop', async () => {
|
||||||
|
const onConfirm = jest.fn();
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<DeleteButton
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
itemsToDelete="foo"
|
||||||
|
deleteDetailsRequests={[
|
||||||
|
{
|
||||||
|
label: 'job',
|
||||||
|
request: CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 1 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
deleteMessage="Delete this?"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await act(async () => wrapper.find('button').simulate('click'));
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () =>
|
||||||
|
wrapper
|
||||||
|
.find('ModalBoxFooter button[aria-label="Confirm Delete"]')
|
||||||
|
.simulate('click')
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(onConfirm).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show delete details error', async () => {
|
||||||
|
const onConfirm = jest.fn();
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<DeleteButton
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
itemsToDelete="foo"
|
||||||
|
deleteDetailsRequests={[
|
||||||
|
{
|
||||||
|
label: 'job',
|
||||||
|
request: CredentialsAPI.read.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/v2/credentals',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => wrapper.find('button').simulate('click'));
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('AlertModal[title="Error!"]')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,16 +10,31 @@ import {
|
|||||||
checkPropTypes,
|
checkPropTypes,
|
||||||
} from 'prop-types';
|
} from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Alert, Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
DropdownItem,
|
||||||
|
Tooltip,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||||
|
import { getRelatedResourceDeleteCounts } from '../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
|
import ErrorDetail from '../ErrorDetail';
|
||||||
|
|
||||||
const WarningMessage = styled(Alert)`
|
const WarningMessage = styled(Alert)`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Label = styled.span`
|
||||||
|
&& {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const requiredField = props => {
|
const requiredField = props => {
|
||||||
const { name, username, image } = props;
|
const { name, username, image } = props;
|
||||||
if (!name && !username && !image) {
|
if (!name && !username && !image) {
|
||||||
@@ -77,20 +92,43 @@ function ToolbarDeleteButton({
|
|||||||
pluralizedItemName,
|
pluralizedItemName,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
deleteDetailsRequests,
|
||||||
warningMessage,
|
warningMessage,
|
||||||
|
deleteMessage,
|
||||||
i18n,
|
i18n,
|
||||||
cannotDelete,
|
cannotDelete,
|
||||||
}) {
|
}) {
|
||||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [deleteDetails, setDeleteDetails] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [deleteMessageError, setDeleteMessageError] = useState();
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
toggleModal();
|
toggleModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleModal = () => {
|
const toggleModal = async isOpen => {
|
||||||
setIsModalOpen(!isModalOpen);
|
setIsLoading(true);
|
||||||
|
setDeleteDetails(null);
|
||||||
|
if (
|
||||||
|
isOpen &&
|
||||||
|
itemsToDelete.length === 1 &&
|
||||||
|
deleteDetailsRequests?.length > 0
|
||||||
|
) {
|
||||||
|
const { results, error } = await getRelatedResourceDeleteCounts(
|
||||||
|
deleteDetailsRequests
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setDeleteMessageError(error);
|
||||||
|
} else {
|
||||||
|
setDeleteDetails(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsModalOpen(isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -126,27 +164,84 @@ function ToolbarDeleteButton({
|
|||||||
const isDisabled =
|
const isDisabled =
|
||||||
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
|
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
|
||||||
|
|
||||||
// NOTE: Once PF supports tooltips on disabled elements,
|
const buildDeleteWarning = () => {
|
||||||
// we can delete the extra <div> around the <DeleteButton> below.
|
const deleteMessages = [];
|
||||||
// See: https://github.com/patternfly/patternfly-react/issues/1894
|
if (warningMessage) {
|
||||||
|
deleteMessages.push(warningMessage);
|
||||||
|
}
|
||||||
|
if (deleteMessage) {
|
||||||
|
if (
|
||||||
|
itemsToDelete[0]?.type !== 'inventory' &&
|
||||||
|
(itemsToDelete.length > 1 || deleteDetails)
|
||||||
|
) {
|
||||||
|
deleteMessages.push(deleteMessage);
|
||||||
|
} else if (deleteDetails || itemsToDelete.length > 1) {
|
||||||
|
deleteMessages.push(deleteMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<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
|
||||||
|
isOpen={deleteMessageError}
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => {
|
||||||
|
toggleModal(false);
|
||||||
|
setDeleteMessageError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorDetail error={deleteMessageError} />
|
||||||
|
</AlertModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const shouldShowDeleteWarning =
|
||||||
|
warningMessage ||
|
||||||
|
(itemsToDelete.length === 1 && deleteDetails) ||
|
||||||
|
(itemsToDelete.length > 1 && deleteMessage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isKebabified ? (
|
{isKebabified ? (
|
||||||
<DropdownItem
|
<Tooltip content={renderTooltip()} position="top">
|
||||||
key="add"
|
<DropdownItem
|
||||||
isDisabled={isDisabled}
|
key="add"
|
||||||
component="button"
|
isDisabled={isDisabled}
|
||||||
onClick={toggleModal}
|
isLoading={isLoading}
|
||||||
>
|
spinnerAriaValueText={isLoading ? 'Loading' : undefined}
|
||||||
{i18n._(t`Delete`)}
|
component="button"
|
||||||
</DropdownItem>
|
onClick={() => {
|
||||||
|
toggleModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DropdownItem>
|
||||||
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip content={renderTooltip()} position="top">
|
<Tooltip content={renderTooltip()} position="top">
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
spinnerAriaValueText={isLoading ? 'Loading' : undefined}
|
||||||
aria-label={i18n._(t`Delete`)}
|
aria-label={i18n._(t`Delete`)}
|
||||||
onClick={toggleModal}
|
onClick={() => toggleModal(true)}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
@@ -154,17 +249,22 @@ function ToolbarDeleteButton({
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={modalTitle}
|
title={modalTitle}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={toggleModal}
|
onClose={() => toggleModal(false)}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
|
ouiaId="delete-modal-confirm"
|
||||||
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`)}
|
||||||
@@ -173,7 +273,7 @@ function ToolbarDeleteButton({
|
|||||||
key="cancel"
|
key="cancel"
|
||||||
variant="link"
|
variant="link"
|
||||||
aria-label={i18n._(t`cancel delete`)}
|
aria-label={i18n._(t`cancel delete`)}
|
||||||
onClick={toggleModal}
|
onClick={() => toggleModal(false)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -186,8 +286,12 @@ function ToolbarDeleteButton({
|
|||||||
<br />
|
<br />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{warningMessage && (
|
{shouldShowDeleteWarning && (
|
||||||
<WarningMessage variant="warning" isInline title={warningMessage} />
|
<WarningMessage
|
||||||
|
variant="warning"
|
||||||
|
isInline
|
||||||
|
title={buildDeleteWarning()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { CredentialsAPI } from '../../api';
|
||||||
import ToolbarDeleteButton from './ToolbarDeleteButton';
|
import ToolbarDeleteButton from './ToolbarDeleteButton';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
|
||||||
const itemA = {
|
const itemA = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
@@ -19,27 +26,180 @@ const itemC = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('<ToolbarDeleteButton />', () => {
|
describe('<ToolbarDeleteButton />', () => {
|
||||||
|
let deleteDetailsRequests;
|
||||||
|
let wrapper;
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteDetailsRequests = [
|
||||||
|
{
|
||||||
|
label: 'Workflow Job Template Node',
|
||||||
|
request: CredentialsAPI.read.mockResolvedValue({ data: { count: 1 } }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
test('should render button', () => {
|
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);
|
||||||
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
|
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open confirmation modal', () => {
|
test('should open confirmation modal', async () => {
|
||||||
const wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
|
wrapper = mountWithContexts(
|
||||||
);
|
<ToolbarDeleteButton
|
||||||
|
onDelete={() => {}}
|
||||||
|
itemsToDelete={[itemA]}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage="Delete this?"
|
||||||
|
warningMessage="Are you sure to want to delete this"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Modal')).toHaveLength(0);
|
expect(wrapper.find('Modal')).toHaveLength(0);
|
||||||
wrapper.find('button').simulate('click');
|
await act(async () => {
|
||||||
wrapper.update();
|
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('Modal')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('div[aria-label="Workflow Job Template Node: 1"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('Button[aria-label="confirm delete"]').prop('isDisabled')
|
||||||
|
).toBe(false);
|
||||||
|
expect(wrapper.find('div[aria-label="Delete this?"]')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open confirmation with enabled delete button modal', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open delete error modal', async () => {
|
||||||
|
const request = [
|
||||||
|
{
|
||||||
|
label: 'Workflow Job Template Node',
|
||||||
|
request: CredentialsAPI.read.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'get',
|
||||||
|
url: '/api/v2/credentals',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
onDelete={() => {}}
|
||||||
|
itemsToDelete={[itemA]}
|
||||||
|
deleteDetailsRequests={request}
|
||||||
|
deleteMessage="Delete this?"
|
||||||
|
warningMessage="Are you sure to want to delete this"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('Modal')).toHaveLength(0);
|
||||||
|
await act(async () => wrapper.find('button').simulate('click'));
|
||||||
|
await waitForElement(wrapper, 'Modal', el => el.length > 0);
|
||||||
|
expect(CredentialsAPI.read).toBeCalled();
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('AlertModal[title="Error!"]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should invoke onDelete prop', () => {
|
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');
|
||||||
@@ -53,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);
|
||||||
@@ -68,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);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ exports[`<ToolbarDeleteButton /> should render button 1`] = `
|
|||||||
<Button
|
<Button
|
||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
isDisabled={true}
|
isDisabled={true}
|
||||||
|
isLoading={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
@@ -93,13 +94,14 @@ exports[`<ToolbarDeleteButton /> should render button 1`] = `
|
|||||||
<Button
|
<Button
|
||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
isDisabled={true}
|
isDisabled={true}
|
||||||
|
isLoading={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
className="pf-c-button pf-m-secondary pf-m-disabled"
|
className="pf-c-button pf-m-secondary pf-m-disabled pf-m-progress"
|
||||||
data-ouia-component-id="OUIA-Generated-Button-secondary-1"
|
data-ouia-component-id="OUIA-Generated-Button-secondary-1"
|
||||||
data-ouia-component-type="PF4/Button"
|
data-ouia-component-type="PF4/Button"
|
||||||
data-ouia-safe={true}
|
data-ouia-safe={true}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { getQSConfig, parseQueryString } from '../../util/qs';
|
|||||||
import useWsTemplates from '../../util/useWsTemplates';
|
import useWsTemplates from '../../util/useWsTemplates';
|
||||||
import AddDropDownButton from '../AddDropDownButton';
|
import AddDropDownButton from '../AddDropDownButton';
|
||||||
import TemplateListItem from './TemplateListItem';
|
import TemplateListItem from './TemplateListItem';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function TemplateList({ defaultParams, i18n }) {
|
function TemplateList({ defaultParams, i18n }) {
|
||||||
// The type value in const qsConfig below does not have a space between job_template and
|
// The type value in const qsConfig below does not have a space between job_template and
|
||||||
@@ -168,6 +169,11 @@ function TemplateList({ defaultParams, i18n }) {
|
|||||||
<AddDropDownButton key="add" dropdownItems={addDropDownButton} />
|
<AddDropDownButton key="add" dropdownItems={addDropDownButton} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -236,6 +242,11 @@ function TemplateList({ defaultParams, i18n }) {
|
|||||||
onDelete={handleTemplateDelete}
|
onDelete={handleTemplateDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Templates`)}
|
pluralizedItemName={i18n._(t`Templates`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This template is currently being used by some workflow nodes. Are you sure you want to delete it?} other {Deleting these templates could impact some workflow nodes that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
|||||||
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
|
||||||
import { Credential } from '../../../types';
|
import { Credential } from '../../../types';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const PluginInputMetadata = styled(CodeEditor)`
|
const PluginInputMetadata = styled(CodeEditor)`
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
@@ -183,6 +184,11 @@ function CredentialDetail({ i18n, credential }) {
|
|||||||
fetchDetails();
|
fetchDetails();
|
||||||
}, [fetchDetails]);
|
}, [fetchDetails]);
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
||||||
|
credential,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
@@ -270,9 +276,14 @@ function CredentialDetail({ i18n, credential }) {
|
|||||||
{user_capabilities.delete && (
|
{user_capabilities.delete && (
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
name={name}
|
name={name}
|
||||||
|
itemToDelete={credential}
|
||||||
modalTitle={i18n._(t`Delete Credential`)}
|
modalTitle={i18n._(t`Delete Credential`)}
|
||||||
onConfirm={deleteCredential}
|
onConfirm={deleteCredential}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This credential is currently being used by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ describe('<CredentialDetail />', () => {
|
|||||||
expect(wrapper.find('CredentialDetail').length).toBe(1);
|
expect(wrapper.find('CredentialDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(6);
|
||||||
|
});
|
||||||
|
|
||||||
test('should render details', () => {
|
test('should render details', () => {
|
||||||
expectDetailToMatch(wrapper, 'Name', mockCredential.name);
|
expectDetailToMatch(wrapper, 'Name', mockCredential.name);
|
||||||
expectDetailToMatch(wrapper, 'Description', mockCredential.description);
|
expectDetailToMatch(wrapper, 'Description', mockCredential.description);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { CredentialsAPI } from '../../../api';
|
import { CredentialsAPI } from '../../../api';
|
||||||
|
import useSelected from '../../../util/useSelected';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import DataListToolbar from '../../../components/DataListToolbar';
|
import DataListToolbar from '../../../components/DataListToolbar';
|
||||||
@@ -18,6 +19,7 @@ import PaginatedTable, {
|
|||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import CredentialListItem from './CredentialListItem';
|
import CredentialListItem from './CredentialListItem';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credential', {
|
const QS_CONFIG = getQSConfig('credential', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -26,9 +28,7 @@ const QS_CONFIG = getQSConfig('credential', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function CredentialList({ i18n }) {
|
function CredentialList({ i18n }) {
|
||||||
const [selected, setSelected] = useState([]);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: {
|
result: {
|
||||||
credentials,
|
credentials,
|
||||||
@@ -77,8 +77,10 @@ function CredentialList({ i18n }) {
|
|||||||
fetchCredentials();
|
fetchCredentials();
|
||||||
}, [fetchCredentials]);
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
const isAllSelected =
|
const { selected, isAllSelected, handleSelect, setSelected } = useSelected(
|
||||||
selected.length > 0 && selected.length === credentials.length;
|
credentials
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading: isDeleteLoading,
|
isLoading: isDeleteLoading,
|
||||||
deleteItems: deleteCredentials,
|
deleteItems: deleteCredentials,
|
||||||
@@ -100,21 +102,12 @@ function CredentialList({ i18n }) {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
|
||||||
setSelected(isSelected ? [...credentials] : []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = row => {
|
|
||||||
if (selected.some(s => s.id === row.id)) {
|
|
||||||
setSelected(selected.filter(s => s.id !== row.id));
|
|
||||||
} else {
|
|
||||||
setSelected(selected.concat(row));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -169,7 +162,9 @@ function CredentialList({ i18n }) {
|
|||||||
{...props}
|
{...props}
|
||||||
showSelectAll
|
showSelectAll
|
||||||
isAllSelected={isAllSelected}
|
isAllSelected={isAllSelected}
|
||||||
onSelectAll={handleSelectAll}
|
onSelectAll={isSelected =>
|
||||||
|
setSelected(isSelected ? [...credentials] : [])
|
||||||
|
}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
additionalControls={[
|
additionalControls={[
|
||||||
...(canAdd
|
...(canAdd
|
||||||
@@ -180,6 +175,11 @@ function CredentialList({ i18n }) {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Credentials`)}
|
pluralizedItemName={i18n._(t`Credentials`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This credential is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these credentials could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ describe('<CredentialList />', () => {
|
|||||||
expect(wrapper.find('CredentialList').length).toBe(1);
|
expect(wrapper.find('CredentialList').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).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', () => {
|
||||||
expect(CredentialsAPI.read).toHaveBeenCalled();
|
expect(CredentialsAPI.read).toHaveBeenCalled();
|
||||||
expect(wrapper.find('CredentialListItem').length).toBe(5);
|
expect(wrapper.find('CredentialListItem').length).toBe(5);
|
||||||
|
|||||||
@@ -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,6 +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,
|
||||||
|
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;
|
||||||
@@ -32,7 +37,35 @@ function CredentialTypeDetails({ credentialType, i18n }) {
|
|||||||
}, [id, history])
|
}, [id, history])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
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 }
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeleteDetails();
|
||||||
|
}, [fetchDeleteDetails]);
|
||||||
|
const { error, dismissError } = useDismissableError(
|
||||||
|
deleteError || deleteDetailsError
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -82,7 +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}
|
||||||
|
disabledTooltip={
|
||||||
|
isDeleteDisabled &&
|
||||||
|
i18n._(
|
||||||
|
t`This credential type is currently being used by some credentials and cannot be deleted`
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
@@ -95,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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,6 +96,38 @@ describe('<CredentialTypeDetails/>', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
test('expected api call is made for delete', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/credential_types/42/details'],
|
initialEntries: ['/credential_types/42/details'],
|
||||||
|
|||||||
@@ -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 CredentialTypeListItem from './CredentialTypeListItem';
|
import CredentialTypeListItem from './CredentialTypeListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credential-type', {
|
const QS_CONFIG = getQSConfig('credential-type', {
|
||||||
@@ -106,6 +106,11 @@ function CredentialTypeList({ i18n }) {
|
|||||||
|
|
||||||
const canAdd = actions && actions.POST;
|
const canAdd = actions && actions.POST;
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -162,6 +167,11 @@ function CredentialTypeList({ i18n }) {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Credential Types`)}
|
pluralizedItemName={i18n._(t`Credential Types`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This credential type is currently being used by some credentials and cannot be deleted.} other {Credential types that are being used by credentials cannot be deleted. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import { CredentialTypesAPI } from '../../../api';
|
import { CredentialTypesAPI, CredentialsAPI } from '../../../api';
|
||||||
import CredentialTypeList from './CredentialTypeList';
|
import CredentialTypeList from './CredentialTypeList';
|
||||||
|
|
||||||
jest.mock('../../../api/models/CredentialTypes');
|
jest.mock('../../../api/models/CredentialTypes');
|
||||||
|
jest.mock('../../../api/models/Credentials');
|
||||||
|
|
||||||
const credentialTypes = {
|
const credentialTypes = {
|
||||||
data: {
|
data: {
|
||||||
@@ -49,6 +50,12 @@ describe('<CredentialTypeList', () => {
|
|||||||
await waitForElement(wrapper, 'CredentialTypeList', el => el.length > 0);
|
await waitForElement(wrapper, 'CredentialTypeList', el => el.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should have data fetched and render 2 rows', async () => {
|
test('should have data fetched and render 2 rows', async () => {
|
||||||
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
|
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
|
||||||
CredentialTypesAPI.readOptions.mockResolvedValue(options);
|
CredentialTypesAPI.readOptions.mockResolvedValue(options);
|
||||||
@@ -65,6 +72,7 @@ describe('<CredentialTypeList', () => {
|
|||||||
test('should delete item successfully', async () => {
|
test('should delete item successfully', async () => {
|
||||||
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
|
CredentialTypesAPI.read.mockResolvedValue(credentialTypes);
|
||||||
CredentialTypesAPI.readOptions.mockResolvedValue(options);
|
CredentialTypesAPI.readOptions.mockResolvedValue(options);
|
||||||
|
CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<CredentialTypeList />);
|
wrapper = mountWithContexts(<CredentialTypeList />);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,10 +6,22 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import { ExecutionEnvironmentsAPI } from '../../../api';
|
import {
|
||||||
|
ExecutionEnvironmentsAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
OrganizationsAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
UnifiedJobTemplatesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import ExecutionEnvironmentList from './ExecutionEnvironmentList';
|
import ExecutionEnvironmentList from './ExecutionEnvironmentList';
|
||||||
|
|
||||||
jest.mock('../../../api/models/ExecutionEnvironments');
|
jest.mock('../../../api/models/ExecutionEnvironments');
|
||||||
|
jest.mock('../../../api/models/UnifiedJobTemplates');
|
||||||
|
jest.mock('../../../api/models/Projects');
|
||||||
|
jest.mock('../../../api/models/Organizations');
|
||||||
|
jest.mock('../../../api/models/InventorySources');
|
||||||
|
jest.mock('../../../api/models/WorkflowJobTemplateNodes');
|
||||||
|
|
||||||
const executionEnvironments = {
|
const executionEnvironments = {
|
||||||
data: {
|
data: {
|
||||||
@@ -43,6 +55,16 @@ describe('<ExecutionEnvironmentList/>', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments);
|
ExecutionEnvironmentsAPI.read.mockResolvedValue(executionEnvironments);
|
||||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options);
|
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(options);
|
||||||
|
InventorySourcesAPI.read.mockResolvedValue({
|
||||||
|
data: { results: [{ id: 10000000 }] },
|
||||||
|
});
|
||||||
|
WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
|
OrganizationsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
|
UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
|
ProjectsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -144,6 +166,11 @@ describe('<ExecutionEnvironmentList/>', () => {
|
|||||||
);
|
);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Button[aria-label="confirm delete"]',
|
||||||
|
el => el.length > 0
|
||||||
|
);
|
||||||
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 +212,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(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { jsonToYaml, isJsonString } from '../../../util/yaml';
|
import { jsonToYaml, isJsonString } from '../../../util/yaml';
|
||||||
import { InstanceGroupsAPI } from '../../../api';
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function ContainerGroupDetails({ instanceGroup, i18n }) {
|
function ContainerGroupDetails({ instanceGroup, i18n }) {
|
||||||
const { id, name } = instanceGroup;
|
const { id, name } = instanceGroup;
|
||||||
@@ -34,7 +35,10 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||||
|
instanceGroup,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList>
|
||||||
@@ -101,6 +105,10 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
|
|||||||
modalTitle={i18n._(t`Delete instance group`)}
|
modalTitle={i18n._(t`Delete instance group`)}
|
||||||
onConfirm={deleteInstanceGroup}
|
onConfirm={deleteInstanceGroup}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This container group is currently being by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '../../../components/DetailList';
|
} from '../../../components/DetailList';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { InstanceGroupsAPI } from '../../../api';
|
import { InstanceGroupsAPI } from '../../../api';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const Unavailable = styled.span`
|
const Unavailable = styled.span`
|
||||||
color: var(--pf-global--danger-color--200);
|
color: var(--pf-global--danger-color--200);
|
||||||
@@ -38,7 +39,10 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||||
|
instanceGroup,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
const verifyInstanceGroup = item => {
|
const verifyInstanceGroup = item => {
|
||||||
if (item.is_isolated) {
|
if (item.is_isolated) {
|
||||||
return (
|
return (
|
||||||
@@ -142,6 +146,10 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
|
|||||||
modalTitle={i18n._(t`Delete instance group`)}
|
modalTitle={i18n._(t`Delete instance group`)}
|
||||||
onConfirm={deleteInstanceGroup}
|
onConfirm={deleteInstanceGroup}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This instance group is currently being by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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 AddDropDownButton from '../../../components/AddDropDownButton';
|
import AddDropDownButton from '../../../components/AddDropDownButton';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
import InstanceGroupListItem from './InstanceGroupListItem';
|
import InstanceGroupListItem from './InstanceGroupListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('instance-group', {
|
const QS_CONFIG = getQSConfig('instance-group', {
|
||||||
@@ -186,7 +186,10 @@ function InstanceGroupList({ i18n }) {
|
|||||||
? `${match.url}/container_group/${item.id}/details`
|
? `${match.url}/container_group/${item.id}/details`
|
||||||
: `${match.url}/${item.id}/details`;
|
: `${match.url}/${item.id}/details`;
|
||||||
};
|
};
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -218,6 +221,11 @@ function InstanceGroupList({ i18n }) {
|
|||||||
itemsToDelete={modifiedSelected}
|
itemsToDelete={modifiedSelected}
|
||||||
pluralizedItemName={i18n._(t`Instance Groups`)}
|
pluralizedItemName={i18n._(t`Instance Groups`)}
|
||||||
errorMessage={errorMessageDelete}
|
errorMessage={errorMessageDelete}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This instance group is currently being by other resources. Are you sure you want to delete it?} other {Deleting these instance groups could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,18 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import { InstanceGroupsAPI } from '../../../api';
|
import {
|
||||||
|
InstanceGroupsAPI,
|
||||||
|
OrganizationsAPI,
|
||||||
|
InventoriesAPI,
|
||||||
|
UnifiedJobTemplatesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import InstanceGroupList from './InstanceGroupList';
|
import InstanceGroupList from './InstanceGroupList';
|
||||||
|
|
||||||
jest.mock('../../../api/models/InstanceGroups');
|
jest.mock('../../../api/models/InstanceGroups');
|
||||||
|
jest.mock('../../../api/models/Organizations');
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
jest.mock('../../../api/models/UnifiedJobTemplates');
|
||||||
|
|
||||||
const instanceGroups = {
|
const instanceGroups = {
|
||||||
data: {
|
data: {
|
||||||
@@ -44,6 +52,9 @@ const instanceGroups = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const options = { data: { actions: { POST: true } } };
|
const options = { data: { actions: { POST: true } } };
|
||||||
|
OrganizationsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
InventoriesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
UnifiedJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
describe('<InstanceGroupList />', () => {
|
describe('<InstanceGroupList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import ChipGroup from '../../../components/ChipGroup';
|
|||||||
import { InventoriesAPI } from '../../../api';
|
import { InventoriesAPI } from '../../../api';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { Inventory } from '../../../types';
|
import { Inventory } from '../../../types';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function InventoryDetail({ inventory, i18n }) {
|
function InventoryDetail({ inventory, i18n }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -54,6 +55,11 @@ function InventoryDetail({ inventory, i18n }) {
|
|||||||
user_capabilities: userCapabilities,
|
user_capabilities: userCapabilities,
|
||||||
} = inventory.summary_fields;
|
} = inventory.summary_fields;
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
|
||||||
|
inventory,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
@@ -126,6 +132,10 @@ function InventoryDetail({ inventory, i18n }) {
|
|||||||
name={inventory.name}
|
name={inventory.name}
|
||||||
modalTitle={i18n._(t`Delete Inventory`)}
|
modalTitle={i18n._(t`Delete Inventory`)}
|
||||||
onConfirm={deleteInventory}
|
onConfirm={deleteInventory}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This inventory is currently being used by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -105,6 +105,18 @@ describe('<InventoryDetail />', () => {
|
|||||||
expect(dates.at(1).prop('date')).toEqual(mockInventory.modified);
|
expect(dates.at(1).prop('date')).toEqual(mockInventory.modified);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryDetail inventory={mockInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
test('should load instance groups', async () => {
|
test('should load instance groups', async () => {
|
||||||
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getQSConfig, parseQueryString } from '../../../util/qs';
|
|||||||
import useWsInventories from './useWsInventories';
|
import useWsInventories from './useWsInventories';
|
||||||
import AddDropDownButton from '../../../components/AddDropDownButton';
|
import AddDropDownButton from '../../../components/AddDropDownButton';
|
||||||
import InventoryListItem from './InventoryListItem';
|
import InventoryListItem from './InventoryListItem';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('inventory', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -126,6 +127,12 @@ function InventoryList({ i18n }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
const addInventory = i18n._(t`Add inventory`);
|
const addInventory = i18n._(t`Add inventory`);
|
||||||
const addSmartInventory = i18n._(t`Add smart inventory`);
|
const addSmartInventory = i18n._(t`Add smart inventory`);
|
||||||
const addButton = (
|
const addButton = (
|
||||||
@@ -216,6 +223,11 @@ function InventoryList({ i18n }) {
|
|||||||
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
|
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
|
||||||
{ numItemsToDelete: selected.length }
|
{ numItemsToDelete: selected.length }
|
||||||
)}
|
)}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This inventory is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these inventories could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { InventoriesAPI } from '../../../api';
|
import {
|
||||||
|
InventoriesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import InventoryList from './InventoryList';
|
import InventoryList from './InventoryList';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
jest.mock('../../../api/models/JobTemplates');
|
||||||
|
jest.mock('../../../api/models/WorkflowJobTemplates');
|
||||||
|
|
||||||
const mockInventories = [
|
const mockInventories = [
|
||||||
{
|
{
|
||||||
@@ -136,6 +142,8 @@ describe('<InventoryList />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
||||||
global.console.debug = () => {};
|
global.console.debug = () => {};
|
||||||
});
|
});
|
||||||
@@ -155,6 +163,16 @@ describe('<InventoryList />', () => {
|
|||||||
expect(wrapper.find('InventoryListItem')).toHaveLength(3);
|
expect(wrapper.find('InventoryListItem')).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryList />);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
test('should select inventory when checked', async () => {
|
test('should select inventory when checked', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
|||||||
import Popover from '../../../components/Popover';
|
import Popover from '../../../components/Popover';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
import { InventorySourcesAPI } from '../../../api';
|
import { InventorySourcesAPI } from '../../../api';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function InventorySourceDetail({ inventorySource, i18n }) {
|
function InventorySourceDetail({ inventorySource, i18n }) {
|
||||||
const {
|
const {
|
||||||
@@ -96,6 +97,12 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
|
||||||
|
inventorySource.inventory,
|
||||||
|
i18n,
|
||||||
|
inventorySource
|
||||||
|
);
|
||||||
|
|
||||||
const VERBOSITY = {
|
const VERBOSITY = {
|
||||||
0: i18n._(t`0 (Warning)`),
|
0: i18n._(t`0 (Warning)`),
|
||||||
1: i18n._(t`1 (Info)`),
|
1: i18n._(t`1 (Info)`),
|
||||||
@@ -281,6 +288,10 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
name={name}
|
name={name}
|
||||||
modalTitle={i18n._(t`Delete inventory source`)}
|
modalTitle={i18n._(t`Delete inventory source`)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This inventory source is currently being used by other resources that rely on it. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -7,9 +7,20 @@ import {
|
|||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import InventorySourceDetail from './InventorySourceDetail';
|
import InventorySourceDetail from './InventorySourceDetail';
|
||||||
import mockInvSource from '../shared/data.inventory_source.json';
|
import mockInvSource from '../shared/data.inventory_source.json';
|
||||||
import { InventorySourcesAPI } from '../../../api';
|
import {
|
||||||
|
InventorySourcesAPI,
|
||||||
|
InventoriesAPI,
|
||||||
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
} from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api/models/InventorySources');
|
jest.mock('../../../api/models/InventorySources');
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
jest.mock('../../../api/models/WorkflowJobTemplateNodes');
|
||||||
|
|
||||||
|
InventoriesAPI.updateSources.mockResolvedValue({
|
||||||
|
data: [{ inventory_source: 1 }],
|
||||||
|
});
|
||||||
|
WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
InventorySourcesAPI.readOptions.mockResolvedValue({
|
InventorySourcesAPI.readOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -101,6 +112,17 @@ describe('InventorySourceDetail', () => {
|
|||||||
expect(wrapper.find('InventorySourceSyncButton')).toHaveLength(1);
|
expect(wrapper.find('InventorySourceSyncButton')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventorySourceDetail inventorySource={mockInvSource} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('should hide expected action buttons for users without permissions', async () => {
|
test('should hide expected action buttons for users without permissions', async () => {
|
||||||
const userCapabilities = {
|
const userCapabilities = {
|
||||||
edit: false,
|
edit: false,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import AlertModal from '../../../components/AlertModal/AlertModal';
|
|||||||
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
|
||||||
import InventorySourceListItem from './InventorySourceListItem';
|
import InventorySourceListItem from './InventorySourceListItem';
|
||||||
import useWsInventorySources from './useWsInventorySources';
|
import useWsInventorySources from './useWsInventorySources';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('inventory', {
|
const QS_CONFIG = getQSConfig('inventory', {
|
||||||
not__source: '',
|
not__source: '',
|
||||||
@@ -142,6 +143,12 @@ function InventorySourceList({ i18n }) {
|
|||||||
sourceChoicesOptions &&
|
sourceChoicesOptions &&
|
||||||
Object.prototype.hasOwnProperty.call(sourceChoicesOptions, 'POST');
|
Object.prototype.hasOwnProperty.call(sourceChoicesOptions, 'POST');
|
||||||
const listUrl = `/inventories/${inventoryType}/${id}/sources/`;
|
const listUrl = `/inventories/${inventoryType}/${id}/sources/`;
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
|
||||||
|
id,
|
||||||
|
i18n,
|
||||||
|
selected[0]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
@@ -174,6 +181,11 @@ function InventorySourceList({ i18n }) {
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Inventory Sources`)}
|
pluralizedItemName={i18n._(t`Inventory Sources`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This inventory source is currently being used by other resources that rely on it. Are you sure you want to delete it?} other {Deleting these inventory sources could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
...(canSyncSources
|
...(canSyncSources
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import React from 'react';
|
|||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { InventoriesAPI, InventorySourcesAPI } from '../../../api';
|
import {
|
||||||
|
InventoriesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -13,6 +17,7 @@ import InventorySourceList from './InventorySourceList';
|
|||||||
jest.mock('../../../api/models/InventorySources');
|
jest.mock('../../../api/models/InventorySources');
|
||||||
jest.mock('../../../api/models/Inventories');
|
jest.mock('../../../api/models/Inventories');
|
||||||
jest.mock('../../../api/models/InventoryUpdates');
|
jest.mock('../../../api/models/InventoryUpdates');
|
||||||
|
jest.mock('../../../api/models/WorkflowJobTemplateNodes');
|
||||||
|
|
||||||
const sources = {
|
const sources = {
|
||||||
data: {
|
data: {
|
||||||
@@ -61,6 +66,12 @@ describe('<InventorySourceList />', () => {
|
|||||||
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
||||||
global.console.debug = () => {};
|
global.console.debug = () => {};
|
||||||
InventoriesAPI.readSources.mockResolvedValue(sources);
|
InventoriesAPI.readSources.mockResolvedValue(sources);
|
||||||
|
InventoriesAPI.updateSources.mockResolvedValue({
|
||||||
|
data: [{ inventory_source: 1 }],
|
||||||
|
});
|
||||||
|
InventorySourcesAPI.readGroups.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
InventorySourcesAPI.readHosts.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
InventorySourcesAPI.readOptions.mockResolvedValue({
|
InventorySourcesAPI.readOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -119,6 +130,12 @@ describe('<InventorySourceList />', () => {
|
|||||||
expect(InventorySourcesAPI.readOptions).toHaveBeenCalled();
|
expect(InventorySourcesAPI.readOptions).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('source data should render properly', async () => {
|
test('source data should render properly', async () => {
|
||||||
await waitForElement(wrapper, 'InventorySourceList', el => el.length > 0);
|
await waitForElement(wrapper, 'InventorySourceList', el => el.length > 0);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('<JobDetail />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
const modal = wrapper.find('Modal');
|
const modal = wrapper.find('Modal');
|
||||||
expect(modal.length).toBe(1);
|
expect(modal.length).toBe(1);
|
||||||
modal.find('button[aria-label="Delete"]').simulate('click');
|
modal.find('button[aria-label="Confirm Delete"]').simulate('click');
|
||||||
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ describe('<JobDetail />', () => {
|
|||||||
const modal = wrapper.find('Modal');
|
const modal = wrapper.find('Modal');
|
||||||
expect(modal.length).toBe(1);
|
expect(modal.length).toBe(1);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
modal.find('button[aria-label="Delete"]').simulate('click');
|
modal.find('button[aria-label="Confirm Delete"]').simulate('click');
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
|
|||||||
@@ -188,9 +188,19 @@ describe('<JobOutput />', () => {
|
|||||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
|
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
|
||||||
await act(async () => {
|
await act(async () =>
|
||||||
wrapper.find('DeleteButton').invoke('onConfirm')();
|
wrapper.find('button[aria-label="Delete"]').simulate('click')
|
||||||
});
|
);
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Modal',
|
||||||
|
el => el.props().isOpen === true && el.props().title === 'Delete Job'
|
||||||
|
);
|
||||||
|
await act(async () =>
|
||||||
|
wrapper
|
||||||
|
.find('Modal button[aria-label="Confirm Delete"]')
|
||||||
|
.simulate('click')
|
||||||
|
);
|
||||||
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
|||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { useConfig } from '../../../contexts/Config';
|
import { useConfig } from '../../../contexts/Config';
|
||||||
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function OrganizationDetail({ i18n, organization }) {
|
function OrganizationDetail({ i18n, organization }) {
|
||||||
const {
|
const {
|
||||||
@@ -71,6 +72,11 @@ function OrganizationDetail({ i18n, organization }) {
|
|||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
|
||||||
|
organization,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
@@ -157,6 +163,10 @@ function OrganizationDetail({ i18n, organization }) {
|
|||||||
modalTitle={i18n._(t`Delete Organization`)}
|
modalTitle={i18n._(t`Delete Organization`)}
|
||||||
onConfirm={deleteOrganization}
|
onConfirm={deleteOrganization}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This organization is currently being by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI, CredentialsAPI } from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -44,6 +44,8 @@ describe('<OrganizationDetail />', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
OrganizationsAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
|
OrganizationsAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +66,20 @@ describe('<OrganizationDetail />', () => {
|
|||||||
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
let component;
|
||||||
|
await act(async () => {
|
||||||
|
component = mountWithContexts(
|
||||||
|
<OrganizationDetail organization={mockOrganization} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(component, 'ContentLoading', el => el.length === 0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
component.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(7);
|
||||||
|
});
|
||||||
|
|
||||||
test('should render the expected instance group', async () => {
|
test('should render the expected instance group', async () => {
|
||||||
let component;
|
let component;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import PaginatedTable, {
|
|||||||
} from '../../../components/PaginatedTable';
|
} from '../../../components/PaginatedTable';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import OrganizationListItem from './OrganizationListItem';
|
import OrganizationListItem from './OrganizationListItem';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('organization', {
|
const QS_CONFIG = getQSConfig('organization', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -116,6 +117,10 @@ function OrganizationsList({ i18n }) {
|
|||||||
setSelected(selected.concat(row));
|
setSelected(selected.concat(row));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -173,6 +178,11 @@ function OrganizationsList({ i18n }) {
|
|||||||
onDelete={handleOrgDelete}
|
onDelete={handleOrgDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Organizations`)}
|
pluralizedItemName={i18n._(t`Organizations`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This organization is currently being by other resources. Are you sure you want to delete it?} other {Deleting these organizations could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI, CredentialsAPI } from '../../../api';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
@@ -70,6 +70,7 @@ const mockOrganizations = {
|
|||||||
describe('<OrganizationsList />', () => {
|
describe('<OrganizationsList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
CredentialsAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
OrganizationsAPI.read.mockResolvedValue(mockOrganizations);
|
OrganizationsAPI.read.mockResolvedValue(mockOrganizations);
|
||||||
OrganizationsAPI.readOptions.mockResolvedValue({
|
OrganizationsAPI.readOptions.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -90,6 +91,20 @@ describe('<OrganizationsList />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
|
});
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'OrganizationsList',
|
||||||
|
el => el.find('ContentLoading').length === 0
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(7);
|
||||||
|
});
|
||||||
|
|
||||||
test('Items are rendered after loading', async () => {
|
test('Items are rendered after loading', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<OrganizationsList />);
|
wrapper = mountWithContexts(<OrganizationsList />);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import CredentialChip from '../../../components/CredentialChip';
|
|||||||
import { ProjectsAPI } from '../../../api';
|
import { ProjectsAPI } from '../../../api';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
import ProjectSyncButton from '../shared/ProjectSyncButton';
|
import ProjectSyncButton from '../shared/ProjectSyncButton';
|
||||||
|
|
||||||
function ProjectDetail({ project, i18n }) {
|
function ProjectDetail({ project, i18n }) {
|
||||||
@@ -52,7 +53,10 @@ function ProjectDetail({ project, i18n }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
|
||||||
|
project,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
let optionsList = '';
|
let optionsList = '';
|
||||||
if (
|
if (
|
||||||
scm_clean ||
|
scm_clean ||
|
||||||
@@ -171,6 +175,10 @@ function ProjectDetail({ project, i18n }) {
|
|||||||
modalTitle={i18n._(t`Delete Project`)}
|
modalTitle={i18n._(t`Delete Project`)}
|
||||||
onConfirm={deleteProject}
|
onConfirm={deleteProject}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This project is currently being used by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { ProjectsAPI } from '../../../api';
|
import {
|
||||||
|
ProjectsAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
} from '../../../api';
|
||||||
import ProjectDetail from './ProjectDetail';
|
import ProjectDetail from './ProjectDetail';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -147,6 +152,27 @@ describe('<ProjectDetail />', () => {
|
|||||||
expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
|
expect(wrapper.find('Detail[label="Options"]').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', () => {
|
||||||
|
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
const mockOptions = {
|
||||||
|
scm_type: '',
|
||||||
|
scm_clean: false,
|
||||||
|
scm_delete_on_update: false,
|
||||||
|
scm_update_on_launch: false,
|
||||||
|
allow_override: false,
|
||||||
|
created: '',
|
||||||
|
modified: '',
|
||||||
|
};
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<ProjectDetail project={{ ...mockProject, ...mockOptions }} />
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('should render with missing summary fields', async () => {
|
test('should render with missing summary fields', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ProjectDetail project={{ ...mockProject, summary_fields: {} }} />
|
<ProjectDetail project={{ ...mockProject, summary_fields: {} }} />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import PaginatedTable, {
|
|||||||
HeaderCell,
|
HeaderCell,
|
||||||
} from '../../../components/PaginatedTable';
|
} from '../../../components/PaginatedTable';
|
||||||
import useWsProjects from './useWsProjects';
|
import useWsProjects from './useWsProjects';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
|
|
||||||
import ProjectListItem from './ProjectListItem';
|
import ProjectListItem from './ProjectListItem';
|
||||||
@@ -116,6 +117,11 @@ function ProjectList({ i18n }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
|
||||||
|
selected[0],
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
@@ -194,6 +200,11 @@ function ProjectList({ i18n }) {
|
|||||||
onDelete={handleProjectDelete}
|
onDelete={handleProjectDelete}
|
||||||
itemsToDelete={selected}
|
itemsToDelete={selected}
|
||||||
pluralizedItemName={i18n._(t`Projects`)}
|
pluralizedItemName={i18n._(t`Projects`)}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
'{numItemsToDelete, plural, one {This project is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these projects could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||||
|
{ numItemsToDelete: selected.length }
|
||||||
|
)}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { ProjectsAPI } from '../../../api';
|
import {
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
ProjectsAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
} from '../../../api';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import ProjectList from './ProjectList';
|
import ProjectList from './ProjectList';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -83,6 +91,9 @@ const mockProjects = [
|
|||||||
|
|
||||||
describe('<ProjectList />', () => {
|
describe('<ProjectList />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
ProjectsAPI.read.mockResolvedValue({
|
ProjectsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: mockProjects.length,
|
count: mockProjects.length,
|
||||||
@@ -138,6 +149,17 @@ describe('<ProjectList />', () => {
|
|||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ProjectList />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper.find('ToolbarDeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('should select all', async () => {
|
test('should select all', async () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -177,10 +199,11 @@ describe('<ProjectList />', () => {
|
|||||||
.at(2)
|
.at(2)
|
||||||
.invoke('onSelect')();
|
.invoke('onSelect')();
|
||||||
});
|
});
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('ToolbarDeleteButton button').prop('disabled')).toEqual(
|
waitForElement(
|
||||||
true
|
wrapper,
|
||||||
|
'ToolbarDeleteButton button',
|
||||||
|
el => el.prop('disabled') === true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { VariablesDetail } from '../../../components/CodeEditor';
|
|||||||
import { JobTemplatesAPI } from '../../../api';
|
import { JobTemplatesAPI } from '../../../api';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function JobTemplateDetail({ i18n, template }) {
|
function JobTemplateDetail({ i18n, template }) {
|
||||||
const {
|
const {
|
||||||
@@ -96,6 +97,10 @@ function JobTemplateDetail({ i18n, template }) {
|
|||||||
|
|
||||||
const { error, dismissError } = useDismissableError(deleteError);
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||||
|
template,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
const canLaunch =
|
const canLaunch =
|
||||||
summary_fields.user_capabilities && summary_fields.user_capabilities.start;
|
summary_fields.user_capabilities && summary_fields.user_capabilities.start;
|
||||||
const verbosityOptions = [
|
const verbosityOptions = [
|
||||||
@@ -401,6 +406,10 @@ function JobTemplateDetail({ i18n, template }) {
|
|||||||
modalTitle={i18n._(t`Delete Job Template`)}
|
modalTitle={i18n._(t`Delete Job Template`)}
|
||||||
onConfirm={deleteJobTemplate}
|
onConfirm={deleteJobTemplate}
|
||||||
isDisabled={isDeleteLoading}
|
isDisabled={isDeleteLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This job template is currently being used by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import JobTemplateDetail from './JobTemplateDetail';
|
import JobTemplateDetail from './JobTemplateDetail';
|
||||||
import { JobTemplatesAPI } from '../../../api';
|
import { JobTemplatesAPI, WorkflowJobTemplateNodesAPI } from '../../../api';
|
||||||
import mockTemplate from '../shared/data.job_template.json';
|
import mockTemplate from '../shared/data.job_template.json';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -25,6 +25,7 @@ describe('<JobTemplateDetail />', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
JobTemplatesAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
|
JobTemplatesAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
|
||||||
|
WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<JobTemplateDetail template={mockTemplate} />
|
<JobTemplateDetail template={mockTemplate} />
|
||||||
@@ -56,6 +57,23 @@ describe('<JobTemplateDetail />', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<JobTemplateDetail
|
||||||
|
template={{
|
||||||
|
...mockTemplate,
|
||||||
|
become_enabled: true,
|
||||||
|
summary_fields: { user_capabilities: { delete: true } },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should request instance groups from api', async () => {
|
test('should request instance groups from api', async () => {
|
||||||
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ErrorDetail from '../../../components/ErrorDetail';
|
|||||||
import { LaunchButton } from '../../../components/LaunchButton';
|
import { LaunchButton } from '../../../components/LaunchButton';
|
||||||
import Sparkline from '../../../components/Sparkline';
|
import Sparkline from '../../../components/Sparkline';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
|
||||||
function WorkflowJobTemplateDetail({ template, i18n }) {
|
function WorkflowJobTemplateDetail({ template, i18n }) {
|
||||||
@@ -102,6 +103,11 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
|
|||||||
type: 'workflow_job',
|
type: 'workflow_job',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||||
|
template,
|
||||||
|
i18n
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
@@ -241,6 +247,10 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
|
|||||||
modalTitle={i18n._(t`Delete Workflow Job Template`)}
|
modalTitle={i18n._(t`Delete Workflow Job Template`)}
|
||||||
onConfirm={deleteWorkflowJobTemplate}
|
onConfirm={deleteWorkflowJobTemplate}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={i18n._(
|
||||||
|
t`This workflow job template is currently being used by other resources. Are you sure you want to delete it?`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Delete`)}
|
{i18n._(t`Delete`)}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { act } from 'react-dom/test-utils';
|
|||||||
|
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
import WorkflowJobTemplateDetail from './WorkflowJobTemplateDetail';
|
||||||
|
import { WorkflowJobTemplateNodesAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
describe('<WorkflowJobTemplateDetail/>', () => {
|
describe('<WorkflowJobTemplateDetail/>', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
@@ -50,6 +53,7 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
WorkflowJobTemplateNodesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
history = createMemoryHistory({
|
history = createMemoryHistory({
|
||||||
initialEntries: ['/templates/workflow_job_template/1/details'],
|
initialEntries: ['/templates/workflow_job_template/1/details'],
|
||||||
});
|
});
|
||||||
@@ -86,6 +90,7 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders successfully', () => {
|
test('renders successfully', () => {
|
||||||
@@ -163,6 +168,12 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
).toBe('Demo EE');
|
).toBe('Demo EE');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have proper number of delete detail requests', async () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('DeleteButton').prop('deleteDetailsRequests')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('link out resource have the correct url', () => {
|
test('link out resource have the correct url', () => {
|
||||||
const inventory = wrapper.find('Detail[label="Inventory"]').find('Link');
|
const inventory = wrapper.find('Detail[label="Inventory"]').find('Link');
|
||||||
const organization = wrapper
|
const organization = wrapper
|
||||||
|
|||||||
144
awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js
Normal file
144
awx/ui_next/src/util/getRelatedResouceDeleteDetails.test.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
getRelatedResourceDeleteCounts,
|
||||||
|
relatedResourceDeleteRequests,
|
||||||
|
} from './getRelatedResourceDeleteDetails';
|
||||||
|
import {
|
||||||
|
InventoriesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
jest.mock('../api/models/Credentials');
|
||||||
|
jest.mock('../api/models/Inventories');
|
||||||
|
jest.mock('../api/models/InventorySources');
|
||||||
|
jest.mock('../api/models/JobTemplates');
|
||||||
|
jest.mock('../api/models/Projects');
|
||||||
|
jest.mock('../api/models/WorkflowJobTemplates');
|
||||||
|
jest.mock('../api/models/WorkflowJobTemplateNodes');
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
_: key => {
|
||||||
|
if (key.values) {
|
||||||
|
Object.entries(key.values).forEach(([k, v]) => {
|
||||||
|
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return key.id;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('delete details', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for credentials list', () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(InventoriesAPI.read).toBeCalledWith({
|
||||||
|
insights_credential: 1,
|
||||||
|
});
|
||||||
|
expect(InventorySourcesAPI.read).toBeCalledWith({
|
||||||
|
credentials__id: 1,
|
||||||
|
});
|
||||||
|
expect(JobTemplatesAPI.read).toBeCalledWith({ credentials: 1 });
|
||||||
|
expect(ProjectsAPI.read).toBeCalledWith({ credentials: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for projects list', () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.project({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||||
|
unified_job_template: 1,
|
||||||
|
});
|
||||||
|
expect(InventorySourcesAPI.read).toBeCalledWith({
|
||||||
|
source_project: 1,
|
||||||
|
});
|
||||||
|
expect(JobTemplatesAPI.read).toBeCalledWith({ project: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for templates list', () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.template({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||||
|
unified_job_template: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for credential type list', () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.credentialType({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(CredentialsAPI.read).toBeCalledWith({
|
||||||
|
credential_type__id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for inventory list', () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.inventory({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 });
|
||||||
|
expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({
|
||||||
|
inventory: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for inventory source list', async () => {
|
||||||
|
InventoriesAPI.updateSources.mockResolvedValue({
|
||||||
|
data: [{ inventory_source: 2 }],
|
||||||
|
});
|
||||||
|
await getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.inventorySource(1, i18n)
|
||||||
|
);
|
||||||
|
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||||
|
unified_job_template: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api for organization list', async () => {
|
||||||
|
getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.organization({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call return error for inventory source list', async () => {
|
||||||
|
InventoriesAPI.updateSources.mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'post',
|
||||||
|
url: '/api/v2/inventories/1/ad_hoc_commands',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { error } = await getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.inventorySource(1, i18n)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return proper results', async () => {
|
||||||
|
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
ProjectsAPI.read.mockResolvedValue({ data: { count: 2 } });
|
||||||
|
InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } });
|
||||||
|
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||||
|
|
||||||
|
const { results } = await getRelatedResourceDeleteCounts(
|
||||||
|
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
|
||||||
|
);
|
||||||
|
expect(results).toEqual({ Projects: 2, Inventories: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
301
awx/ui_next/src/util/getRelatedResourceDeleteDetails.js
Normal file
301
awx/ui_next/src/util/getRelatedResourceDeleteDetails.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UnifiedJobTemplatesAPI,
|
||||||
|
CredentialsAPI,
|
||||||
|
InventoriesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
|
CredentialInputSourcesAPI,
|
||||||
|
TeamsAPI,
|
||||||
|
NotificationTemplatesAPI,
|
||||||
|
ExecutionEnvironmentsAPI,
|
||||||
|
ApplicationsAPI,
|
||||||
|
OrganizationsAPI,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
export async function getRelatedResourceDeleteCounts(requests) {
|
||||||
|
const results = {};
|
||||||
|
let error = null;
|
||||||
|
let hasCount = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
requests.map(async ({ request, label }) => {
|
||||||
|
const {
|
||||||
|
data: { count },
|
||||||
|
} = await request();
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
results[label] = count;
|
||||||
|
hasCount = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: hasCount && results,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const relatedResourceDeleteRequests = {
|
||||||
|
credential: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
JobTemplatesAPI.read({
|
||||||
|
credentials: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Job Templates`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () => ProjectsAPI.read({ credentials: selected.id }),
|
||||||
|
label: i18n._(t`Projects`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
InventoriesAPI.read({
|
||||||
|
insights_credential: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Inventories`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
InventorySourcesAPI.read({
|
||||||
|
credentials__id: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Inventory Sources`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
CredentialInputSourcesAPI.read({
|
||||||
|
source_credential: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Credential`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
ExecutionEnvironmentsAPI.read({
|
||||||
|
credential: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Execution Environments`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
credentialType: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
CredentialsAPI.read({
|
||||||
|
credential_type__id: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Credentials`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
inventory: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
JobTemplatesAPI.read({
|
||||||
|
inventory: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Job Templates`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () => WorkflowJobTemplatesAPI.read({ inventory: selected.id }),
|
||||||
|
label: i18n._(t`Workflow Job Template`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
inventorySource: (inventoryId, i18n, inventorySource) => [
|
||||||
|
{
|
||||||
|
request: async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await InventoriesAPI.updateSources(inventoryId);
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
data.map(async datum =>
|
||||||
|
WorkflowJobTemplateNodesAPI.read({
|
||||||
|
unified_job_template: datum.inventory_source,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const total = results.reduce(
|
||||||
|
({ data: { count: acc } }, { data: { count: cur } }) => acc + cur,
|
||||||
|
{ data: { count: 0 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data: { count: total } };
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: i18n._(t`Workflow Job Template Nodes`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () => InventorySourcesAPI.readGroups(inventorySource.id),
|
||||||
|
label: i18n._(t`Groups`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () => InventorySourcesAPI.readHosts(inventorySource.id),
|
||||||
|
label: i18n._(t`Hosts`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
project: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
JobTemplatesAPI.read({
|
||||||
|
project: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Job Templates`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
WorkflowJobTemplateNodesAPI.read({
|
||||||
|
unified_job_template: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Workflow Job Templates`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
InventorySourcesAPI.read({
|
||||||
|
source_project: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Inventory Sources`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
template: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
WorkflowJobTemplateNodesAPI.read({
|
||||||
|
unified_job_template: selected.id,
|
||||||
|
}),
|
||||||
|
label: [i18n._(t`Workflow Job Template Nodes`)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
organization: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
CredentialsAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Credential`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
TeamsAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Teams`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
NotificationTemplatesAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Notification Templates`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
ExecutionEnvironmentsAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Execution Environments`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
ProjectsAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: [i18n._(t`Projects`)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
InventoriesAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Inventories`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
ApplicationsAPI.read({
|
||||||
|
organization: selected.id,
|
||||||
|
}),
|
||||||
|
label: i18n._(t`Applications`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executionEnvironment: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
UnifiedJobTemplatesAPI.read({
|
||||||
|
execution_environment: selected.id,
|
||||||
|
}),
|
||||||
|
label: [i18n._(t`Templates`)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
ProjectsAPI.read({
|
||||||
|
default_environment: selected.id,
|
||||||
|
}),
|
||||||
|
label: [i18n._(t`Projects`)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () =>
|
||||||
|
OrganizationsAPI.read({
|
||||||
|
default_environment: selected.id,
|
||||||
|
}),
|
||||||
|
label: [i18n._(t`Organizations`)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: async () => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { results },
|
||||||
|
} = await InventorySourcesAPI.read({
|
||||||
|
execution_environment: selected.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responses = await Promise.all(
|
||||||
|
results.map(result =>
|
||||||
|
WorkflowJobTemplateNodesAPI.read({
|
||||||
|
unified_job_template: result.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = responses.reduce(
|
||||||
|
({ data: { count: acc } }, { data: { count: cur } }) => acc + cur,
|
||||||
|
{ data: { count: 0 } }
|
||||||
|
);
|
||||||
|
return { data: { count: total } };
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: [i18n._(t`Workflow Job Template Nodes`)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
instanceGroup: (selected, i18n) => [
|
||||||
|
{
|
||||||
|
request: () => OrganizationsAPI.read({ instance_groups: selected.id }),
|
||||||
|
label: i18n._(t`Organizations`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () => InventoriesAPI.read({ instance_groups: selected.id }),
|
||||||
|
label: i18n._(t`Inventories`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: () =>
|
||||||
|
UnifiedJobTemplatesAPI.read({ instance_groups: selected.id }),
|
||||||
|
label: i18n._(t`Templates`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user