Merge pull request #5707 from backstage/jhaals/orphan-warning

catalog: Display warning when entity is orphan
This commit is contained in:
Johan Haals
2021-05-20 12:16:28 +02:00
committed by GitHub
8 changed files with 367 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Display warning when Entity has orphan annotation.
@@ -40,6 +40,8 @@ import {
isComponentType,
isKind,
EntityHasResourcesCard,
EntityOrphanWarning,
isOrphan,
} from '@backstage/plugin-catalog';
import {
EntityCircleCIContent,
@@ -214,7 +216,15 @@ const errorsContent = (
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
<EntityOrphanWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={8} xs={12}>
<EntityAboutCard variant="gridItem" />
</Grid>
@@ -226,7 +236,7 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={4} sm={6}>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
@@ -260,7 +270,7 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6}>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
@@ -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>
);
};
@@ -0,0 +1,70 @@
/*
* Copyright 2020 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 { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
catalogApiRef,
catalogRouteRef,
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',
kind: 'Component',
metadata: {
name: 'software',
description: 'This is the description',
annotations: { 'backstage.io/orphan': 'true' },
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<EntityOrphanWarning />
</EntityProvider>
</ApiProvider>,
{
mountedRoutes: {
'/create': catalogRouteRef,
},
},
);
expect(
getByText(
'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.',
),
).toBeInTheDocument();
});
});
@@ -0,0 +1,56 @@
/*
* 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 { useRouteRef } from '@backstage/core';
import { catalogRouteRef, useEntity } from '@backstage/plugin-catalog-react';
import { Alert } from '@material-ui/lab';
import React, { useState } from 'react';
import { useNavigate } from 'react-router';
import { DeleteEntityDialog } from './DeleteEntityDialog';
export const isOrphan = (entity: Entity) =>
entity?.metadata?.annotations?.['backstage.io/orphan'] === 'true';
/**
* Displays a warning alert if the entity is marked as orphan with the ability to delete said entity.
*/
export const EntityOrphanWarning = () => {
const navigate = useNavigate();
const catalogLink = useRouteRef(catalogRouteRef);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const { entity } = useEntity();
const cleanUpAfterRemoval = async () => {
setConfirmationDialogOpen(false);
navigate(catalogLink());
};
return (
<>
<Alert severity="warning" onClick={() => setConfirmationDialogOpen(true)}>
This entity is not referenced by any location and is therefore not
receiving updates. Click here to delete.
</Alert>
<DeleteEntityDialog
open={confirmationDialogOpen}
entity={entity!}
onConfirm={cleanUpAfterRemoval}
onClose={() => setConfirmationDialogOpen(false)}
/>
</>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { EntityOrphanWarning, isOrphan } from './EntityOrphanWarning';
+1
View File
@@ -35,3 +35,4 @@ export {
EntityLinksCard,
EntitySystemDiagramCard,
} from './plugin';
export * from './components/EntityOrphanWarning';