diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 150f5362f3..14628a0323 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -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:^", diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 4f46d1d7f3..96b1a3ae59 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -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( @@ -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 = + jest.fn(); + const getEntities: jest.MockedFunction = jest.fn(); + const removeEntityByUid: jest.MockedFunction< + CatalogApi['removeEntityByUid'] + > = jest.fn(); + const getEntityFacets: jest.MockedFunction = + 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( + + + + +
tabbed-test-content
+
+
+
+
, + { + 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( + + + + +
tabbed-test-content
+
+
+
+
, + { + 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'); + }); + }); +}); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 7df02abe4e..8e11f92599 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -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 diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index d310639fb1..f41fefe404 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -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, }, }); diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index ab70a3c551..1926576f70 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -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', });