diff --git a/.changeset/lucky-kids-cough.md b/.changeset/lucky-kids-cough.md
new file mode 100644
index 0000000000..5ab1856e35
--- /dev/null
+++ b/.changeset/lucky-kids-cough.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+- Fixes bug where after unregistering an entity you are redirected to `/`.
+- Adds an optional external route `unregisterRedirect` to override this behaviour to another route.
diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx
index 68ce5fb497..25181db2c1 100644
--- a/packages/app/src/components/catalog/EntityPage.test.tsx
+++ b/packages/app/src/components/catalog/EntityPage.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { EntityLayout } from '@backstage/plugin-catalog';
+import { EntityLayout, catalogPlugin } from '@backstage/plugin-catalog';
import {
EntityProvider,
starredEntitiesApiRef,
@@ -51,6 +51,7 @@ describe('EntityPage Test', () => {
listWorkflowRuns: jest.fn().mockResolvedValue([]),
};
const mockPermissionApi = new MockPermissionApi();
+ const rootRouteRef = catalogPlugin.routes.catalogIndex;
describe('cicdContent', () => {
it('Should render GitHub Actions View', async () => {
@@ -70,6 +71,11 @@ describe('EntityPage Test', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog': rootRouteRef,
+ },
+ },
);
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md
index 6647f8deaa..874d294cdc 100644
--- a/plugins/catalog/api-report.md
+++ b/plugins/catalog/api-report.md
@@ -122,6 +122,7 @@ export const catalogPlugin: BackstagePlugin<
},
true
>;
+ unregisterRedirect: ExternalRouteRef;
}
>;
diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx
index 3634405af8..c222c2d7b6 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,27 +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';
-
-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 { Route, Routes } from 'react-router-dom';
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(
@@ -65,6 +72,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -97,6 +105,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -120,6 +129,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -146,6 +156,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -178,6 +189,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -225,6 +237,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -253,6 +266,7 @@ describe('EntityLayout', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -265,3 +279,168 @@ 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');
+ },
+ };
+
+ it('redirects to externalRouteRef when unregisterRedirectRouteRef is bound', async () => {
+ await renderInTestApp(
+
+
+
+
+ tabbed-test-content
+
+
+
+
+ catalog-page
} />
+ external-page} />
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
+ '/testRoute': 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(screen.getByText('external-page')).toBeInTheDocument();
+ });
+ });
+
+ it('redirects to rootRouteRef when unregisterRedirectRouteRef is not bound', async () => {
+ await renderInTestApp(
+
+
+
+
+ tabbed-test-content
+
+
+
+
+ catalog-page} />
+ external-page} />
+
+ ,
+ {
+ 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(screen.getByText('catalog-page')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
index f9999f26e9..8e11f92599 100644
--- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
+++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
@@ -33,6 +33,7 @@ import {
attachComponentData,
IconComponent,
useElementFilter,
+ useRouteRef,
useRouteRefParams,
} from '@backstage/core-plugin-api';
import {
@@ -50,6 +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, unregisterRedirectRouteRef } from '../../routes';
/** @public */
export type EntityLayoutRouteProps = {
@@ -229,10 +231,15 @@ export const EntityLayout = (props: EntityLayoutProps) => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false);
const navigate = useNavigate();
+ const catalogRoute = useRouteRef(rootRouteRef);
+ const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef);
+
const cleanUpAfterRemoval = async () => {
setConfirmationDialogOpen(false);
setInspectionDialogOpen(false);
- navigate('/');
+ 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..dcc46c7988 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: 'catalog:unregister-redirect',
+ optional: true,
+});
+
export const rootRouteRef = createRouteRef({
id: 'catalog',
});
diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx
index 6613aa5b6a..cd5801ac71 100644
--- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx
+++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx
@@ -31,7 +31,7 @@ import {
mockedCatalogApiSupportingGroups,
} from '../../../../__testUtils__/catalogMocks';
import { permissionApiRef } from '@backstage/plugin-permission-react';
-import { EntityLayout } from '@backstage/plugin-catalog';
+import { EntityLayout, catalogPlugin } from '@backstage/plugin-catalog';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Observable } from '@backstage/types';
@@ -63,6 +63,8 @@ const mockedStarredEntitiesApi: Partial = {
},
};
+const rootRouteRef = catalogPlugin.routes.catalogIndex;
+
describe('MemberTab Test', () => {
const groupEntity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -122,6 +124,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -151,6 +154,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -179,6 +183,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -206,6 +211,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -231,6 +237,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -265,6 +272,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);
@@ -299,6 +307,7 @@ describe('MemberTab Test', () => {
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/catalog': rootRouteRef,
},
},
);