feat(EntityLayout): navigates to externalRouteRef after entity removal if bound

Signed-off-by: Matthew Prinold <matthewprinold@gmail.com>
This commit is contained in:
Matthew Prinold
2023-10-26 12:08:54 +01:00
parent cd910c4fa5
commit 934ddf3794
5 changed files with 204 additions and 20 deletions
+2 -1
View File
@@ -77,7 +77,8 @@
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
"react-router-dom": "6.0.0-beta.0 || ^6.3.0",
"react-router": "6.17.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
@@ -15,7 +15,11 @@
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
ANNOTATION_ORIGIN_LOCATION,
Entity,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { ApiProvider } from '@backstage/core-app-api';
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
import {
@@ -30,28 +34,30 @@ import { permissionApiRef } from '@backstage/plugin-permission-react';
import {
MockPermissionApi,
renderInTestApp,
TestApiProvider,
TestApiRegistry,
} from '@backstage/test-utils';
import { act, fireEvent, screen } from '@testing-library/react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { EntityLayout } from './EntityLayout';
import { rootRouteRef } from '../../routes';
const mockEntity = {
kind: 'MyKind',
metadata: {
name: 'my-entity',
},
} as Entity;
const mockApis = TestApiRegistry.from(
[catalogApiRef, {} as CatalogApi],
[alertApiRef, {} as AlertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
);
import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes';
import * as router from 'react-router';
describe('EntityLayout', () => {
const mockEntity = {
kind: 'MyKind',
metadata: {
name: 'my-entity',
},
} as Entity;
const mockApis = TestApiRegistry.from(
[catalogApiRef, {} as CatalogApi],
[alertApiRef, {} as AlertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
);
it('renders simplest case', async () => {
await renderInTestApp(
<ApiProvider apis={mockApis}>
@@ -272,3 +278,169 @@ describe('EntityLayout', () => {
expect(linkParent?.tagName).toBe('P');
});
});
describe('EntityLayout - CleanUpAfterRemoval', () => {
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'n',
namespace: 'ns',
annotations: {
[ANNOTATION_ORIGIN_LOCATION]: 'url:http://example.com',
},
},
spec: {
owner: 'tools',
type: 'service',
},
relations: [
{
type: RELATION_OWNED_BY,
targetRef: 'group:default/tools',
},
],
};
const getLocationByRef: jest.MockedFunction<CatalogApi['getLocationByRef']> =
jest.fn();
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
const removeEntityByUid: jest.MockedFunction<
CatalogApi['removeEntityByUid']
> = jest.fn();
const getEntityFacets: jest.MockedFunction<CatalogApi['getEntityFacets']> =
jest.fn();
getLocationByRef.mockResolvedValue(undefined);
getEntities.mockResolvedValue({ items: [{ ...entity }] });
getEntityFacets.mockResolvedValue({
facets: {
'relations.ownedBy': [{ count: 1, value: 'group:default/tools' }],
},
});
const alertApi: AlertApi = {
post() {
return undefined;
},
alert$() {
throw new Error('not implemented');
},
};
const navigate = jest.fn();
beforeEach(() => {
jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate);
});
afterEach(() => {
jest.clearAllMocks();
});
it('redirects to externalRouteRef when unregisterRedirectRouteRef is bound', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[
catalogApiRef,
{
getLocationByRef,
getEntities,
removeEntityByUid,
getEntityFacets,
},
],
[alertApiRef, alertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
]}
>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/catalog': rootRouteRef,
'/testExternalRouteRef': unregisterRedirectRouteRef,
},
},
);
const menuButton = screen.queryAllByTestId('menu-button')[0];
fireEvent.click(menuButton);
const listItemUnregister = screen.queryAllByRole('menuitem', {
name: /Unregister entity/i,
})[0];
fireEvent.click(listItemUnregister);
await waitFor(() => {
const deleteEntityButton = screen.getByRole('button', {
name: /Delete Entity/i,
});
act(() => {
fireEvent.click(deleteEntityButton);
});
});
await waitFor(() => {
expect(navigate).toHaveBeenCalledWith('/testExternalRouteRef');
});
});
it('redirects to rootRouteRef when unregisterRedirectRouteRef is not bound', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[
catalogApiRef,
{
getLocationByRef,
getEntities,
removeEntityByUid,
getEntityFacets,
},
],
[alertApiRef, alertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
]}
>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/catalog': rootRouteRef,
},
},
);
const menuButton = screen.queryAllByTestId('menu-button')[0];
fireEvent.click(menuButton);
const listItemUnregister = screen.queryAllByRole('menuitem', {
name: /Unregister entity/i,
})[0];
fireEvent.click(listItemUnregister);
await waitFor(() => {
const deleteEntityButton = screen.getByRole('button', {
name: /Delete Entity/i,
});
act(() => {
fireEvent.click(deleteEntityButton);
});
});
await waitFor(() => {
expect(navigate).toHaveBeenCalledWith('/catalog');
});
});
});
@@ -51,7 +51,7 @@ import { Alert } from '@material-ui/lab';
import React, { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { rootRouteRef } from '../../routes';
import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes';
/** @public */
export type EntityLayoutRouteProps = {
@@ -232,10 +232,14 @@ export const EntityLayout = (props: EntityLayoutProps) => {
const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false);
const navigate = useNavigate();
const catalogRoute = useRouteRef(rootRouteRef);
const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef);
const cleanUpAfterRemoval = async () => {
setConfirmationDialogOpen(false);
setInspectionDialogOpen(false);
navigate(catalogRoute());
navigate(
unregisterRedirectRoute ? unregisterRedirectRoute() : catalogRoute(),
);
};
// Make sure to close the dialog if the user clicks links in it that navigate
+2
View File
@@ -25,6 +25,7 @@ import {
import {
createComponentRouteRef,
createFromTemplateRouteRef,
unregisterRedirectRouteRef,
viewTechDocRouteRef,
} from './routes';
import {
@@ -89,6 +90,7 @@ export const catalogPlugin = createPlugin({
createComponent: createComponentRouteRef,
viewTechDoc: viewTechDocRouteRef,
createFromTemplate: createFromTemplateRouteRef,
unregisterRedirect: unregisterRedirectRouteRef,
},
});
+5
View File
@@ -36,6 +36,11 @@ export const createFromTemplateRouteRef = createExternalRouteRef({
params: ['namespace', 'templateName'],
});
export const unregisterRedirectRouteRef = createExternalRouteRef({
id: 'unregister-redirect',
optional: true,
});
export const rootRouteRef = createRouteRef({
id: 'catalog',
});