Merge pull request #20786 from NeuralEvolution/bug/18436-entity-unregister-redirect

Bug/18436 entity unregister redirect
This commit is contained in:
Ben Lambert
2023-11-20 14:45:47 +01:00
committed by GitHub
8 changed files with 234 additions and 19 deletions
+6
View File
@@ -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.
@@ -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', () => {
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog': rootRouteRef,
},
},
);
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
+1
View File
@@ -122,6 +122,7 @@ export const catalogPlugin: BackstagePlugin<
},
true
>;
unregisterRedirect: ExternalRouteRef<undefined, true>;
}
>;
@@ -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(
<ApiProvider apis={mockApis}>
@@ -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<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');
},
};
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>
<Routes>
<Route path="/catalog" element={<p>catalog-page</p>} />
<Route path="/testRoute" element={<p>external-page</p>} />
</Routes>
</TestApiProvider>,
{
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(
<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>
<Routes>
<Route path="/catalog" element={<p>catalog-page</p>} />
<Route path="/testRoute" element={<p>external-page</p>} />
</Routes>
</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(screen.getByText('catalog-page')).toBeInTheDocument();
});
});
});
@@ -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
+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: 'catalog:unregister-redirect',
optional: true,
});
export const rootRouteRef = createRouteRef({
id: 'catalog',
});
@@ -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<StarredEntitiesApi> = {
},
};
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,
},
},
);