From d720a3ecf896ac2d610b2a60e2d3a10f1d9e1875 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 18 May 2021 16:38:55 +0200 Subject: [PATCH] Catalog: Add dialog to delete entity Signed-off-by: Johan Haals --- .../DeleteEntityDialog.test.tsx | 132 ++++++++++++++++++ .../DeleteEntityDialog.tsx | 73 ++++++++++ .../EntityOrphanWarning.test.tsx | 36 ++--- .../EntityOrphanWarning.tsx | 11 +- 4 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx create mode 100644 plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx new file mode 100644 index 0000000000..d9f266cce7 --- /dev/null +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx @@ -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 = { + post: jest.fn(), + alert$: jest.fn(), + }; + + const catalogClient: jest.Mocked = { + 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 }) => ( + {children} + ); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('can cancel', async () => { + const onClose = jest.fn(); + + await renderInTestApp( + + {}} + entity={entity} + /> + , + ); + + userEvent.click(screen.getByText('Cancel')); + + await waitFor(() => { + expect(onClose).toBeCalled(); + }); + }); + + it('can delete', async () => { + const onConfirm = jest.fn(); + + await renderInTestApp( + + {}} + onConfirm={onConfirm} + entity={entity} + /> + , + ); + + 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( + + {}} + onConfirm={onConfirm} + entity={entity} + /> + , + ); + + 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' }); + }); + }); +}); diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx new file mode 100644 index 0000000000..26b6b29650 --- /dev/null +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -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 ( + + + Are you sure you want to delete this entity? + + + + + + + ); +}; diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index 5681a0e289..482be1dd25 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -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('', () => { + const catalogClient: jest.Mocked = { + 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('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: {}, - }), - ), - ); const { getByText } = await renderInTestApp( @@ -59,7 +57,7 @@ describe('', () => { ); 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('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: {}, - }), - ), - ); const { queryByText } = await renderInTestApp( @@ -97,7 +87,7 @@ describe('', () => { ); 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(); }); diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index 1215daeed1..6e4780f1a0 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -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 = () => { <> 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. -