diff --git a/.changeset/hungry-buttons-fetch.md b/.changeset/hungry-buttons-fetch.md
new file mode 100644
index 0000000000..f2f9bc26bc
--- /dev/null
+++ b/.changeset/hungry-buttons-fetch.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+The `entityRouteRef` is now a well-known route that should be imported directly from `@backstage/plugin-catalog-react`. It is guaranteed to be globally unique across duplicate installations of the `@backstage/plugin-catalog-react`, starting at this version.
+
+Deprecated `entityRoute` in favor of `entityRouteRef`.
+
+Deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead.
diff --git a/.changeset/rude-hounds-happen.md b/.changeset/rude-hounds-happen.md
new file mode 100644
index 0000000000..b88c2b12d2
--- /dev/null
+++ b/.changeset/rude-hounds-happen.md
@@ -0,0 +1,27 @@
+---
+'@backstage/create-app': patch
+---
+
+Added an external route binding from the `org` plugin to the catalog index page.
+
+This change is needed because `@backstage/plugin-org` now has a required external route that needs to be bound for the app to start.
+
+To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`:
+
+```diff
+ import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
++import { orgPlugin } from '@backstage/plugin-org';
+ import { SearchPage } from '@backstage/plugin-search';
+```
+
+And further down within the `createApp` call:
+
+```diff
+ bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: catalogImportPlugin.routes.importPage,
+ });
++ bind(orgPlugin.externalRoutes, {
++ catalogIndex: catalogPlugin.routes.catalogIndex,
++ });
+ },
+```
diff --git a/.changeset/shaggy-days-film.md b/.changeset/shaggy-days-film.md
new file mode 100644
index 0000000000..ae9c47c318
--- /dev/null
+++ b/.changeset/shaggy-days-film.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-bazaar': patch
+---
+
+Switched out internal usage of the `catalogRouteRef` from `@backstage/plugin-catalog-react`.
diff --git a/.changeset/short-apples-return.md b/.changeset/short-apples-return.md
new file mode 100644
index 0000000000..1339de3410
--- /dev/null
+++ b/.changeset/short-apples-return.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Added validation during the application startup that detects if there are any plugins present that have not had their required external routes bound. Failing the validation will cause a hard crash as it is a programmer error. It lets you detect early on that there are dangling routes, rather than having them cause an error later on.
diff --git a/.changeset/tough-wombats-taste.md b/.changeset/tough-wombats-taste.md
new file mode 100644
index 0000000000..2d034f35b6
--- /dev/null
+++ b/.changeset/tough-wombats-taste.md
@@ -0,0 +1,11 @@
+---
+'@backstage/plugin-org': minor
+---
+
+**BREAKING**: Added a new and required `catalogIndex` external route. It should typically be linked to the `catalogIndex` route of the Catalog plugin:
+
+```ts
+bind(orgPlugin.externalRoutes, {
+ catalogIndex: catalogPlugin.routes.catalogIndex,
+});
+```
diff --git a/microsite/pages/en/link.js b/microsite/pages/en/link.js
new file mode 100644
index 0000000000..ddac35e545
--- /dev/null
+++ b/microsite/pages/en/link.js
@@ -0,0 +1,27 @@
+const React = require('react');
+
+// This is an index of stable short-links to different doc sites
+// for example https://backstage.io/link?bind-routes
+const redirects = {
+ 'bind-routes':
+ '/docs/plugins/composability#binding-external-routes-in-the-app',
+};
+const fallback = '/docs';
+
+function Link() {
+ return (
+
+
+
+ );
+}
+
+module.exports = Link;
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 21eb3aa38a..0e75008571 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -53,6 +53,7 @@ import {
CostInsightsPage,
CostInsightsProjectGrowthInstructionsPage,
} from '@backstage/plugin-cost-insights';
+import { orgPlugin } from '@backstage/plugin-org';
import { ExplorePage, explorePlugin } from '@backstage/plugin-explore';
import { GcpProjectsPage } from '@backstage/plugin-gcp-projects';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
@@ -125,6 +126,9 @@ const app = createApp({
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
+ bind(orgPlugin.externalRoutes, {
+ catalogIndex: catalogPlugin.routes.catalogIndex,
+ });
},
});
diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx
index b09d424737..467145acfc 100644
--- a/packages/core-app-api/src/app/AppManager.test.tsx
+++ b/packages/core-app-api/src/app/AppManager.test.tsx
@@ -35,31 +35,9 @@ import {
createRoutableExtension,
analyticsApiRef,
} from '@backstage/core-plugin-api';
-import { generateBoundRoutes, AppManager } from './AppManager';
+import { AppManager } from './AppManager';
import { AppComponents, AppIcons } from './types';
-describe('generateBoundRoutes', () => {
- it('runs happy path', () => {
- const external = { myRoute: createExternalRouteRef({ id: '1' }) };
- const ref = createRouteRef({ id: 'ref-1' });
- const result = generateBoundRoutes(({ bind }) => {
- bind(external, { myRoute: ref });
- });
-
- expect(result.get(external.myRoute)).toBe(ref);
- });
-
- it('throws on unknown keys', () => {
- const external = { myRoute: createExternalRouteRef({ id: '2' }) };
- const ref = createRouteRef({ id: 'ref-2' });
- expect(() =>
- generateBoundRoutes(({ bind }) => {
- bind(external, { someOtherRoute: ref } as any);
- }),
- ).toThrow('Key someOtherRoute is not an existing external route');
- });
-});
-
describe('Integration Test', () => {
const noOpAnalyticsApi = createApiFactory(
analyticsApiRef,
@@ -545,8 +523,47 @@ describe('Integration Test', () => {
});
expect(errorLogs).toEqual([
expect.stringContaining(
- 'Parameter :thing is duplicated in path /test/:thing/some/:thing',
+ 'The above error occurred in the component',
),
+ ]);
+ });
+
+ it('should throw an error when required external plugin routes are not bound', () => {
+ const app = new AppManager({
+ apis: [],
+ defaultApis: [],
+ themes: [
+ {
+ id: 'light',
+ title: 'Light Theme',
+ variant: 'light',
+ Provider: ({ children }) => <>{children}>,
+ },
+ ],
+ icons,
+ plugins: [],
+ components,
+ configLoader: async () => [],
+ });
+
+ const Provider = app.getProvider();
+ const Router = app.getRouter();
+ const { error: errorLogs } = withLogCollector(() => {
+ expect(() =>
+ render(
+
+
+
+ } />
+
+
+ ,
+ ),
+ ).toThrow(
+ /^External route 'extRouteRef1' of the 'blob' plugin must be bound to a target route/,
+ );
+ });
+ expect(errorLogs).toEqual([
expect.stringContaining(
'The above error occurred in the component',
),
diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx
index d52e656559..0262ece8f4 100644
--- a/packages/core-app-api/src/app/AppManager.tsx
+++ b/packages/core-app-api/src/app/AppManager.tsx
@@ -45,9 +45,6 @@ import {
IdentityApi,
identityApiRef,
BackstagePlugin,
- RouteRef,
- SubRouteRef,
- ExternalRouteRef,
} from '@backstage/core-plugin-api';
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
import {
@@ -64,7 +61,10 @@ import {
} from '../routing/collectors';
import { RoutingProvider } from '../routing/RoutingProvider';
import { RouteTracker } from '../routing/RouteTracker';
-import { validateRoutes } from '../routing/validation';
+import {
+ validateRouteParameters,
+ validateRouteBindings,
+} from '../routing/validation';
import { AppContextProvider } from './AppContext';
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
import {
@@ -72,13 +72,13 @@ import {
AppConfigLoader,
AppContext,
AppOptions,
- AppRouteBinder,
BackstageApp,
SignInPageProps,
} from './types';
import { AppThemeProvider } from './AppThemeProvider';
import { defaultConfigLoader } from './defaultConfigLoader';
import { ApiRegistry } from '../apis/system/ApiRegistry';
+import { resolveRouteBindings } from './resolveRouteBindings';
type CompatiblePlugin =
| BackstagePlugin
@@ -86,35 +86,6 @@ type CompatiblePlugin =
output(): Array<{ type: 'feature-flag'; name: string }>;
});
-export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
- const result = new Map();
-
- if (bindRoutes) {
- const bind: AppRouteBinder = (
- externalRoutes,
- targetRoutes: { [name: string]: RouteRef | SubRouteRef },
- ) => {
- for (const [key, value] of Object.entries(targetRoutes)) {
- const externalRoute = externalRoutes[key];
- if (!externalRoute) {
- throw new Error(`Key ${key} is not an existing external route`);
- }
- if (!value && !externalRoute.optional) {
- throw new Error(
- `External route ${key} is required but was undefined`,
- );
- }
- if (value) {
- result.set(externalRoute, value);
- }
- }
- };
- bindRoutes({ bind });
- }
-
- return result;
-}
-
/**
* Get the app base path from the configured app baseUrl.
*
@@ -225,39 +196,57 @@ export class AppManager implements BackstageApp {
getProvider(): ComponentType<{}> {
const appContext = new AppContextImpl(this);
+ // We only validate routes once
+ let routesHaveBeenValidated = false;
+
const Provider = ({ children }: PropsWithChildren<{}>) => {
const appThemeApi = useMemo(
() => AppThemeSelector.createWithStorage(this.themes),
[],
);
- const { routePaths, routeParents, routeObjects, featureFlags } =
- useMemo(() => {
- const result = traverseElementTree({
- root: children,
- discoverers: [childDiscoverer, routeElementDiscoverer],
- collectors: {
- routePaths: routePathCollector,
- routeParents: routeParentCollector,
- routeObjects: routeObjectCollector,
- collectedPlugins: pluginCollector,
- featureFlags: featureFlagCollector,
- },
- });
+ const {
+ routePaths,
+ routeParents,
+ routeObjects,
+ featureFlags,
+ routeBindings,
+ } = useMemo(() => {
+ const result = traverseElementTree({
+ root: children,
+ discoverers: [childDiscoverer, routeElementDiscoverer],
+ collectors: {
+ routePaths: routePathCollector,
+ routeParents: routeParentCollector,
+ routeObjects: routeObjectCollector,
+ collectedPlugins: pluginCollector,
+ featureFlags: featureFlagCollector,
+ },
+ });
- validateRoutes(result.routePaths, result.routeParents);
+ // TODO(Rugvip): Restructure the public API so that we can get an immediate view of
+ // the app, rather than having to wait for the provider to render.
+ // For now we need to push the additional plugins we find during
+ // collection and then make sure we initialize things afterwards.
+ result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
+ this.verifyPlugins(this.plugins);
- // TODO(Rugvip): Restructure the public API so that we can get an immediate view of
- // the app, rather than having to wait for the provider to render.
- // For now we need to push the additional plugins we find during
- // collection and then make sure we initialize things afterwards.
- result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
- this.verifyPlugins(this.plugins);
+ // Initialize APIs once all plugins are available
+ this.getApiHolder();
+ return {
+ ...result,
+ routeBindings: resolveRouteBindings(this.bindRoutes),
+ };
+ }, [children]);
- // Initialize APIs once all plugins are available
- this.getApiHolder();
- return result;
- }, [children]);
+ if (!routesHaveBeenValidated) {
+ routesHaveBeenValidated = true;
+ validateRouteParameters(routePaths, routeParents);
+ validateRouteBindings(
+ routeBindings,
+ this.plugins as Iterable>,
+ );
+ }
const loadedConfig = useConfigLoader(
this.configLoader,
@@ -318,7 +307,7 @@ export class AppManager implements BackstageApp {
routePaths={routePaths}
routeParents={routeParents}
routeObjects={routeObjects}
- routeBindings={generateBoundRoutes(this.bindRoutes)}
+ routeBindings={routeBindings}
basePath={getBasePath(loadedConfig.api)}
>
{children}
diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts
new file mode 100644
index 0000000000..5c9fde53b7
--- /dev/null
+++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createExternalRouteRef,
+ createRouteRef,
+} from '@backstage/core-plugin-api';
+import { resolveRouteBindings } from './resolveRouteBindings';
+
+describe('resolveRouteBindings', () => {
+ it('runs happy path', () => {
+ const external = { myRoute: createExternalRouteRef({ id: '1' }) };
+ const ref = createRouteRef({ id: 'ref-1' });
+ const result = resolveRouteBindings(({ bind }) => {
+ bind(external, { myRoute: ref });
+ });
+
+ expect(result.get(external.myRoute)).toBe(ref);
+ });
+
+ it('throws on unknown keys', () => {
+ const external = { myRoute: createExternalRouteRef({ id: '2' }) };
+ const ref = createRouteRef({ id: 'ref-2' });
+ expect(() =>
+ resolveRouteBindings(({ bind }) => {
+ bind(external, { someOtherRoute: ref } as any);
+ }),
+ ).toThrow('Key someOtherRoute is not an existing external route');
+ });
+});
diff --git a/packages/core-app-api/src/app/resolveRouteBindings.ts b/packages/core-app-api/src/app/resolveRouteBindings.ts
new file mode 100644
index 0000000000..63119b204b
--- /dev/null
+++ b/packages/core-app-api/src/app/resolveRouteBindings.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ RouteRef,
+ SubRouteRef,
+ ExternalRouteRef,
+} from '@backstage/core-plugin-api';
+import { AppOptions, AppRouteBinder } from './types';
+
+export function resolveRouteBindings(bindRoutes: AppOptions['bindRoutes']) {
+ const result = new Map();
+
+ if (bindRoutes) {
+ const bind: AppRouteBinder = (
+ externalRoutes,
+ targetRoutes: { [name: string]: RouteRef | SubRouteRef },
+ ) => {
+ for (const [key, value] of Object.entries(targetRoutes)) {
+ const externalRoute = externalRoutes[key];
+ if (!externalRoute) {
+ throw new Error(`Key ${key} is not an existing external route`);
+ }
+ if (!value && !externalRoute.optional) {
+ throw new Error(
+ `External route ${key} is required but was undefined`,
+ );
+ }
+ if (value) {
+ result.set(externalRoute, value);
+ }
+ }
+ };
+ bindRoutes({ bind });
+ }
+
+ return result;
+}
diff --git a/packages/core-app-api/src/routing/RoutingProvider.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.test.tsx
index fbdd34ae68..3391bab8b0 100644
--- a/packages/core-app-api/src/routing/RoutingProvider.test.tsx
+++ b/packages/core-app-api/src/routing/RoutingProvider.test.tsx
@@ -39,7 +39,7 @@ import {
routeParentCollector,
routeObjectCollector,
} from './collectors';
-import { validateRoutes } from './validation';
+import { validateRouteParameters } from './validation';
import { RouteResolver } from './RouteResolver';
import { AnyRouteRef, RouteFunc } from './types';
import { AppContextProvider } from '../app/AppContext';
@@ -323,7 +323,7 @@ describe('discovery', () => {
},
});
- expect(() => validateRoutes(routePaths, routeParents)).toThrow(
+ expect(() => validateRouteParameters(routePaths, routeParents)).toThrow(
'Parameter :id is duplicated in path /foo/:id/bar/:id',
);
});
diff --git a/packages/core-app-api/src/routing/validation.ts b/packages/core-app-api/src/routing/validation.ts
index 51078a0130..c5d3e4aca9 100644
--- a/packages/core-app-api/src/routing/validation.ts
+++ b/packages/core-app-api/src/routing/validation.ts
@@ -14,9 +14,16 @@
* limitations under the License.
*/
+import {
+ BackstagePlugin,
+ ExternalRouteRef,
+ RouteRef,
+ SubRouteRef,
+} from '@backstage/core-plugin-api';
import { AnyRouteRef } from './types';
-export function validateRoutes(
+// Validates that there is no duplication of route parameter names
+export function validateRouteParameters(
routePaths: Map,
routeParents: Map,
) {
@@ -54,3 +61,30 @@ export function validateRoutes(
}
}
}
+
+// Validates that all non-optional external routes have been bound
+export function validateRouteBindings(
+ routeBindings: Map,
+ plugins: Iterable>>,
+) {
+ for (const plugin of plugins) {
+ if (!plugin.externalRoutes) {
+ continue;
+ }
+
+ for (const [name, externalRouteRef] of Object.entries(
+ plugin.externalRoutes,
+ )) {
+ if (externalRouteRef.optional) {
+ continue;
+ }
+
+ if (!routeBindings.has(externalRouteRef)) {
+ throw new Error(
+ `External route '${name}' of the '${plugin.getId()}' plugin must be bound to a target route. ` +
+ 'See https://backstage.io/link?bind-routes for details.',
+ );
+ }
+ }
+ }
+}
diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx
index 4c284e9c9a..78949b0755 100644
--- a/packages/create-app/templates/default-app/packages/app/src/App.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx
@@ -11,6 +11,7 @@ import {
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
+import { orgPlugin } from '@backstage/plugin-org';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
@@ -42,6 +43,9 @@ const app = createApp({
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
});
+ bind(orgPlugin.externalRoutes, {
+ catalogIndex: catalogPlugin.routes.catalogIndex,
+ });
},
});
diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx
index 3f9da083bb..5624f40765 100644
--- a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx
+++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx
@@ -44,10 +44,7 @@ import { Member, BazaarProject } from '../../types';
import { bazaarApiRef } from '../../api';
import { Alert } from '@material-ui/lab';
import useAsyncFn from 'react-use/lib/useAsyncFn';
-import {
- catalogApiRef,
- catalogRouteRef,
-} from '@backstage/plugin-catalog-react';
+import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import {
parseEntityName,
@@ -85,7 +82,7 @@ export const HomePageBazaarInfoCard = ({
initEntity,
}: Props) => {
const classes = useStyles();
- const catalogLink = useRouteRef(catalogRouteRef);
+ const entityLink = useRouteRef(entityRouteRef);
const bazaarApi = useApi(bazaarApiRef);
const identity = useApi(identityApiRef);
const catalogApi = useApi(catalogApiRef);
@@ -156,7 +153,7 @@ export const HomePageBazaarInfoCard = ({
const { name, kind, namespace } = parseEntityName(
bazaarProject.value.entityRef,
);
- return `${catalogLink()}/${namespace}/${kind}/${name}`;
+ return entityLink({ kind, namespace, name });
}
return '';
};
diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md
index 129f7b36af..398bace903 100644
--- a/plugins/catalog-react/api-report.md
+++ b/plugins/catalog-react/api-report.md
@@ -102,7 +102,7 @@ export type CatalogReactUserListPickerClassKey =
// Warning: (ae-missing-release-tag) "catalogRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const catalogRouteRef: RouteRef;
// Warning: (ae-missing-release-tag) "createDomainColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -592,7 +592,7 @@ export const EntityRefLinks: ({
// Warning: (ae-missing-release-tag) "entityRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const entityRoute: RouteRef<{
name: string;
kind: string;
@@ -610,7 +610,7 @@ export function entityRouteParams(entity: Entity): {
// Warning: (ae-missing-release-tag) "entityRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public
export const entityRouteRef: RouteRef<{
name: string;
kind: string;
@@ -813,7 +813,7 @@ export function reduceEntityFilters(
// Warning: (ae-missing-release-tag) "rootRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public @deprecated (undocumented)
export const rootRoute: RouteRef;
// @public
diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts
index f29d52f08a..1fcbd14866 100644
--- a/plugins/catalog-react/src/routes.ts
+++ b/plugins/catalog-react/src/routes.ts
@@ -16,20 +16,43 @@
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { createRouteRef } from '@backstage/core-plugin-api';
+import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
// TODO(Rugvip): Move these route refs back to the catalog plugin once we're all ported to using external routes
+/**
+ * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`.
+ */
export const rootRoute = createRouteRef({
id: 'catalog',
});
+/**
+ * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`.
+ */
export const catalogRouteRef = rootRoute;
-export const entityRoute = createRouteRef({
- id: 'catalog:entity',
- params: ['namespace', 'kind', 'name'],
-});
+/**
+ * A stable route ref that points to the catalog page for an individual entity.
+ *
+ * This `RouteRef` can be imported and used directly, and does not need to be referenced
+ * via an `ExternalRouteRef`.
+ *
+ * If you want to replace the `EntityPage` from `@backstage/catalog-plugin` in your app,
+ * you need to use the `entityRouteRef` as the mount point instead of your own.
+ */
+export const entityRouteRef = getOrCreateGlobalSingleton(
+ 'catalog:entity-route-ref',
+ () =>
+ createRouteRef({
+ id: 'catalog:entity',
+ params: ['namespace', 'kind', 'name'],
+ }),
+);
-export const entityRouteRef = entityRoute;
+/**
+ * @deprecated use `entityRouteRef` instead.
+ */
+export const entityRoute = entityRouteRef;
// Utility function to get suitable route params for entityRoute, given an
// entity instance
diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md
index 19f5acda69..b181a3aab4 100644
--- a/plugins/org/api-report.md
+++ b/plugins/org/api-report.md
@@ -7,6 +7,7 @@
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
+import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { GroupEntity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core-components';
import { UserEntity } from '@backstage/catalog-model';
@@ -74,7 +75,12 @@ export const MembersListCard: (_props: {
// Warning: (ae-missing-release-tag) "orgPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-const orgPlugin: BackstagePlugin<{}, {}>;
+const orgPlugin: BackstagePlugin<
+ {},
+ {
+ catalogIndex: ExternalRouteRef;
+ }
+>;
export { orgPlugin };
export { orgPlugin as plugin };
diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx
index 6a20035dfa..a0536e1a74 100644
--- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx
+++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx
@@ -19,7 +19,6 @@ import { ApiProvider } from '@backstage/core-app-api';
import {
CatalogApi,
catalogApiRef,
- catalogRouteRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
@@ -31,6 +30,7 @@ import {
} from '@backstage/theme';
import { Grid, ThemeProvider } from '@material-ui/core';
import React from 'react';
+import { catalogIndexRouteRef } from '../../../routes';
import { OwnershipCard } from './OwnershipCard';
export default {
@@ -100,7 +100,7 @@ export const Default = () =>
,
{
- mountedRoutes: { '/catalog': catalogRouteRef },
+ mountedRoutes: { '/catalog': catalogIndexRouteRef },
},
);
@@ -134,6 +134,6 @@ export const Themed = () =>
,
{
- mountedRoutes: { '/catalog': catalogRouteRef },
+ mountedRoutes: { '/catalog': catalogIndexRouteRef },
},
);
diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx
index 439365e583..26a79dfaee 100644
--- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx
+++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx
@@ -23,11 +23,11 @@ import {
CatalogApi,
catalogApiRef,
EntityProvider,
- catalogRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { queryByText } from '@testing-library/react';
import React from 'react';
+import { catalogIndexRouteRef } from '../../../routes';
import { OwnershipCard } from './OwnershipCard';
const items = [
@@ -159,7 +159,7 @@ describe('OwnershipCard', () => {
,
{
mountedRoutes: {
- '/create': catalogRouteRef,
+ '/create': catalogIndexRouteRef,
},
},
);
@@ -205,7 +205,7 @@ describe('OwnershipCard', () => {
,
{
mountedRoutes: {
- '/create': catalogRouteRef,
+ '/create': catalogIndexRouteRef,
},
},
);
@@ -236,7 +236,7 @@ describe('OwnershipCard', () => {
,
{
mountedRoutes: {
- '/create': catalogRouteRef,
+ '/create': catalogIndexRouteRef,
},
},
);
@@ -282,7 +282,7 @@ describe('OwnershipCard', () => {
,
{
mountedRoutes: {
- '/create': catalogRouteRef,
+ '/create': catalogIndexRouteRef,
},
},
);
diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx
index d3a90d63e3..c89534ea7a 100644
--- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx
+++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx
@@ -25,7 +25,6 @@ import {
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
catalogApiRef,
- catalogRouteRef,
formatEntityRefTitle,
isOwnerOf,
useEntity,
@@ -42,6 +41,7 @@ import qs from 'qs';
import React from 'react';
import pluralize from 'pluralize';
import useAsync from 'react-use/lib/useAsync';
+import { catalogIndexRouteRef } from '../../../routes';
type EntityTypeProps = {
kind: string;
@@ -138,7 +138,7 @@ export const OwnershipCard = ({
}) => {
const { entity } = useEntity();
const catalogApi = useApi(catalogApiRef);
- const catalogLink = useRouteRef(catalogRouteRef);
+ const catalogLink = useRouteRef(catalogIndexRouteRef);
const {
loading,
diff --git a/plugins/org/src/plugin.ts b/plugins/org/src/plugin.ts
index b2183db8ef..5da517f1db 100644
--- a/plugins/org/src/plugin.ts
+++ b/plugins/org/src/plugin.ts
@@ -17,9 +17,13 @@ import {
createComponentExtension,
createPlugin,
} from '@backstage/core-plugin-api';
+import { catalogIndexRouteRef } from './routes';
export const orgPlugin = createPlugin({
id: 'org',
+ externalRoutes: {
+ catalogIndex: catalogIndexRouteRef,
+ },
});
export const EntityGroupProfileCard = orgPlugin.provide(
diff --git a/plugins/org/src/routes.ts b/plugins/org/src/routes.ts
new file mode 100644
index 0000000000..aa24c1b1cc
--- /dev/null
+++ b/plugins/org/src/routes.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createExternalRouteRef } from '@backstage/core-plugin-api';
+
+export const catalogIndexRouteRef = createExternalRouteRef({
+ id: 'catalog-index',
+});