Catalog: Add dialog to delete entity

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2021-05-18 16:38:55 +02:00
parent fce24d462c
commit d720a3ecf8
4 changed files with 222 additions and 30 deletions
@@ -0,0 +1,132 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import userEvent from '@testing-library/user-event';
import React from 'react';
import { DeleteEntityDialog } from './DeleteEntityDialog';
import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model';
import {
AlertApi,
alertApiRef,
ApiProvider,
ApiRegistry,
} from '@backstage/core';
import { CatalogApi } from '@backstage/catalog-client';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { screen, waitFor } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
describe('DeleteEntityDialog', () => {
const alertApi: jest.Mocked<AlertApi> = {
post: jest.fn(),
alert$: jest.fn(),
};
const catalogClient: jest.Mocked<CatalogApi> = {
removeEntityByUid: jest.fn(),
} as any;
const apis = ApiRegistry.with(catalogApiRef, catalogClient).with(
alertApiRef,
alertApi,
);
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
uid: '123',
name: 'n',
namespace: 'ns',
annotations: {
[ORIGIN_LOCATION_ANNOTATION]: 'url:http://example.com',
},
},
spec: {},
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
);
afterEach(() => {
jest.resetAllMocks();
});
it('can cancel', async () => {
const onClose = jest.fn();
await renderInTestApp(
<Wrapper>
<DeleteEntityDialog
open
onClose={onClose}
onConfirm={() => {}}
entity={entity}
/>
</Wrapper>,
);
userEvent.click(screen.getByText('Cancel'));
await waitFor(() => {
expect(onClose).toBeCalled();
});
});
it('can delete', async () => {
const onConfirm = jest.fn();
await renderInTestApp(
<Wrapper>
<DeleteEntityDialog
open
onClose={() => {}}
onConfirm={onConfirm}
entity={entity}
/>
</Wrapper>,
);
userEvent.click(screen.getByText('Delete'));
await waitFor(() => {
expect(catalogClient.removeEntityByUid).toBeCalledWith('123');
expect(onConfirm).toBeCalled();
});
});
it('handles error', async () => {
const onConfirm = jest.fn();
await renderInTestApp(
<Wrapper>
<DeleteEntityDialog
open
onClose={() => {}}
onConfirm={onConfirm}
entity={entity}
/>
</Wrapper>,
);
catalogClient.removeEntityByUid.mockRejectedValue(new Error('no no no'));
userEvent.click(screen.getByText('Delete'));
await waitFor(() => {
expect(catalogClient.removeEntityByUid).toBeCalledWith('123');
expect(alertApi.post).toBeCalledWith({ message: 'no no no' });
});
});
});
@@ -0,0 +1,73 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { alertApiRef, useApi } from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Button, Dialog, DialogActions, DialogTitle } from '@material-ui/core';
import React, { useState } from 'react';
type Props = {
open: boolean;
onClose: () => any;
onConfirm: () => any;
entity: Entity;
};
export const DeleteEntityDialog = ({
open,
onClose,
onConfirm,
entity,
}: Props) => {
const [busy, setBusy] = useState(false);
const catalogApi = useApi(catalogApiRef);
const alertApi = useApi(alertApiRef);
const onDelete = async () => {
setBusy(true);
try {
const uid = entity.metadata.uid;
await catalogApi.removeEntityByUid(uid!);
onConfirm();
} catch (err) {
alertApi.post({ message: err.message });
} finally {
setBusy(false);
}
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle id="responsive-dialog-title">
Are you sure you want to delete this entity?
</DialogTitle>
<DialogActions>
<Button
variant="contained"
color="secondary"
disabled={busy}
onClick={onDelete}
>
Delete
</Button>
<Button onClick={onClose} color="primary">
Cancel
</Button>
</DialogActions>
</Dialog>
);
};
@@ -14,17 +14,23 @@
* limitations under the License.
*/
import { ApiProvider, ApiRegistry, ConfigReader } from '@backstage/core';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityOrphanWarning } from './EntityOrphanWarning';
describe('<EntityOrphanWarning />', () => {
const catalogClient: jest.Mocked<CatalogApi> = {
removeEntityByUid: jest.fn(),
} as any;
const apis = ApiRegistry.with(catalogApiRef, catalogClient);
it('renders EntityOrphanWarning if the entity is orphan', async () => {
const entity = {
apiVersion: 'v1',
@@ -41,14 +47,6 @@ describe('<EntityOrphanWarning />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
),
);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
@@ -59,7 +57,7 @@ describe('<EntityOrphanWarning />', () => {
);
expect(
getByText(
'This entity is not referenced by any location and is therefore not receiving updates. Click here to unregister.',
'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.',
),
).toBeInTheDocument();
});
@@ -79,14 +77,6 @@ describe('<EntityOrphanWarning />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
),
);
const { queryByText } = await renderInTestApp(
<ApiProvider apis={apis}>
@@ -97,7 +87,7 @@ describe('<EntityOrphanWarning />', () => {
);
expect(
queryByText(
'This entity is not referenced by any location and is therefore not receiving updates. Click here to unregister.',
'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.',
),
).not.toBeInTheDocument();
});
@@ -18,7 +18,7 @@ import { useEntity } from '@backstage/plugin-catalog-react';
import { Alert } from '@material-ui/lab';
import React, { useState } from 'react';
import { useNavigate } from 'react-router';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
import { DeleteEntityDialog } from './DeleteEntityDialog';
/**
* Displays a warning alert if the entity is marked as orphan with the ability to delete said entity.
@@ -28,10 +28,7 @@ export const EntityOrphanWarning = () => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const { entity } = useEntity();
if (
!entity.metadata?.annotations ||
entity.metadata?.annotations['backstage.io/orphan'] !== 'true'
) {
if (entity.metadata?.annotations?.['backstage.io/orphan'] !== 'true') {
return null;
}
@@ -44,9 +41,9 @@ export const EntityOrphanWarning = () => {
<>
<Alert severity="warning" onClick={() => setConfirmationDialogOpen(true)}>
This entity is not referenced by any location and is therefore not
receiving updates. Click here to unregister.
receiving updates. Click here to delete.
</Alert>
<UnregisterEntityDialog
<DeleteEntityDialog
open={confirmationDialogOpen}
entity={entity!}
onConfirm={cleanUpAfterRemoval}