Adds delete warnings on the lists and detail pages that have resources that are used by other resources

This commit is contained in:
Alex Corey
2021-02-17 14:03:51 -05:00
parent 14f42af700
commit 7e7bb5261b
24 changed files with 799 additions and 88 deletions

View File

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

View File

@@ -77,7 +77,7 @@ function AlertModal({
aria-label={label || i18n._(t`Alert modal`)} aria-label={label || i18n._(t`Alert modal`)}
aria-labelledby="alert-modal-header-label" aria-labelledby="alert-modal-header-label"
isOpen={Boolean(isOpen)} isOpen={Boolean(isOpen)}
variant="small" variant="medium"
title={title} title={title}
{...props} {...props}
> >

View File

@@ -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 } 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 DetailsWrapper = styled.span`
:not(:first-of-type) {
padding-left: 10px;
}
`;
function DeleteButton({ function DeleteButton({
onConfirm, onConfirm,
modalTitle, modalTitle,
@@ -14,51 +25,101 @@ function DeleteButton({
children, children,
isDisabled, isDisabled,
ouiaId, ouiaId,
deleteMessage,
deleteDetailsRequests,
}) { }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [deleteMessageError, setDeleteMessageError] = useState();
const [deleteDetails, setDeleteDetails] = useState({});
const toggleModal = async isModalOpen => {
if (deleteDetailsRequests?.length && isModalOpen) {
const { results, error } = await getRelatedResourceDeleteCounts(
deleteDetailsRequests
);
if (error) {
setDeleteMessageError(error);
} else {
setDeleteDetails(results);
}
}
setIsOpen(isModalOpen);
};
if (deleteMessageError) {
return (
<AlertModal
isOpen={deleteMessageError}
title={i18n._(t`Error!`)}
onClose={() => {
toggleModal(false);
setDeleteMessageError();
}}
>
<ErrorDetail error={deleteMessageError} />
</AlertModal>
);
}
return ( return (
<> <>
<Button <Button
variant={variant || 'secondary'} variant={variant || 'secondary'}
aria-label={i18n._(t`Delete`)} aria-label={i18n._(t`Delete`)}
isDisabled={isDisabled} isDisabled={isDisabled}
onClick={() => setIsOpen(true)} onClick={() => toggleModal(true)}
ouiaId={ouiaId}
> >
{children || i18n._(t`Delete`)} {children || i18n._(t`Delete`)}
</Button> </Button>
<AlertModal {!deleteMessageError && (
isOpen={isOpen} <AlertModal
title={modalTitle} isOpen={isOpen}
variant="danger" title={modalTitle}
onClose={() => setIsOpen(false)} variant="danger"
actions={[ onClose={() => toggleModal(false)}
<Button actions={[
ouiaId="delete-modal-confirm" <Button
key="delete" ouiaId="delete-modal-confirm"
variant="danger" key="delete"
aria-label={i18n._(t`Delete`)} variant="danger"
isDisabled={isDisabled} aria-label={i18n._(t`Confirm Delete`)}
onClick={onConfirm} isDisabled={isDisabled}
> onClick={onConfirm}
{i18n._(t`Delete`)} >
</Button>, {i18n._(t`Delete`)}
<Button </Button>,
ouiaId="delete-modal-cancel" <Button
key="cancel" ouiaId="delete-modal-cancel"
variant="link" key="cancel"
aria-label={i18n._(t`Cancel`)} variant="link"
onClick={() => setIsOpen(false)} aria-label={i18n._(t`Cancel`)}
> onClick={() => toggleModal(false)}
{i18n._(t`Cancel`)} >
</Button>, {i18n._(t`Cancel`)}
]} </Button>,
> ]}
{i18n._(t`Are you sure you want to delete:`)} >
<br /> {i18n._(t`Are you sure you want to delete:`)}
<strong>{name}</strong> <br />
</AlertModal> <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]) => (
<DetailsWrapper aria-label={`${key}: ${value}`} key={key}>
<span>{key}</span> <Badge>{value}</Badge>
</DetailsWrapper>
))}
</div>
}
/>
)}
</AlertModal>
)}
</> </>
); );
} }

View File

@@ -0,0 +1,113 @@
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"
/>
);
});
// expect(wrapper.find('Modal')).toHaveLength(0);
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);
});
});

View File

@@ -10,15 +10,29 @@ 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 DetailsWrapper = styled.span`
:not(:first-of-type) {
padding-left: 10px;
}
`;
const requiredField = props => { const requiredField = props => {
const { name, username, image } = props; const { name, username, image } = props;
@@ -77,20 +91,40 @@ 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({});
const deleteMessages = [warningMessage, deleteMessage].filter(
message => message
);
const [deleteMessageError, setDeleteMessageError] = useState();
const handleDelete = () => { const handleDelete = () => {
onDelete(); onDelete();
toggleModal(); toggleModal();
}; };
const toggleModal = () => { const toggleModal = async isOpen => {
setIsModalOpen(!isModalOpen); if (itemsToDelete.length === 1 && deleteDetailsRequests?.length > 0) {
const { results, error } = await getRelatedResourceDeleteCounts(
deleteDetailsRequests
);
if (error) {
setDeleteMessageError(error);
} else {
setDeleteDetails(results);
}
}
setIsModalOpen(isOpen);
}; };
useEffect(() => { useEffect(() => {
@@ -126,40 +160,55 @@ 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, if (deleteMessageError) {
// we can delete the extra <div> around the <DeleteButton> below. return (
// See: https://github.com/patternfly/patternfly-react/issues/1894 <AlertModal
isOpen={deleteMessageError}
title={i18n._(t`Error!`)}
onClose={() => {
toggleModal(false);
setDeleteMessageError();
}}
>
<ErrorDetail error={deleteMessageError} />
</AlertModal>
);
}
return ( return (
<> <>
{isKebabified ? ( {isKebabified ? (
<DropdownItem <Tooltip content={renderTooltip()} position="top">
key="add" <DropdownItem
isDisabled={isDisabled} key="add"
component="button" isDisabled={isDisabled}
onClick={toggleModal} component="button"
> onClick={() => toggleModal(true)}
{i18n._(t`Delete`)} >
</DropdownItem> {i18n._(t`Delete`)}
</DropdownItem>
</Tooltip>
) : ( ) : (
<Tooltip content={renderTooltip()} position="top"> <Tooltip content={renderTooltip()} position="top">
<div> <div>
<Button <Button
variant="secondary" variant="secondary"
aria-label={i18n._(t`Delete`)} aria-label={i18n._(t`Delete`)}
onClick={toggleModal} onClick={() => toggleModal(true)}
isDisabled={isDisabled} isAriaDisabled={isDisabled}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</Button> </Button>
</div> </div>
</Tooltip> </Tooltip>
)} )}
{isModalOpen && (
{isModalOpen && !deleteMessageError && (
<AlertModal <AlertModal
variant="danger" variant="danger"
title={modalTitle} title={modalTitle}
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={toggleModal} onClose={() => toggleModal(false)}
actions={[ actions={[
<Button <Button
key="delete" key="delete"
@@ -173,7 +222,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 +235,43 @@ function ToolbarDeleteButton({
<br /> <br />
</span> </span>
))} ))}
{warningMessage && ( {itemsToDelete.length === 1 &&
<WarningMessage variant="warning" isInline title={warningMessage} /> Object.values(deleteDetails).length > 0 && (
<WarningMessage
variant="warning"
isInline
title={
<div>
{deleteMessages.map(message => (
<div aria-label={message} key={message}>
{message}
</div>
))}
{itemsToDelete.length === 1 && (
<>
<br />
{Object.entries(deleteDetails).map(([key, value]) => (
<DetailsWrapper
key={key}
aria-label={`${key}: ${value}`}
>
<span>{key}</span> <Badge>{value}</Badge>
</DetailsWrapper>
))}
</>
)}
</div>
}
/>
)}
{itemsToDelete.length > 1 && (
<WarningMessage
variant="warning"
isInline
title={deleteMessages.map(message => (
<div>{message}</div>
))}
/>
)} )}
</AlertModal> </AlertModal>
)} )}

View File

@@ -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,6 +26,15 @@ const itemC = {
}; };
describe('<ToolbarDeleteButton />', () => { describe('<ToolbarDeleteButton />', () => {
let deleteDetailsRequests;
beforeEach(() => {
deleteDetailsRequests = [
{
label: 'job',
request: CredentialsAPI.read.mockResolvedValue({ data: { count: 1 } }),
},
];
});
test('should render button', () => { test('should render button', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[]} /> <ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[]} />
@@ -27,14 +43,27 @@ describe('<ToolbarDeleteButton />', () => {
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot(); expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
}); });
test('should open confirmation modal', () => { test('should open confirmation modal', async () => {
const wrapper = mountWithContexts( let wrapper;
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} /> await act(async () => {
); 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(wrapper.find('Modal')).toHaveLength(1); expect(wrapper.find('Modal')).toHaveLength(1);
expect(wrapper.find('div[aria-label="Delete this?"]')).toHaveLength(1);
}); });
test('should invoke onDelete prop', () => { test('should invoke onDelete prop', () => {

View File

@@ -74,7 +74,7 @@ exports[`<ToolbarDeleteButton /> should render button 1`] = `
> >
<Button <Button
aria-label="Delete" aria-label="Delete"
isDisabled={true} isAriaDisabled={true}
onClick={[Function]} onClick={[Function]}
variant="secondary" variant="secondary"
> >
@@ -92,19 +92,20 @@ exports[`<ToolbarDeleteButton /> should render button 1`] = `
> >
<Button <Button
aria-label="Delete" aria-label="Delete"
isDisabled={true} isAriaDisabled={true}
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-aria-disabled"
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}
disabled={true} disabled={false}
onClick={[Function]} onClick={[Function]}
onKeyPress={[Function]}
role={null} role={null}
tabIndex={null} tabIndex={null}
type="button" type="button"

View File

@@ -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 { deleteRequests } from '../../util/getDeleteDetails';
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,8 @@ function TemplateList({ defaultParams, i18n }) {
<AddDropDownButton key="add" dropdownItems={addDropDownButton} /> <AddDropDownButton key="add" dropdownItems={addDropDownButton} />
); );
const deleteDetailsRequests = deleteRequests.template(selected[0], i18n);
return ( return (
<Fragment> <Fragment>
<Card> <Card>
@@ -236,6 +239,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 }
)}
/>, />,
]} ]}
/> />

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import {
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';
import { CredentialTypesAPI } from '../../../api'; import { CredentialTypesAPI } from '../../../api';
import { jsonToYaml } from '../../../util/yaml'; import { jsonToYaml } from '../../../util/yaml';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
function CredentialTypeDetails({ credentialType, i18n }) { function CredentialTypeDetails({ credentialType, i18n }) {
const { id, name, description, injectors, inputs } = credentialType; const { id, name, description, injectors, inputs } = credentialType;
@@ -32,6 +33,11 @@ function CredentialTypeDetails({ credentialType, i18n }) {
}, [id, history]) }, [id, history])
); );
const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType(
credentialType,
i18n
);
const { error, dismissError } = useDismissableError(deleteError); const { error, dismissError } = useDismissableError(deleteError);
return ( return (
@@ -83,6 +89,10 @@ function CredentialTypeDetails({ credentialType, i18n }) {
modalTitle={i18n._(t`Delete credential type`)} modalTitle={i18n._(t`Delete credential type`)}
onConfirm={deleteCredentialType} onConfirm={deleteCredentialType}
isDisabled={isLoading} isDisabled={isLoading}
deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={i18n._(
t`This credential type is currently being used by some credentials. Are you sure you want to delete it?`
)}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</DeleteButton> </DeleteButton>

View File

@@ -19,7 +19,7 @@ import PaginatedTable, {
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import DatalistToolbar from '../../../components/DataListToolbar'; import DatalistToolbar from '../../../components/DataListToolbar';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import 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. Are you sure you want to delete it?} other {Deleting these credential types could impact other credentials that rely on them. Are you sure you want to delete anyway?}}',
{ numItemsToDelete: selected.length }
)}
/>, />,
]} ]}
/> />

View File

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

View File

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

View File

@@ -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,11 @@ function InventorySourceDetail({ inventorySource, i18n }) {
} }
}; };
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
inventorySource.inventory,
i18n
);
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 +287,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 some workflow job template nodes. Are you sure you want to delete it?`
)}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</DeleteButton> </DeleteButton>

View File

@@ -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,11 @@ 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
);
return ( return (
<> <>
<PaginatedDataList <PaginatedDataList
@@ -174,6 +180,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 workflow job template nodes. Are you sure you want to delete it?} other {Deleting these inventory sources could impact some workflow job template nodes that rely on them. Are you sure you want to delete anyway?}}',
{ numItemsToDelete: selected.length }
)}
/>, />,
...(canSyncSources ...(canSyncSources
? [ ? [

View File

@@ -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 used some credentials. Are you sure you want to delete it?`
)}
> >
{i18n._(t`Delete`)} {i18n._(t`Delete`)}
</DeleteButton> </DeleteButton>

View File

@@ -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 used some credentials. Are you sure you want to delete it?} other {Deleting these organizations could impact some credentials that rely on them. Are you sure you want to delete anyway?}}',
{ numItemsToDelete: selected.length }
)}
/>, />,
]} ]}
/> />

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
import { t } from '@lingui/macro';
import {
CredentialsAPI,
InventoriesAPI,
InventorySourcesAPI,
JobTemplatesAPI,
ProjectsAPI,
WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI,
} from '../api';
export default async function getDeleteDetails(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 deleteRequests = {
credential: (selected, i18n) => [
{
request: async () =>
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`),
},
],
template: (selected, i18n) => [
{
request: async () =>
WorkflowJobTemplateNodesAPI.read({
unified_job_template: selected.id,
}),
label: [i18n._(t`Workflow Job Template Node`)],
},
],
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) => [
{
request: async () => {
try {
const { data } = await InventoriesAPI.updateSources(inventoryId);
return WorkflowJobTemplateNodesAPI.read({
unified_job_template: data[0].inventory_source,
});
} catch (err) {
throw new Error(err);
}
},
label: i18n._(t`Workflow Job Template Node`),
},
],
organization: (selected, i18n) => [
{
request: async () =>
CredentialsAPI.read({
organization: selected.id,
}),
label: i18n._(t`Credential`),
},
],
};

View File

@@ -0,0 +1,149 @@
import { t } from '@lingui/macro';
import {
CredentialsAPI,
InventoriesAPI,
InventorySourcesAPI,
JobTemplatesAPI,
ProjectsAPI,
WorkflowJobTemplateNodesAPI,
WorkflowJobTemplatesAPI,
} 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: async () =>
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`),
},
],
project: (selected, i18n) => [
{
request: async () =>
JobTemplatesAPI.read({
credentials: selected.id,
}),
label: i18n._(t`Job Templates`),
},
{
request: () => WorkflowJobTemplatesAPI.read({ credentials: selected.id }),
label: i18n._(t`Workflow Job Templates`),
},
{
request: () =>
InventorySourcesAPI.read({
credentials__id: 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 Node`)],
},
],
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) => [
{
request: async () => {
try {
const { data } = await InventoriesAPI.updateSources(inventoryId);
return WorkflowJobTemplateNodesAPI.read({
unified_job_template: data[0].inventory_source,
});
} catch (err) {
throw new Error(err);
}
},
label: i18n._(t`Workflow Job Template Node`),
},
],
organization: (selected, i18n) => [
{
request: async () =>
CredentialsAPI.read({
organization: selected.id,
}),
label: i18n._(t`Credential`),
},
],
};