diff --git a/.changeset/large-eggs-help.md b/.changeset/large-eggs-help.md
new file mode 100644
index 0000000000..e310ad7645
--- /dev/null
+++ b/.changeset/large-eggs-help.md
@@ -0,0 +1,19 @@
+---
+'@backstage/plugin-explore': minor
+---
+
+Introduce external route for linking to the entity page from the explore plugin.
+
+To use the explore plugin you have to bind the external route in your app:
+
+```typescript
+const app = createApp({
+ ...
+ bindRoutes({ bind }) {
+ ...
+ bind(explorePlugin.externalRoutes, {
+ catalogEntity: catalogPlugin.routes.catalogEntity,
+ });
+ },
+});
+```
diff --git a/.changeset/soft-months-invite.md b/.changeset/soft-months-invite.md
new file mode 100644
index 0000000000..a6a20973e9
--- /dev/null
+++ b/.changeset/soft-months-invite.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Introduce parameters for namespace, kind, and name to `entityRouteRef`.
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 5b6f7b95fe..7acde03953 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -33,7 +33,7 @@ import {
CostInsightsPage,
CostInsightsProjectGrowthInstructionsPage,
} from '@backstage/plugin-cost-insights';
-import { ExplorePage } from '@backstage/plugin-explore';
+import { ExplorePage, explorePlugin } from '@backstage/plugin-explore';
import { GcpProjectsPage } from '@backstage/plugin-gcp-projects';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
import { LighthousePage } from '@backstage/plugin-lighthouse';
@@ -79,6 +79,9 @@ const app = createApp({
bind(apiDocsPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
});
+ bind(explorePlugin.externalRoutes, {
+ catalogEntity: catalogPlugin.routes.catalogEntity,
+ });
},
});
diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx
index 5ceb123b06..b9fdf05444 100644
--- a/packages/core-api/src/app/App.test.tsx
+++ b/packages/core-api/src/app/App.test.tsx
@@ -50,23 +50,30 @@ describe('generateBoundRoutes', () => {
describe('Integration Test', () => {
const plugin1RouteRef = createRouteRef({ path: '/blah1', title: '' });
- const plugin2RouteRef = createRouteRef({ path: '/blah2', title: '' });
- const externalRouteRef = createExternalRouteRef({ id: '3' });
- const optionalBarExternalRouteRef = createExternalRouteRef({
- id: 'bar',
+ const plugin2RouteRef = createRouteRef({
+ path: '/blah2',
+ title: '',
+ params: ['x'],
+ });
+ const err = createExternalRouteRef({ id: 'err' });
+ const errParams = createExternalRouteRef({ id: 'errParams', params: ['x'] });
+ const errOptional = createExternalRouteRef({
+ id: 'errOptional',
optional: true,
});
- const optionalBazExternalRouteRef = createExternalRouteRef({
- id: 'baz',
+ const errParamsOptional = createExternalRouteRef({
+ id: 'errParamsOptional',
optional: true,
+ params: ['x'],
});
const plugin1 = createPlugin({
id: 'blob',
externalRoutes: {
- foo: externalRouteRef,
- bar: optionalBarExternalRouteRef,
- baz: optionalBazExternalRouteRef,
+ err,
+ errParams,
+ errOptional,
+ errParamsOptional,
},
});
@@ -85,14 +92,19 @@ describe('Integration Test', () => {
createRoutableExtension({
component: () =>
Promise.resolve((_: PropsWithChildren<{ path?: string }>) => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const externalLink = useRouteRef(externalRouteRef);
- const barLink = useRouteRef(optionalBarExternalRouteRef);
- const bazLink = useRouteRef(optionalBazExternalRouteRef);
+ const errLink = useRouteRef(err);
+ const errParamsLink = useRouteRef(errParams);
+ const errOptionalLink = useRouteRef(errOptional);
+ const errParamsOptionalLink = useRouteRef(errParamsOptional);
return (
- Our routes are: {externalLink()}, bar: {barLink?.() ?? 'none'},
- baz: {bazLink?.() ?? 'none'}
+ err: {errLink()}
+ errParams: {errParamsLink({ x: 'a' })}
+ errOptional: {errOptionalLink?.() ?? ''}
+
+ errParamsOptional:{' '}
+ {errParamsOptionalLink?.({ x: 'b' }) ?? ''}
+
);
}),
@@ -100,14 +112,14 @@ describe('Integration Test', () => {
}),
);
- it('runs happy paths', async () => {
- const components = {
- NotFoundErrorPage: () => null,
- BootErrorPage: () => null,
- Progress: () => null,
- Router: BrowserRouter,
- };
+ const components = {
+ NotFoundErrorPage: () => null,
+ BootErrorPage: () => null,
+ Progress: () => null,
+ Router: BrowserRouter,
+ };
+ it('runs happy paths', async () => {
const app = new PrivateAppImpl({
apis: [],
defaultApis: [],
@@ -124,8 +136,10 @@ describe('Integration Test', () => {
components,
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
- foo: plugin2RouteRef,
- bar: plugin2RouteRef,
+ err: plugin1RouteRef,
+ errParams: plugin2RouteRef,
+ errOptional: plugin1RouteRef,
+ errParamsOptional: plugin2RouteRef,
});
},
});
@@ -138,25 +152,19 @@ describe('Integration Test', () => {
-
+
,
);
- expect(
- screen.getByText('Our routes are: /foo/bar, bar: /foo/bar, baz: none'),
- ).toBeInTheDocument();
+ expect(screen.getByText('err: /')).toBeInTheDocument();
+ expect(screen.getByText('errParams: /foo')).toBeInTheDocument();
+ expect(screen.getByText('errOptional: /')).toBeInTheDocument();
+ expect(screen.getByText('errParamsOptional: /foo')).toBeInTheDocument();
});
- it('should throw some error when the route has duplicate params', () => {
- const components = {
- NotFoundErrorPage: () => null,
- BootErrorPage: () => null,
- Progress: () => null,
- Router: BrowserRouter,
- };
-
+ it('runs happy paths without optional routes', async () => {
const app = new PrivateAppImpl({
apis: [],
defaultApis: [],
@@ -172,7 +180,53 @@ describe('Integration Test', () => {
plugins: [],
components,
bindRoutes: ({ bind }) => {
- bind(plugin1.externalRoutes, { foo: plugin2RouteRef });
+ bind(plugin1.externalRoutes, {
+ err: plugin1RouteRef,
+ errParams: plugin2RouteRef,
+ });
+ },
+ });
+
+ const Provider = app.getProvider();
+ const Router = app.getRouter();
+
+ await renderWithEffects(
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText('err: /')).toBeInTheDocument();
+ expect(screen.getByText('errParams: /foo')).toBeInTheDocument();
+ expect(screen.getByText('errOptional: ')).toBeInTheDocument();
+ expect(screen.getByText('errParamsOptional: ')).toBeInTheDocument();
+ });
+
+ it('should throw some error when the route has duplicate params', () => {
+ const app = new PrivateAppImpl({
+ apis: [],
+ defaultApis: [],
+ themes: [
+ {
+ id: 'light',
+ title: 'Light Theme',
+ variant: 'light',
+ theme: lightTheme,
+ },
+ ],
+ icons: defaultSystemIcons,
+ plugins: [],
+ components,
+ bindRoutes: ({ bind }) => {
+ bind(plugin1.externalRoutes, {
+ err: plugin1RouteRef,
+ errParams: plugin2RouteRef,
+ });
},
});
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 9ab19b5588..3c58930709 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -50,14 +50,14 @@ import {
} from '../extensions/traversal';
import { IconComponent, IconComponentMap, IconKey } from '../icons';
import { BackstagePlugin } from '../plugin';
-import { RouteRef } from '../routing';
+import { AnyRoutes } from '../plugin/types';
+import { RouteRef, ExternalRouteRef } from '../routing';
import {
routeObjectCollector,
routeParentCollector,
routePathCollector,
} from '../routing/collectors';
import { RoutingProvider, validateRoutes } from '../routing/hooks';
-import { ExternalRouteRef } from '../routing/RouteRef';
import { AppContextProvider } from './AppContext';
import { AppIdentity } from './AppIdentity';
import { AppThemeProvider } from './AppThemeProvider';
@@ -78,7 +78,7 @@ export function generateBoundRoutes(
const result = new Map();
if (bindRoutes) {
- const bind: AppRouteBinder = (externalRoutes, targetRoutes) => {
+ const bind: AppRouteBinder = (externalRoutes, targetRoutes: AnyRoutes) => {
for (const [key, value] of Object.entries(targetRoutes)) {
const externalRoute = externalRoutes[key];
if (!externalRoute) {
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
index 701862601d..df4180c6e4 100644
--- a/packages/core-api/src/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -16,7 +16,7 @@
import { ComponentType } from 'react';
import { IconComponent, IconComponentMap, IconKey } from '../icons';
-import { BackstagePlugin } from '../plugin/types';
+import { AnyExternalRoutes, BackstagePlugin } from '../plugin/types';
import { ExternalRouteRef, RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
import { AppTheme, ProfileInfo } from '../apis/definitions';
@@ -79,49 +79,39 @@ export type AppComponents = {
*/
export type AppConfigLoader = () => Promise;
-/**
- * Extracts the Optional type of a map of ExternalRouteRefs, leaving only the boolean in place as value
- */
-type ExternalRouteRefsToOptionalMap<
- T extends { [name in string]: ExternalRouteRef }
-> = {
- [name in keyof T]: T[name] extends ExternalRouteRef ? U : never;
-};
-
/**
* Extracts a union of the keys in a map whose value extends the given type
*/
-type ExtractKeysWithType = {
+type KeysWithType = {
[key in keyof Obj]: Obj[key] extends Type ? key : never;
}[keyof Obj];
/**
- * Given a map of boolean values denoting whether a route is optional, create a
- * map of needed RouteRefs.
- *
- * For example { foo: false, bar: true } gives { foo: RouteRef, bar?: RouteRef }
+ * Takes a map Map required values and makes all keys matching Keys optional
*/
-type CombineOptionalAndRequiredRoutes<
- OptionalMap extends { [key in string]: boolean }
-> = {
- [name in ExtractKeysWithType]: RouteRef;
-} &
- { [name in keyof OptionalMap]?: RouteRef };
+type PartialKeys<
+ Map extends { [name in string]: any },
+ Keys extends keyof Map
+> = Partial> & Required>;
/**
- * Creates a map of required target routes based on whether the input external
- * routes are optional or not. The external routes that are marked as optional
- * will also be optional in the target routes map.
+ * Creates a map of target routes with matching parameters based on a map of external routes.
*/
-type TargetRoutesMap<
- T extends { [name in string]: ExternalRouteRef }
-> = CombineOptionalAndRequiredRoutes>;
+type TargetRouteMap = {
+ [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
+ infer Params,
+ any
+ >
+ ? RouteRef
+ : never;
+};
-export type AppRouteBinder = <
- ExternalRoutes extends { [name in string]: ExternalRouteRef }
->(
+export type AppRouteBinder = (
externalRoutes: ExternalRoutes,
- targetRoutes: TargetRoutesMap,
+ targetRoutes: PartialKeys<
+ TargetRouteMap,
+ KeysWithType>
+ >,
) => void;
export type AppOptions = {
diff --git a/packages/core-api/src/plugin/types.ts b/packages/core-api/src/plugin/types.ts
index 711246f1f4..c3b64d8cb6 100644
--- a/packages/core-api/src/plugin/types.ts
+++ b/packages/core-api/src/plugin/types.ts
@@ -15,9 +15,8 @@
*/
import { ComponentType } from 'react';
-import { RouteRef } from '../routing';
+import { RouteRef, ExternalRouteRef } from '../routing';
import { AnyApiFactory } from '../apis/system';
-import { ExternalRouteRef } from '../routing/RouteRef';
export type RouteOptions = {
// Whether the route path must match exactly, defaults to true.
diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts
index 67ff9bc1fb..eda16ccc91 100644
--- a/packages/core-api/src/routing/RouteRef.ts
+++ b/packages/core-api/src/routing/RouteRef.ts
@@ -14,19 +14,40 @@
* limitations under the License.
*/
-import { RouteRef } from './types';
+import {
+ RouteRef,
+ ExternalRouteRef,
+ routeRefType,
+ AnyParams,
+ ParamKeys,
+} from './types';
import { IconComponent } from '../icons';
-export type RouteRefConfig = {
- params?: Array;
- /** @deprecated Route refs no longer decide their own path */
+// TODO(Rugvip): Remove this once we get rid of the deprecated fields, it's not exported
+export type RouteRefConfig = {
+ params?: ParamKeys;
path?: string;
icon?: IconComponent;
title: string;
};
-export class AbsoluteRouteRef {
- constructor(private readonly config: RouteRefConfig) {}
+class RouteRefBase {
+ constructor(type: string, id: string) {
+ this.toString = () => `routeRef{type=${type},id=${id}}`;
+ }
+}
+
+export class RouteRefImpl extends RouteRefBase
+ implements RouteRef {
+ readonly [routeRefType] = 'absolute';
+
+ constructor(private readonly config: RouteRefConfig) {
+ super('absolute', config.title);
+ }
+
+ get params(): ParamKeys {
+ return this.config.params as any;
+ }
get icon() {
return this.config.icon;
@@ -40,12 +61,12 @@ export class AbsoluteRouteRef {
get title() {
return this.config.title;
}
-
- toString() {
- return `routeRef{title=${this.title}}`;
- }
}
+type OptionalParams<
+ Params extends { [param in string]: string }
+> = Params[keyof Params] extends never ? undefined : Params;
+
export function createRouteRef<
// Params is the type that we care about and the one to be embedded in the route ref.
// For example, given the params ['name', 'kind'], Params will be {name: string, kind: string}
@@ -58,27 +79,47 @@ export function createRouteRef<
params?: ParamKey[];
/** @deprecated Route refs no longer decide their own path */
path?: string;
+ /** @deprecated Route refs no longer decide their own icon */
icon?: IconComponent;
+ /** @deprecated Route refs no longer decide their own title */
title: string;
-}): RouteRef {
- return new AbsoluteRouteRef(config);
+}): RouteRef> {
+ return new RouteRefImpl>({
+ ...config,
+ params: (config.params ?? []) as ParamKeys>,
+ });
}
-export class ExternalRouteRef {
- readonly optional: boolean;
+export class ExternalRouteRefImpl<
+ Params extends AnyParams,
+ Optional extends boolean
+> extends RouteRefBase implements ExternalRouteRef {
+ readonly [routeRefType] = 'external';
- private constructor({ id, optional }: ExternalRouteRefOptions) {
- this.toString = () => `externalRouteRef{${id}}`;
- this.optional = Boolean(optional);
+ constructor(
+ id: string,
+ readonly params: ParamKeys,
+ readonly optional: Optional,
+ ) {
+ super('external', id);
}
}
-export type ExternalRouteRefOptions = {
+export function createExternalRouteRef<
+ Params extends { [param in ParamKey]: string },
+ Optional extends boolean = false,
+ ParamKey extends string = never
+>(options: {
/**
* An identifier for this route, used to identify it in error messages
*/
id: string;
+ /**
+ * The parameters that will be provided to the external route reference.
+ */
+ params?: ParamKey[];
+
/**
* Whether or not this route is optional, defaults to false.
*
@@ -86,14 +127,10 @@ export type ExternalRouteRefOptions = {
* if they aren't, `useRouteRef` will return `undefined`.
*/
optional?: Optional;
-};
-
-export function createExternalRouteRef(
- options: ExternalRouteRefOptions,
-): ExternalRouteRef {
- return new ((ExternalRouteRef as unknown) as {
- new (options: ExternalRouteRefOptions): ExternalRouteRef<
- Optional
- >;
- })(options);
+}): ExternalRouteRef, Optional> {
+ return new ExternalRouteRefImpl, Optional>(
+ options.id,
+ (options.params ?? []) as ParamKeys>,
+ Boolean(options.optional) as Optional,
+ );
}
diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx
index 94cd3a704d..0ada37b18d 100644
--- a/packages/core-api/src/routing/hooks.test.tsx
+++ b/packages/core-api/src/routing/hooks.test.tsx
@@ -38,10 +38,9 @@ import {
import {
createRouteRef,
createExternalRouteRef,
- ExternalRouteRef,
RouteRefConfig,
} from './RouteRef';
-import { AnyRouteRef, RouteRef } from './types';
+import { AnyRouteRef, RouteRef, ExternalRouteRef } from './types';
const mockConfig = (extra?: Partial>) => ({
path: '/unused',
@@ -58,12 +57,19 @@ const ref1 = createRouteRef(mockConfig({ path: '/wat1' }));
const ref2 = createRouteRef(mockConfig({ path: '/wat2' }));
const ref3 = createRouteRef(mockConfig({ path: '/wat3' }));
const ref4 = createRouteRef(mockConfig({ path: '/wat4' }));
-const ref5 = createRouteRef(mockConfig({ path: '/wat5' }));
+const ref5 = createRouteRef({
+ ...mockConfig({ path: '/wat5' }),
+ params: ['x'],
+});
const eRefA = createExternalRouteRef({ id: '1' });
const eRefB = createExternalRouteRef({ id: '2' });
-const eRefC = createExternalRouteRef({ id: '3' });
+const eRefC = createExternalRouteRef({ id: '3', params: ['y'] });
const eRefD = createExternalRouteRef({ id: '4', optional: true });
-const eRefE = createExternalRouteRef({ id: '5', optional: true });
+const eRefE = createExternalRouteRef({
+ id: '5',
+ optional: true,
+ params: ['z'],
+});
const MockRouteSource = (props: {
path?: string;
diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx
index 54490fb4f2..4d419485f6 100644
--- a/packages/core-api/src/routing/hooks.tsx
+++ b/packages/core-api/src/routing/hooks.tsx
@@ -15,19 +15,22 @@
*/
import React, { createContext, ReactNode, useContext, useMemo } from 'react';
-import { AnyRouteRef, BackstageRouteObject, RouteRef } from './types';
+import {
+ AnyRouteRef,
+ BackstageRouteObject,
+ RouteRef,
+ ExternalRouteRef,
+ AnyParams,
+} from './types';
import { generatePath, matchRoutes, useLocation } from 'react-router-dom';
-import { ExternalRouteRef } from './RouteRef';
// The extra TS magic here is to require a single params argument if the RouteRef
// had at least one param defined, but require 0 arguments if there are no params defined.
// Without this we'd have to pass in empty object to all parameter-less RouteRefs
// just to make TypeScript happy, or we would have to make the argument optional in
// which case you might forget to pass it in when it is actually required.
-export type RouteFunc = (
- ...[params]: Params[keyof Params] extends never
- ? readonly []
- : readonly [Params]
+export type RouteFunc = (
+ ...[params]: Params extends undefined ? readonly [] : readonly [Params]
) => string;
class RouteResolver {
@@ -38,8 +41,8 @@ class RouteResolver {
private readonly routeBindings: Map,
) {}
- resolve(
- routeRefOrExternalRouteRef: RouteRef | ExternalRouteRef,
+ resolve(
+ routeRefOrExternalRouteRef: RouteRef | ExternalRouteRef,
sourceLocation: ReturnType,
): RouteFunc | undefined {
const routeRef =
@@ -112,14 +115,14 @@ class RouteResolver {
const RoutingContext = createContext(undefined);
-export function useRouteRef(
- routeRef: ExternalRouteRef,
-): Optional extends true ? RouteFunc<{}> | undefined : RouteFunc<{}>;
-export function useRouteRef(
+export function useRouteRef(
+ routeRef: ExternalRouteRef,
+): Optional extends true ? RouteFunc | undefined : RouteFunc;
+export function useRouteRef(
routeRef: RouteRef,
): RouteFunc;
-export function useRouteRef(
- routeRef: RouteRef | ExternalRouteRef,
+export function useRouteRef(
+ routeRef: RouteRef | ExternalRouteRef,
): RouteFunc | undefined {
const sourceLocation = useLocation();
const resolver = useContext(RoutingContext);
diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts
index 100158c3c5..2a0685f583 100644
--- a/packages/core-api/src/routing/index.ts
+++ b/packages/core-api/src/routing/index.ts
@@ -19,12 +19,9 @@ export type {
AbsoluteRouteRef,
ConcreteRoute,
MutableRouteRef,
+ ExternalRouteRef,
} from './types';
export { FlatRoutes } from './FlatRoutes';
-export {
- createRouteRef,
- createExternalRouteRef,
- ExternalRouteRef,
-} from './RouteRef';
+export { createRouteRef, createExternalRouteRef } from './RouteRef';
export type { RouteRefConfig } from './RouteRef';
export { useRouteRef } from './hooks';
diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts
index b21fa38052..69de5345a5 100644
--- a/packages/core-api/src/routing/types.ts
+++ b/packages/core-api/src/routing/types.ts
@@ -15,35 +15,51 @@
*/
import { IconComponent } from '../icons';
-import { ExternalRouteRef } from './RouteRef';
+import { getGlobalSingleton } from '../lib/globalObject';
-// @ts-ignore, we're just embedding the Params type for usage in other places
-export type RouteRef = {
- // TODO(Rugvip): Remove path, look up via registry instead
+export type AnyParams = { [param in string]: string } | undefined;
+export type ParamKeys = keyof Params extends never
+ ? []
+ : (keyof Params)[];
+
+export const routeRefType: unique symbol = getGlobalSingleton(
+ 'route-ref-type',
+ () => Symbol('route-ref-type'),
+);
+
+export type RouteRef = {
+ readonly [routeRefType]: 'absolute';
+
+ params: ParamKeys;
+
+ // TODO(Rugvip): Remove all of these once plugins don't rely on the path
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
path: string;
+ /** @deprecated icons are no longer accessed via RouteRefs */
icon?: IconComponent;
- title: string;
+ /** @deprecated titles are no longer accessed via RouteRefs */
+ title?: string;
};
-export type AnyRouteRef = RouteRef | ExternalRouteRef;
+export type ExternalRouteRef<
+ Params extends AnyParams = any,
+ Optional extends boolean = any
+> = {
+ readonly [routeRefType]: 'external';
-/**
- * This type should not be used
- * @deprecated
- */
+ params: ParamKeys;
+
+ optional?: Optional;
+};
+
+export type AnyRouteRef = RouteRef | ExternalRouteRef;
+
+// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release
+/** @deprecated */
export type ConcreteRoute = {};
-
-/**
- * This type should not be used, use RouteRef instead
- * @deprecated
- */
+/** @deprecated */
export type AbsoluteRouteRef = RouteRef<{}>;
-
-/**
- * This type should not be used, use RouteRef instead
- * @deprecated
- */
+/** @deprecated */
export type MutableRouteRef = RouteRef<{}>;
// A duplicate of the react-router RouteObject, but with routeRef added
diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
index 9d409051f6..56a02a3642 100644
--- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
+++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
@@ -15,13 +15,13 @@
*/
import { DomainEntity } from '@backstage/catalog-model';
-import { render } from '@testing-library/react';
+import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
+import { catalogEntityRouteRef } from '../../routes';
import { DomainCard } from './DomainCard';
describe('', () => {
- it('renders a domain card', () => {
+ it('renders a domain card', async () => {
const entity: DomainEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
@@ -34,9 +34,14 @@ describe('', () => {
owner: 'guest',
},
};
- const { getByText } = render(, {
- wrapper: MemoryRouter,
- });
+ const { getByText } = await renderInTestApp(
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
+ },
+ },
+ );
expect(getByText('artists')).toBeInTheDocument();
expect(getByText('Everything about artists')).toBeInTheDocument();
diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx
index e5efc543b2..67bee13696 100644
--- a/plugins/explore/src/components/DomainCard/DomainCard.tsx
+++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx
@@ -14,15 +14,14 @@
* limitations under the License.
*/
import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model';
-import { ItemCard } from '@backstage/core';
+import { ItemCard, useRouteRef } from '@backstage/core';
import {
EntityRefLinks,
- entityRoute,
entityRouteParams,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import React from 'react';
-import { generatePath } from 'react-router-dom';
+import { catalogEntityRouteRef } from '../../routes';
type DomainCardProps = {
entity: DomainEntity;
@@ -30,6 +29,7 @@ type DomainCardProps = {
export const DomainCard = ({ entity }: DomainCardProps) => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
+ const catalogEntityRoute = useRouteRef(catalogEntityRouteRef);
return (
{
/>
}
label="Explore"
- // TODO: Use useRouteRef here to generate the path
- href={generatePath(
- `/catalog/${entityRoute.path}`,
- entityRouteParams(entity),
- )}
+ href={catalogEntityRoute(entityRouteParams(entity))}
/>
);
};
diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx
index bad3ca581b..ee06a50427 100644
--- a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx
+++ b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx
@@ -15,13 +15,13 @@
*/
import { DomainEntity } from '@backstage/catalog-model';
-import { render } from '@testing-library/react';
+import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
+import { catalogEntityRouteRef } from '../../routes';
import { DomainCardGrid } from './DomainCardGrid';
describe('', () => {
- it('renders a grid of domain cards', () => {
+ it('renders a grid of domain cards', async () => {
const entities: DomainEntity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
@@ -44,9 +44,14 @@ describe('', () => {
},
},
];
- const { getByText } = render(, {
- wrapper: MemoryRouter,
- });
+ const { getByText } = await renderInTestApp(
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
+ },
+ },
+ );
expect(getByText('artists')).toBeInTheDocument();
expect(getByText('playback')).toBeInTheDocument();
diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx
index f29ad2cd6a..cd3f2847c6 100644
--- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx
+++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx
@@ -20,6 +20,7 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
+import { catalogEntityRouteRef } from '../../routes';
import { DomainExplorerContent } from './DomainExplorerContent';
describe('', () => {
@@ -71,6 +72,11 @@ describe('', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
+ },
+ },
);
await waitFor(() => {
@@ -86,6 +92,11 @@ describe('', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
+ },
+ },
);
await waitFor(() =>
@@ -101,6 +112,11 @@ describe('', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
+ },
+ },
);
await waitFor(() =>
diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts
index 27573eaeb9..c5dd6644e7 100644
--- a/plugins/explore/src/plugin.ts
+++ b/plugins/explore/src/plugin.ts
@@ -16,7 +16,7 @@
import { createApiFactory, createPlugin } from '@backstage/core';
import { exploreToolsConfigRef } from '@backstage/plugin-explore-react';
-import { exploreRouteRef } from './routes';
+import { catalogEntityRouteRef, exploreRouteRef } from './routes';
import { exampleTools } from './util/examples';
export const explorePlugin = createPlugin({
@@ -37,4 +37,7 @@ export const explorePlugin = createPlugin({
routes: {
explore: exploreRouteRef,
},
+ externalRoutes: {
+ catalogEntity: catalogEntityRouteRef,
+ },
});
diff --git a/plugins/explore/src/routes.ts b/plugins/explore/src/routes.ts
index c41a53128f..fc7868c4c9 100644
--- a/plugins/explore/src/routes.ts
+++ b/plugins/explore/src/routes.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { createRouteRef } from '@backstage/core';
+import { createExternalRouteRef, createRouteRef } from '@backstage/core';
const NoIcon = () => null;
@@ -22,3 +22,8 @@ export const exploreRouteRef = createRouteRef({
icon: NoIcon,
title: 'Explore',
});
+
+export const catalogEntityRouteRef = createExternalRouteRef({
+ id: 'catalog-entity',
+ params: ['namespace', 'kind', 'name'],
+});