From f369994ef6569d05d790d9164da28d0cf2d9fc25 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 14 Dec 2023 16:12:18 +0100 Subject: [PATCH 01/42] docs: start drafting new route system Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 457 ++++++++++++++---- 1 file changed, 367 insertions(+), 90 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 1402796077..b38ef9016c 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -8,143 +8,420 @@ description: Frontend routes > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -See [routing system docs](../../plugins/composability.md#routing-system) - ## Introduction - +The composability system isn't a single API surface. It is a collection of patterns, primitives, and APIs. At the core is the concept of extensions, which are exported by plugins for use in the app. One of the concepts is `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. ## Route References - +There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each. Keep reading 🙂! ### Creating a Route Reference - +```tsx +// plugins/catalog/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; -### Using a Route Reference +export const rootRouteRef = createRouteRef(); +``` - +Note that you almost always want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. ### Route Path Parameters - +The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a kind, namespace and name `params`, like in this path `/entities/:name/:namespace/:kind`: + +```tsx +// plugins/catalog/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +type DetailsRouteParams = { namespace: string; name: string; kind: string }; + +export const detailsRouteRef = createRouteRef({ + // A list of parameter names that the path that this route ref is bound to must contain + params: ['namespace', 'name', 'kind'], +}); +``` ### Providing Route References to Plugins - +Route refs do not have any behavior, in other words, they are an opaque type that represents route targets in an app, which are bound to specific paths at runtime, but they provide a level of indirection to help mix together different plugins that otherwise wouldn't know how to route to each other. + +The code snippet of the previous section does not indicate which plugin the route belongs to. To do so, we have to extend Backstage with a new page extension associated with the newly created `RouteRef` and provide the extension through our plugin. Here's what we need to do: + +```tsx +// plugins/catalog/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef(); // [1] + +// plugins/catalog/src/plugin.tsx +import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-api'; +import { rootRouteRef } from './routes'; + +const rootPage = createPageExtension({ // [2] + // Ommiting name since it is the root page + defaultPath: '/' + routeRef: rootRouteRef, + loader: async () => Root Page +}); + +export default const createPlugin({ // [3] + id: 'catalog', + routes: { + root: rootRouteRef, + }, + extensions: [rootPage] +}); + +// plugins/catalog/src/index.ts +export { default } from './plugin' +``` + +We have completed our journey of creating a plugin page route. This is what the code does: + +- [1] The line 1 creates a route reference, which is not yet associated with any plugin page; +- [2] A route that renders nothing makes no sense, does it? So we are creating a page extension that associates a path and a component with the newly created route ref; +- [3] Finally, our plugin provides both routes and extensions. + +It's a smart question, and the answer can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. + +### Using a Route Reference + +You can link to the routes from other pages in the same plugin or you can also link between different plugins pages. In this section we will cover the first scenario, if you are interested in link to a page of a plugin that isn't yours, please go to the [external routes](#external-router-references) section below. + +Alright, let's presume that we have a plugin that renders tow different pages, and these pages links to each other. + +First lets create the routes references: + +```tsx +// plugins/catalog/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef(); + +type DetailsRouteParams = { + namespace: string; + name: string; + kind: string; +}; + +export const detailsRouteRef = createRouteRef({ + // A list of parameter names that the path that this route ref is bound to must contain + params: ['namespace', 'name', 'kind'], +}); +``` + +Now we are ready to provide these routes via plugin extensions and link between pages: + +```tsx +// plugins/catalog/src/plugin.tsx +import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; +import { rootRouteRef, detailsRouteRef } from './routes'; + +const rootPage = createPageExtension({ + // Ommiting name since it is the root page + defaultPath: '/' + routeRef: rootRouteRef, + loader: async () => { + const href = useRouteRef(catalogEntityDetailsRouteRef)({ + kind: 'Component', + namespace: 'Default', + name: 'foo' + }); + + return ( +
+

Index Page

+ Entity Foo +
+ ); + } +}); + +const detailsPage = createPageExtension({ + name: 'details', + defaultPath: '/entities/:namespace/:kind/:name' + routeRef: detailsRouteRef, + loader: async () => ( +
+

Catalog Entities

+
+ ) +}); + +export default const createPlugin({ + id: 'catalog', + routes: { + list: catalogEntityListRouteRef, + details: catalogEntityDetailsRouteRef, + }, + extensions: [rootPage, detailsPage] +}); + +// plugins/catalog/src/index.ts +export { default } from './plugin' +``` + +During runtime, we used a hook `useRouteRef` to get the path to the details page. Because we are linking to pages of the same plugin, we are currently accessing the reference directly, but in the following sections, you will see how to link to pages of different plugins. ## External Router References - +```tsx +// plugins/catalog/src/routes.ts +import { + createRouteRef, + createExternalRouteRef, +} from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef(); +const createComponentRouteRef = createExternalRouteRef(); + +// plugins/catalog/src/plugin.tsx +import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; +import { rootRouteRef, createComponentRouteRef } from './routes'; + +const rootPage = createPageExtension({ + // Ommiting name since it is the root page + defaultPath: '/' + routeRef: rootRouteRef, + loader: async () => { + const href = useRouteRef(catalogCreateComponentRouteRef)(); + + return ( +
+

Catalog Entities

+ {/* Linking to a create component page without direct reference */} + Create Component +
+ ); + } +}); + +export default const createPlugin({ + id: 'catalog', + routes: { + entityList: entityListRouteRef + } + externalRoutes: { + createComponent: createComponentRouteRef, + }, + extensions: [rootPage] +}); + +// plugins/catalog/src/index.ts +export { default } from './plugin'; +``` + +```tsx +// plugins/scaffolder/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +const createComponentRouteRef = createRouteRef(); + +// plugins/scaffolder/src/plugin.tsx +import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-api'; +import { createComponentRouteRef } from './routes'; + +const createComponentPage = createPageExtension({ + defaultPath: '/' + routeRef: createComponentRouteRef, + loader: async () => ( +
+

Create Component

+
+ ) +}); + +export default const createPlugin({ + id: 'scaffolder', + routes: { + createComponent: createComponentRouteRef, + }, + extensions: [createComponentPage] +}); + +// plugins/scaffolder/src/index.ts +export { default } from './plugin'; +``` + +On important thing to highlight is that it is currently not possible to have parameterized `ExternalRouteRefs`, or to bind an external route to a parameterized route, although this may be added in the future if needed. + +Now let's move on and configure the app to point to the Scaffolder create component page when the catalog create component ref be used. ### Binding External Route References - +```yaml +# app-config.yaml +app: + routes: + bindings: + plugin.catalog.externalRoutes.createComponent: plugin.scaffolfer.routes.createComponent +``` + +Or via code, in the file where the app is created: + +```tsx +// packages/app/src/App.tsx +import { createApp } from '@backstage/frontend-app-api'; +import catalog from '@backstage/plugin-catalog'; +import scaffolder from '@backstage/plugin-scaffolder'; + +const app = createApp({ + bindRoutes({ bind }) { + bind(catalog.externalRoutes, { + createComponent: scaffolder.routes.createComponent, + }); + }, +}); + +export default app.createRoot(); + +// packages/app/src/index.ts +import ReactDOM from 'react-dom/client'; +import app from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render(app); +``` + +Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. + +Note that we are not importing and using the RouteRefs directly in the app, and instead rely on the plugin instance to access routes of the plugins. This is a new convention that was introduced to provide better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. this his indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. + +Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters.parameters. ### Optional External Route References - +```tsx +// plugins/catalog/src/routes.ts +import { + createRouteRef, + createExternalRouteRef, +} from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef(); +const createComponentRouteRef = createExternalRouteRef({ + optional: true, +}); +``` + +An external route that is marked as optional is not required to be bound in the app, allowing it to be used as a switch for whether a particular link should be displayed or action should be taken. + +When calling useRouteRef with an optional external route, its return signature is changed to RouteFunc | undefined, allowing for logic like this: + +```tsx +// plugins/catalog/src/plugin.tsx +import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; +import { rootRouteRef, createComponentRouteRef } from './routes'; + +const catalogEntityListPage = createPageExtension({ + defaultPath: '/' + routeRef: rootRouteRef, + loader: async () => { + const href = useRouteRef(catalogCreateComponentRouteRef)(); + + return ( +
+

Catalog Entities

+ {/* Since the route is optional, rendering the link only if the href is defined */} + {href && Create Component +
+ ); + } +}); + +export default const createPlugin({ + id: 'catalog', + routes: { + entityList: rootRouteRef + } + externalRoutes: { + createComponent: createComponentRouteRef, + }, + extensions: [catalogEntityListPage] +}); + +// index.ts +export { default } from './plugin'; +``` ## Sub Route References - +const rootRouteRef = createRouteRef(); +const detailsRouteRef = createSubRouteRef({ + parent: rootRouteRef, + path: '/details', +}); -```ts -/* +// plugins/catalog/src/plugin.ts +import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; +import { rootRouteRef, detailsRouteRef } from './routes.ts'; -Some examples +const DetailsPage = () => ( +
+

Entity Details

+
+); -export const indexPageRouteRef = createRouteRef() +const rootPage = createPageExtension({ + defaultPath: '/' + routeRef: rootRouteRef, + loader: async () => { + const { path } = useRouteRef(detailsRouteRef)(); -export const catalogPlugin = createPlugin({ - id: 'catalog, + return ( +
+

Index Page

+ + } /> + +
+ ); + } +}); + +export default createPlugin({ + id: 'catalog', routes: { - index: indexPageRouteRef, + root: rootRouteRef, + details: detailsRouteRef, }, -}) + extensions: [rootPage] +}); -// in catalog plugin - -import {indexPageRouteRef} from '../../routes - -const link = useRouteRef(indexPageRouteRef) - -// scaffolder - -export const catalogIndexPageRouteRef = createExternalRouteRef({ - defaultTarget: 'catalog/index', -}) - -export const scaffolderPlugin = createPlugin({ - id: 'scaffolder, - externalRoutes: { - catalogIndex: catalogIndexPageRouteRef, - }, -}) - - -import {catalogIndexPageRouteRef} from '../../routes - -const link = useRouteRef(catalogIndexPageRouteRef) - -// app - -import {catalogPlugin} from '@backstage/plugin-catalog' - -const app = createApp({ - bindRoutes({bind}) { - bind(scaffolderPlugin, { - catalogIndex: catalogPlugin.routes.index, - }) - }, -}) -*/ +// plugins/catalog/src/index.ts +export { default } from './plugin'; ``` From 21170a170969630b4be5aab671633ae832737516 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:46:16 +0100 Subject: [PATCH 02/42] docs(frontend-system): apply routes introduction suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index b38ef9016c..2280baba27 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -18,7 +18,7 @@ The composability system isn't a single API surface. It is a collection of patte ## Route References -In order to address the problem outlined above, we introduced the `RouteRefs` concept. `RouteRefs` abstract paths in a the Backstage app, and these paths can be configured both at the plugin level (by plugin developers) and at the instance level (by application integrators). +In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract paths in a the Backstage app, and these paths can be configured both at the plugin level (by plugin developers) and at the instance level (by application integrators). Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at the app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, all that path can be changed, so app integrators can set a custom path to a route whenever they like to (more information in the following sessions). From 324cfdce7d6cec371f846b38d1fe8bc397db7a51 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:46:47 +0100 Subject: [PATCH 03/42] docs(frontend-system): apply external routes suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 2280baba27..8a33fb0f15 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -59,7 +59,7 @@ export const detailsRouteRef = createRouteRef({ Route refs do not have any behavior, in other words, they are an opaque type that represents route targets in an app, which are bound to specific paths at runtime, but they provide a level of indirection to help mix together different plugins that otherwise wouldn't know how to route to each other. -The code snippet of the previous section does not indicate which plugin the route belongs to. To do so, we have to extend Backstage with a new page extension associated with the newly created `RouteRef` and provide the extension through our plugin. Here's what we need to do: +The code snippet of the previous section does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension. When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the the extension. Here's what we need to do: ```tsx // plugins/catalog/src/routes.ts From 103d2e11d1575f95a674b81b2de6bf0f10a9627b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:47:20 +0100 Subject: [PATCH 04/42] docs(frontend-system): apply binding routes suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 8a33fb0f15..c8db088ed6 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -72,7 +72,7 @@ import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-ap import { rootRouteRef } from './routes'; const rootPage = createPageExtension({ // [2] - // Ommiting name since it is the root page + // The `name` option is omitted since this is the root page defaultPath: '/' routeRef: rootRouteRef, loader: async () => Root Page From 4143a4d8452bc4b83e5b4e87820637aa16aac876 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:48:21 +0100 Subject: [PATCH 05/42] docs(frontend-system): replace page with div in route examples Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index c8db088ed6..fca9f35704 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -75,7 +75,7 @@ const rootPage = createPageExtension({ // [2] // The `name` option is omitted since this is the root page defaultPath: '/' routeRef: rootRouteRef, - loader: async () => Root Page + loader: async () =>
Root Page
}); export default const createPlugin({ // [3] From 1bb3addf16a3078fc12a30a29ca14372dd41e5d6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:50:03 +0100 Subject: [PATCH 06/42] docs(frontend-system): fix using external route refs example Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index fca9f35704..0b08975ada 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -340,7 +340,7 @@ const catalogEntityListPage = createPageExtension({ defaultPath: '/' routeRef: rootRouteRef, loader: async () => { - const href = useRouteRef(catalogCreateComponentRouteRef)(); + const href = useRouteRef(catalogCreateComponentRouteRef); return (
From 76cbd8a54b098bce22c6bb7ccf97857bd2b46626 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:51:39 +0100 Subject: [PATCH 07/42] docs(frontend-system): fix route subpaths example Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 0b08975ada..422cecdf4d 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -400,13 +400,13 @@ const rootPage = createPageExtension({ defaultPath: '/' routeRef: rootRouteRef, loader: async () => { - const { path } = useRouteRef(detailsRouteRef)(); + const detailsRouteRef = useRouteRef(detailsRouteRef)(); return (

Index Page

- } /> + } />
); From 8a42e2248763019e11b0c0d0b881f67757989aad Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:54:20 +0100 Subject: [PATCH 08/42] docs(frontend-system): adjust subroute refs description Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 422cecdf4d..59785e1dfe 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -369,7 +369,7 @@ export { default } from './plugin'; ## Sub Route References -The last kind of route refs that can be created are `SubRouteRefs`, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. +The last kind of route refs that can be created are `SubRouteRef`s, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. And they can be a useful utility to handle routing within a plugin itself as well. For example: From 144f94ecbe14b2ee9687e9c7effb369d4396df6e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 09:56:44 +0100 Subject: [PATCH 09/42] docs(frontend-system): replace routes composability with frontend system Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 59785e1dfe..ccc44967d2 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -10,11 +10,11 @@ description: Frontend routes ## Introduction -This page describes the composability system that helps bring together content from a multitude of plugins into one Backstage application. +This page describes the frontend system that helps bring together content from a multitude of plugins into one Backstage application. -The core principle of the composability system is that plugins should have clear boundaries and connections. It should isolate crashes within a plugin, but allow navigation between them. It should allow for plugins to be loaded only when needed, and enable plugins to provide extension points for other plugins to build upon. The composability system is also built with an app-first mindset, prioritizing simplicity and clarity in the app over that in the plugins and core APIs. +The core principle of the frontend system is that plugins should have clear boundaries and connections. It should isolate crashes within a plugin, but allow navigation between them. It should allow for plugins to be loaded only when needed, and enable plugins to provide extension points for other plugins to build upon. The frontend system is also built with an app-first mindset, prioritizing simplicity and clarity in the app over that in the plugins and core APIs. -The composability system isn't a single API surface. It is a collection of patterns, primitives, and APIs. At the core is the concept of extensions, which are exported by plugins for use in the app. One of the concepts is `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. +The frontend system isn't a single API surface. It is a collection of patterns, primitives, and APIs. At the core is the concept of extensions, which are exported by plugins for use in the app. One of the concepts is `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. ## Route References From 16a04419d2e6a24133b10354f5c182030c0a3bdd Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 9 Jan 2024 13:02:16 +0100 Subject: [PATCH 10/42] docs(frontend-system): combine optional external routes code snippets Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index ccc44967d2..1639c8ece9 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -312,7 +312,7 @@ Another thing to note is that this indirection in the routing is particularly us ### Optional External Route References -When creating an ExternalRouteRef it is possible to mark it as optional: +It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used used to decide whether a certain link should be displayed or if an action should be taken: ```tsx // plugins/catalog/src/routes.ts @@ -325,13 +325,7 @@ const rootRouteRef = createRouteRef(); const createComponentRouteRef = createExternalRouteRef({ optional: true, }); -``` -An external route that is marked as optional is not required to be bound in the app, allowing it to be used as a switch for whether a particular link should be displayed or action should be taken. - -When calling useRouteRef with an optional external route, its return signature is changed to RouteFunc | undefined, allowing for logic like this: - -```tsx // plugins/catalog/src/plugin.tsx import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; import { rootRouteRef, createComponentRouteRef } from './routes'; From bba9de8e5cc2404bfb7c3cd268fd8aad45e8a58a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 15:16:28 +0100 Subject: [PATCH 11/42] docs(frontend-system): add subroutes with params example Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 104 ++++++++++++++++-- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 1639c8ece9..1e81b8485f 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -381,6 +381,8 @@ const detailsRouteRef = createSubRouteRef({ }); // plugins/catalog/src/plugin.ts +import React from 'react'; +import { Routes, Route, Link } from 'react-router-dom'; import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; import { rootRouteRef, detailsRouteRef } from './routes.ts'; @@ -391,19 +393,27 @@ const DetailsPage = () => ( ); const rootPage = createPageExtension({ - defaultPath: '/' + defaultPath: '/', routeRef: rootRouteRef, loader: async () => { - const detailsRouteRef = useRouteRef(detailsRouteRef)(); + const Component = () => { + const to = useRouteRef(detailsRouteRef)(); - return ( -
-

Index Page

- - } /> - -
- ); + return ( +
+

Index Page

+ {/* Registering the details subroute */} + + } /> + + {/* Linking to the details subroute */} + Details Subpage +
+ ); + }; + + return ; + }, } }); @@ -419,3 +429,77 @@ export default createPlugin({ // plugins/catalog/src/index.ts export { default } from './plugin'; ``` + +Subroutes can also receive parameters, here is an example: + +```tsx +// plugins/catalog/src/routes.ts +import { + createRouteRef, + createSubRouteRef, +} from '@backstage/frontend-plugin-api'; + +const rootRouteRef = createRouteRef(); +const detailsRouteRef = createSubRouteRef({ + parent: rootRouteRef, + path: '/details/:name/:namespace/:kind', +}); + +// plugins/catalog/src/plugin.ts +import React from 'react'; +import { Routes, Route, Link } from 'react-router-dom'; +import { + createPlugin, + createPageExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { rootRouteRef, detailsRouteRef } from './routes.ts'; + +const DetailsPage = () => ( +
+

Entity Details

+ {/* can get the entity ref from URL and load the entity data here */} +
+); + +const rootPage = createPageExtension({ + defaultPath: '/subroutes', + routeRef: rootRouteRef, + loader: async () => { + const Component = () => { + const detailsRoutePath = useRouteRef(detailsRouteRef)({ + // Setting the details subroute params + name: 'entity1', + namespace: 'default', + kind: 'component', + }); + + return ( +
+

Index Page

+ {/* Registering the details subroute */} + + } /> + + {/* Linking to the details subroute */} + Entity 1 Details Subpage +
+ ); + }; + + return ; + }, +}); + +export default createPlugin({ + id: 'catalog', + routes: { + root: rootRouteRef, + details: detailsRouteRef, + }, + extensions: [rootPage], +}); + +// plugins/catalog/src/index.ts +export { default } from './plugin'; +``` From c0a0452bb81ab7d28f5d86624472077cb4d78202 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 16:45:52 +0100 Subject: [PATCH 12/42] docs(frontend-system): improve optional external route explanation Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 1e81b8485f..2eb10b7fc9 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -93,7 +93,7 @@ export { default } from './plugin' We have completed our journey of creating a plugin page route. This is what the code does: - [1] The line 1 creates a route reference, which is not yet associated with any plugin page; -- [2] A route that renders nothing makes no sense, does it? So we are creating a page extension that associates a path and a component with the newly created route ref; +- [2] We associate our route reference with our page by providing it as an option during creation of the page extension. - [3] Finally, our plugin provides both routes and extensions. It's a smart question, and the answer can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. From 70fdbad26a1aa79378804706309b0fa821719f92 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 17:40:06 +0100 Subject: [PATCH 13/42] docs(frontend-system): export route when providing refs Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 2eb10b7fc9..52edea5de9 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -65,7 +65,7 @@ The code snippet of the previous section does not indicate which plugin the rout // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); // [1] +export const rootRouteRef = createRouteRef(); // [1] // plugins/catalog/src/plugin.tsx import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-api'; From 2ae04c3c1d8d139dc8f964fb26cf52c037bc84c6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 17:46:00 +0100 Subject: [PATCH 14/42] docs(frontend-system): fix dialogue on external routes binding Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 52edea5de9..2bc2792e89 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -96,7 +96,7 @@ We have completed our journey of creating a plugin page route. This is what the - [2] We associate our route reference with our page by providing it as an option during creation of the page extension. - [3] Finally, our plugin provides both routes and extensions. -It's a smart question, and the answer can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. +It may be unclear why we need to pass the route to the plugin once it has already been passed to the extension. It's a good point, and the explanation can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. ### Using a Route Reference From b0ee827e0068704722b049107495da5693c52b0e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 17:51:06 +0100 Subject: [PATCH 15/42] docs(frontend-system): remove duplicated binding routes explanation Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 2bc2792e89..b76619f9c3 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -306,7 +306,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(app); Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. -Note that we are not importing and using the RouteRefs directly in the app, and instead rely on the plugin instance to access routes of the plugins. This is a new convention that was introduced to provide better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. this his indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. +Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This is a new convention that was introduced to provide better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters.parameters. From d0fda6d3e6959d97cb9c92bda9542cb1ed1e7521 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 17:52:04 +0100 Subject: [PATCH 16/42] docs(frontend-system): rephrase external routes description Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index b76619f9c3..04f6341e70 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -100,7 +100,7 @@ It may be unclear why we need to pass the route to the plugin once it has alread ### Using a Route Reference -You can link to the routes from other pages in the same plugin or you can also link between different plugins pages. In this section we will cover the first scenario, if you are interested in link to a page of a plugin that isn't yours, please go to the [external routes](#external-router-references) section below. +You can link to the routes from other pages in the same plugin or you can also link between different plugins pages. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. Alright, let's presume that we have a plugin that renders tow different pages, and these pages links to each other. From 137c73ca535cc317db7836672ea5b599c4972661 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 18:00:09 +0100 Subject: [PATCH 17/42] docs(frontend-system): apply more routes suggestions Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 04f6341e70..0acbebb106 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -102,7 +102,7 @@ It may be unclear why we need to pass the route to the plugin once it has alread You can link to the routes from other pages in the same plugin or you can also link between different plugins pages. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. -Alright, let's presume that we have a plugin that renders tow different pages, and these pages links to each other. +Alright, let's presume that we have a plugin that renders two different pages, and these pages link to each other. First lets create the routes references: @@ -110,7 +110,7 @@ First lets create the routes references: // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); +export const rootRouteRef = createRouteRef(); type DetailsRouteParams = { namespace: string; @@ -136,7 +136,7 @@ const rootPage = createPageExtension({ defaultPath: '/' routeRef: rootRouteRef, loader: async () => { - const href = useRouteRef(catalogEntityDetailsRouteRef)({ + const href = useRouteRef(detailsRouteRef)({ kind: 'Component', namespace: 'Default', name: 'foo' @@ -165,8 +165,8 @@ const detailsPage = createPageExtension({ export default const createPlugin({ id: 'catalog', routes: { - list: catalogEntityListRouteRef, - details: catalogEntityDetailsRouteRef, + root: rootRouteRef + details: detailsRouteRef, }, extensions: [rootPage, detailsPage] }); @@ -179,9 +179,9 @@ During runtime, we used a hook `useRouteRef` to get the path to the details page ## External Router References -Now let's assume that we want to link from the Catalog entity list page to the Scaffolder create component page. We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use ExternalRouteRefs. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. +Now let's assume that we want to link from the Catalog entity list page to the Scaffolder create component page. We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. -We create a new ExternalRouteRef inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an ExternalRouteRef similar to this: +We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this: ```tsx // plugins/catalog/src/routes.ts @@ -217,7 +217,7 @@ const rootPage = createPageExtension({ export default const createPlugin({ id: 'catalog', routes: { - entityList: entityListRouteRef + root: rootRouteRef, } externalRoutes: { createComponent: createComponentRouteRef, From 472a39e1092026181daaa15329050ff908487e39 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 19:26:02 +0100 Subject: [PATCH 18/42] docs(frontend-system): more code snippet fixes Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 341 +++++++++++------- 1 file changed, 202 insertions(+), 139 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 0acbebb106..f4cebe3dd1 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -26,15 +26,13 @@ There are 3 types of route references: regular route, sub route, and external ro ### Creating a Route Reference -Route references, also known as root plugin pages, are created as follows: - -_Catalog Plugin_ +Route references, also known as plugins' index pages, are created as follows: ```tsx // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -export const rootRouteRef = createRouteRef(); +export const indexRouteRef = createRouteRef(); ``` Note that you almost always want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. @@ -47,10 +45,8 @@ The referenced route can also accepts `params`. Here is how you create a referen // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -type DetailsRouteParams = { namespace: string; name: string; kind: string }; - -export const detailsRouteRef = createRouteRef({ - // A list of parameter names that the path that this route ref is bound to must contain +export const detailsRouteRef = createRouteRef({ + // The parameters that must be included in the path of this route reference params: ['namespace', 'name', 'kind'], }); ``` @@ -65,29 +61,35 @@ The code snippet of the previous section does not indicate which plugin the rout // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -export const rootRouteRef = createRouteRef(); // [1] +export const indexRouteRef = createRouteRef(); // [1] // plugins/catalog/src/plugin.tsx -import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-api'; -import { rootRouteRef } from './routes'; +import React from 'react'; +import { + createPlugin, + createPageExtension, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef } from './routes'; -const rootPage = createPageExtension({ // [2] - // The `name` option is omitted since this is the root page - defaultPath: '/' - routeRef: rootRouteRef, - loader: async () =>
Root Page
+const indexPage = createPageExtension({ + // [2] + // The `name` option is omitted since this is the index page + defaultPath: '/entities', + routeRef: indexRouteRef, + loader: async () =>
Root Page
, }); -export default const createPlugin({ // [3] +export default createPlugin({ + // [3] id: 'catalog', routes: { - root: rootRouteRef, + index: indexRouteRef, }, - extensions: [rootPage] + extensions: [indexPage], }); // plugins/catalog/src/index.ts -export { default } from './plugin' +export { default } from './plugin'; ``` We have completed our journey of creating a plugin page route. This is what the code does: @@ -110,16 +112,10 @@ First lets create the routes references: // plugins/catalog/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -export const rootRouteRef = createRouteRef(); +export const indexRouteRef = createRouteRef(); -type DetailsRouteParams = { - namespace: string; - name: string; - kind: string; -}; - -export const detailsRouteRef = createRouteRef({ - // A list of parameter names that the path that this route ref is bound to must contain +export const detailsRouteRef = createRouteRef({ + // The parameters that must be included in the path of this route reference params: ['namespace', 'name', 'kind'], }); ``` @@ -128,51 +124,73 @@ Now we are ready to provide these routes via plugin extensions and link between ```tsx // plugins/catalog/src/plugin.tsx -import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; +import React from 'react'; +import { useParams } from 'react-router'; +import { + createPlugin, + createPageExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; import { rootRouteRef, detailsRouteRef } from './routes'; -const rootPage = createPageExtension({ +const indexPage = createPageExtension({ // Ommiting name since it is the root page - defaultPath: '/' - routeRef: rootRouteRef, + defaultPath: '/entities', + routeRef: indexRouteRef, loader: async () => { - const href = useRouteRef(detailsRouteRef)({ - kind: 'Component', - namespace: 'Default', - name: 'foo' - }); + const Component = () => { + const href = useRouteRef(detailsRouteRef)({ + kind: 'component', + namespace: 'default', + name: 'foo', + }); - return ( -
-

Index Page

- Entity Foo -
- ); - } + return ( +
+

Index Page

+ Entity Foo +
+ ); + }; + + return ; + }, }); const detailsPage = createPageExtension({ name: 'details', - defaultPath: '/entities/:namespace/:kind/:name' + defaultPath: '/entities/:namespace/:kind/:name', routeRef: detailsRouteRef, - loader: async () => ( -
-

Catalog Entities

-
- ) + loader: async () => { + const Component = () => { + // Getting the parameters from the URL + const params = useParams(); + return ( +
+

Details Page

+
    +
  • Kind: {params.kind}
  • +
  • Namespace: {params.namespace}
  • +
  • Name: {params.name}
  • +
+
+ ); + }; + return ; + }, }); -export default const createPlugin({ +export default createPlugin({ id: 'catalog', routes: { - root: rootRouteRef + index: indexRouteRef, details: detailsRouteRef, }, - extensions: [rootPage, detailsPage] + extensions: [indexPage, detailsPage], }); // plugins/catalog/src/index.ts -export { default } from './plugin' +export { default } from './plugin'; ``` During runtime, we used a hook `useRouteRef` to get the path to the details page. Because we are linking to pages of the same plugin, we are currently accessing the reference directly, but in the following sections, you will see how to link to pages of different plugins. @@ -183,6 +201,8 @@ Now let's assume that we want to link from the Catalog entity list page to the S We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this: +_Catalog Plugin_ + ```tsx // plugins/catalog/src/routes.ts import { @@ -190,19 +210,19 @@ import { createExternalRouteRef, } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); +const indexRouteRef = createRouteRef(); const createComponentRouteRef = createExternalRouteRef(); // plugins/catalog/src/plugin.tsx +import React from 'react'; import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; -import { rootRouteRef, createComponentRouteRef } from './routes'; +import { indexRouteRef, createComponentRouteRef } from './routes'; -const rootPage = createPageExtension({ - // Ommiting name since it is the root page - defaultPath: '/' - routeRef: rootRouteRef, +const indexPage = createPageExtension({ + defaultPath: '/entities', + routeRef: indexRouteRef, loader: async () => { - const href = useRouteRef(catalogCreateComponentRouteRef)(); + const href = useRouteRef(createComponentRouteRef)(); return (
@@ -214,47 +234,53 @@ const rootPage = createPageExtension({ } }); -export default const createPlugin({ +export default createPlugin({ id: 'catalog', routes: { - root: rootRouteRef, + index: indexRouteRef, } externalRoutes: { createComponent: createComponentRouteRef, }, - extensions: [rootPage] + extensions: [indexPage] }); // plugins/catalog/src/index.ts export { default } from './plugin'; ``` +_Scaffolder Plugin_ + ```tsx // plugins/scaffolder/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -const createComponentRouteRef = createRouteRef(); +const indexRouteRef = createRouteRef(); // plugins/scaffolder/src/plugin.tsx -import { createPlugin, createPageExtension } from '@backstage/frontend-plugin-api'; -import { createComponentRouteRef } from './routes'; +import React from 'react'; +import { + createPlugin, + createPageExtension, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef } from './routes'; -const createComponentPage = createPageExtension({ - defaultPath: '/' - routeRef: createComponentRouteRef, +const indexPage = createPageExtension({ + defaultPath: '/create', + routeRef: indexRouteRef, loader: async () => (

Create Component

- ) + ), }); -export default const createPlugin({ +export default createPlugin({ id: 'scaffolder', routes: { - createComponent: createComponentRouteRef, + index: indexRouteRef, }, - extensions: [createComponentPage] + extensions: [indexPage], }); // plugins/scaffolder/src/index.ts @@ -276,7 +302,7 @@ Using the above example of the Catalog entities list page to the Scaffolder crea app: routes: bindings: - plugin.catalog.externalRoutes.createComponent: plugin.scaffolfer.routes.createComponent + plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index ``` Or via code, in the file where the app is created: @@ -296,12 +322,6 @@ const app = createApp({ }); export default app.createRoot(); - -// packages/app/src/index.ts -import ReactDOM from 'react-dom/client'; -import app from './App'; - -ReactDOM.createRoot(document.getElementById('root')!).render(app); ``` Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. @@ -321,40 +341,45 @@ import { createExternalRouteRef, } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); +const indexRouteRef = createRouteRef(); const createComponentRouteRef = createExternalRouteRef({ optional: true, }); // plugins/catalog/src/plugin.tsx -import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; -import { rootRouteRef, createComponentRouteRef } from './routes'; +import React from 'react'; +import { + createPlugin, + createPageExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef, createComponentRouteRef } from './routes'; -const catalogEntityListPage = createPageExtension({ - defaultPath: '/' - routeRef: rootRouteRef, +const indexPage = createPageExtension({ + defaultPath: '/entities', + routeRef: indexRouteRef, loader: async () => { - const href = useRouteRef(catalogCreateComponentRouteRef); + const link = useRouteRef(createComponentRouteRef); return (

Catalog Entities

{/* Since the route is optional, rendering the link only if the href is defined */} - {href && Create Component + {link && Create Component}
); - } + }, }); -export default const createPlugin({ +export default createPlugin({ id: 'catalog', routes: { - entityList: rootRouteRef - } + index: indexRouteRef, + }, externalRoutes: { createComponent: createComponentRouteRef, }, - extensions: [catalogEntityListPage] + extensions: [indexPage], }); // index.ts @@ -371,59 +396,66 @@ For example: // plugins/catalog/src/routes.ts import { createRouteRef, - createSubRouteRef + createSubRouteRef, } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); -const detailsRouteRef = createSubRouteRef({ - parent: rootRouteRef, +const indexRouteRef = createRouteRef(); +const detailsSubRouteRef = createSubRouteRef({ + parent: indexRouteRef, path: '/details', }); // plugins/catalog/src/plugin.ts import React from 'react'; -import { Routes, Route, Link } from 'react-router-dom'; -import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; -import { rootRouteRef, detailsRouteRef } from './routes.ts'; +import { Routes, Route, useLocation } from 'react-router-dom'; +import { + createPlugin, + createPageExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef, detailsSubRouteRef } from './routes.ts'; const DetailsPage = () => (
-

Entity Details

+

Details Sub Page

); -const rootPage = createPageExtension({ - defaultPath: '/', - routeRef: rootRouteRef, +const indexPage = createPageExtension({ + defaultPath: '/entities', + routeRef: indexRouteRef, loader: async () => { const Component = () => { - const to = useRouteRef(detailsRouteRef)(); + const { pathname } = useLocation(); + const indexPath = useRouteRef(indexRouteRef)(); + const detailsPath = useRouteRef(detailsSubRouteRef)(); return (

Index Page

- {/* Registering the details subroute */} + {/* Linking to the index route */} + {pathname === detailsPath && Hide details} + {/* Linking to the details sub route */} + {pathname === indexPath && Show details} + {/* Registering the details sub route */} - } /> + } /> - {/* Linking to the details subroute */} - Details Subpage
); }; return ; }, - } }); export default createPlugin({ id: 'catalog', routes: { - root: rootRouteRef, - details: detailsRouteRef, + index: indexRouteRef, + details: detailsSubRouteRef, }, - extensions: [rootPage] + extensions: [indexPage], }); // plugins/catalog/src/index.ts @@ -439,50 +471,81 @@ import { createSubRouteRef, } from '@backstage/frontend-plugin-api'; -const rootRouteRef = createRouteRef(); -const detailsRouteRef = createSubRouteRef({ - parent: rootRouteRef, - path: '/details/:name/:namespace/:kind', +const indexRouteRef = createRouteRef(); +const detailsSubRouteRef = createSubRouteRef({ + parent: indexRouteRef, + path: '/:name/:namespace/:kind', }); // plugins/catalog/src/plugin.ts import React from 'react'; -import { Routes, Route, Link } from 'react-router-dom'; +import { Routes, Route, Link, useParams, useLocation } from 'react-router-dom'; import { createPlugin, createPageExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { rootRouteRef, detailsRouteRef } from './routes.ts'; +import { indexRouteRef, detailsSubRouteRef } from './routes.ts'; -const DetailsPage = () => ( -
-

Entity Details

- {/* can get the entity ref from URL and load the entity data here */} -
-); +const DetailsPage = () => { + // Getting the parameters from the URL + const params = useParams(); + return ( +
+

Details Sub Page

+
    +
  • Kind: {params.kind}
  • +
  • Namespace: {params.namespace}
  • +
  • Name: {params.name}
  • +
+
+ ); +}; -const rootPage = createPageExtension({ - defaultPath: '/subroutes', - routeRef: rootRouteRef, +const indexPage = createPageExtension({ + defaultPath: '/entities', + routeRef: indexRouteRef, loader: async () => { const Component = () => { - const detailsRoutePath = useRouteRef(detailsRouteRef)({ + const { pathname } = useLocation(); + const indexPath = useRouteRef(indexRouteRef)(); + const detailsPath = useRouteRef(detailsSubRouteRef)({ // Setting the details subroute params - name: 'entity1', - namespace: 'default', kind: 'component', + namespace: 'default', + name: 'foo', }); return (

Index Page

+ + + + + + + + + + + + + +
NameDetails
Foo + {/* Linking to the index route */} + {pathname === detailsPath && ( + Hide details + )} + {/* Linking to the details sub route */} + {pathname === indexPath && ( + Show details + )} +
{/* Registering the details subroute */} - } /> + } /> - {/* Linking to the details subroute */} - Entity 1 Details Subpage
); }; @@ -494,10 +557,10 @@ const rootPage = createPageExtension({ export default createPlugin({ id: 'catalog', routes: { - root: rootRouteRef, - details: detailsRouteRef, + index: indexRouteRef, + details: detailsSubRouteRef, }, - extensions: [rootPage], + extensions: [indexPage], }); // plugins/catalog/src/index.ts From 394b286aa56cd6c9262d5fc47dec5b83abd918ce Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 19:33:44 +0100 Subject: [PATCH 19/42] docs(frontend-system): remove parameterised external route note Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index f4cebe3dd1..8ffecaa827 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -287,7 +287,6 @@ export default createPlugin({ export { default } from './plugin'; ``` -On important thing to highlight is that it is currently not possible to have parameterized `ExternalRouteRefs`, or to bind an external route to a parameterized route, although this may be added in the future if needed. Now let's move on and configure the app to point to the Scaffolder create component page when the catalog create component ref be used. From 007003719a1bc82452744d6c38bf6e92627a01f3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 19:36:30 +0100 Subject: [PATCH 20/42] docs(frontend-system): remove redundant page name comment Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 8ffecaa827..caa93eaf67 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -134,7 +134,6 @@ import { import { rootRouteRef, detailsRouteRef } from './routes'; const indexPage = createPageExtension({ - // Ommiting name since it is the root page defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { @@ -287,7 +286,6 @@ export default createPlugin({ export { default } from './plugin'; ``` - Now let's move on and configure the app to point to the Scaffolder create component page when the catalog create component ref be used. ### Binding External Route References From 6f1016a126cf7fff8b33341421eb9813b09f182c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 11 Jan 2024 19:55:46 +0100 Subject: [PATCH 21/42] docs(frontend-system): more routes adjustments Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 216 ++++++++++++------ 1 file changed, 151 insertions(+), 65 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index caa93eaf67..0fe2a8cc85 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -18,15 +18,15 @@ The frontend system isn't a single API surface. It is a collection of patterns, ## Route References -In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract paths in a the Backstage app, and these paths can be configured both at the plugin level (by plugin developers) and at the instance level (by application integrators). +In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at instance level (by application integrators). -Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at the app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, all that path can be changed, so app integrators can set a custom path to a route whenever they like to (more information in the following sessions). +Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sessions). There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each. Keep reading 🙂! ### Creating a Route Reference -Route references, also known as plugins' index pages, are created as follows: +Route references, also known as plugins' index pages or regular routes, are created as follows: ```tsx // plugins/catalog/src/routes.ts @@ -37,20 +37,6 @@ export const indexRouteRef = createRouteRef(); Note that you almost always want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. -### Route Path Parameters - -The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a kind, namespace and name `params`, like in this path `/entities/:name/:namespace/:kind`: - -```tsx -// plugins/catalog/src/routes.ts -import { createRouteRef } from '@backstage/frontend-plugin-api'; - -export const detailsRouteRef = createRouteRef({ - // The parameters that must be included in the path of this route reference - params: ['namespace', 'name', 'kind'], -}); -``` - ### Providing Route References to Plugins Route refs do not have any behavior, in other words, they are an opaque type that represents route targets in an app, which are bound to specific paths at runtime, but they provide a level of indirection to help mix together different plugins that otherwise wouldn't know how to route to each other. @@ -71,12 +57,12 @@ import { } from '@backstage/frontend-plugin-api'; import { indexRouteRef } from './routes'; -const indexPage = createPageExtension({ +// The `name` option is omitted since this is the index page +const catalogIndexPage = createPageExtension({ // [2] - // The `name` option is omitted since this is the index page defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () =>
Root Page
, + loader: async () =>
Index Page
, }); export default createPlugin({ @@ -85,7 +71,7 @@ export default createPlugin({ routes: { index: indexRouteRef, }, - extensions: [indexPage], + extensions: [catalogIndexPage], }); // plugins/catalog/src/index.ts @@ -98,7 +84,21 @@ We have completed our journey of creating a plugin page route. This is what the - [2] We associate our route reference with our page by providing it as an option during creation of the page extension. - [3] Finally, our plugin provides both routes and extensions. -It may be unclear why we need to pass the route to the plugin once it has already been passed to the extension. It's a good point, and the explanation can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. +It may be unclear why we need to pass the route to the plugin once it has already been passed to the extension, but the explanation can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. + +### Route Path Parameters + +The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` params, like in this path `/entities/:kind/:namespace/:name`: + +```tsx +// plugins/catalog/src/routes.ts +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +export const detailsRouteRef = createRouteRef({ + // The parameters that must be included in the path of this route reference + params: ['kind', 'namespace', 'name'], +}); +``` ### Using a Route Reference @@ -116,7 +116,7 @@ export const indexRouteRef = createRouteRef(); export const detailsRouteRef = createRouteRef({ // The parameters that must be included in the path of this route reference - params: ['namespace', 'name', 'kind'], + params: ['kind', 'namespace', 'name'], }); ``` @@ -133,12 +133,13 @@ import { } from '@backstage/frontend-plugin-api'; import { rootRouteRef, detailsRouteRef } from './routes'; -const indexPage = createPageExtension({ +const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { const Component = () => { - const href = useRouteRef(detailsRouteRef)({ + const detailsLink = useRouteRef(detailsRouteRef); + const detailsPath = detailsLink({ kind: 'component', namespace: 'default', name: 'foo', @@ -147,7 +148,7 @@ const indexPage = createPageExtension({ return (

Index Page

- Entity Foo + See details
); }; @@ -156,8 +157,10 @@ const indexPage = createPageExtension({ }, }); -const detailsPage = createPageExtension({ +const catalogDetailsPage = createPageExtension({ name: 'details', + // It is important to mention here if an integrator configures a different path for this page via config file + // It will be their responsibility to make sure that it contains the same parameters, although they don't necessarily need to be in the same order. defaultPath: '/entities/:namespace/:kind/:name', routeRef: detailsRouteRef, loader: async () => { @@ -185,18 +188,18 @@ export default createPlugin({ index: indexRouteRef, details: detailsRouteRef, }, - extensions: [indexPage, detailsPage], + extensions: [catalogIndexPage, catalogDetailsPage], }); // plugins/catalog/src/index.ts export { default } from './plugin'; ``` -During runtime, we used a hook `useRouteRef` to get the path to the details page. Because we are linking to pages of the same plugin, we are currently accessing the reference directly, but in the following sections, you will see how to link to pages of different plugins. +During runtime, in the index page, we used a hook `useRouteRef` to get the path to the details page. Because we are linking to pages of the same plugin, we are currently accessing the reference directly, but in the following sections, you will see how to link to pages of different plugins. ## External Router References -Now let's assume that we want to link from the Catalog entity list page to the Scaffolder create component page. We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. +Now let's assume that we want to link from the Catalog entities list page to the Scaffolder create component page. We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this: @@ -209,39 +212,67 @@ import { createExternalRouteRef, } from '@backstage/frontend-plugin-api'; -const indexRouteRef = createRouteRef(); -const createComponentRouteRef = createExternalRouteRef(); +export const indexRouteRef = createRouteRef(); +export const detailsRouteRef = createRouteRef({ + // The parameters that must be included in the path of this route reference + params: ['kind', 'namespace', 'name'], +}); +export const createComponentRouteRef = createExternalRouteRef(); // plugins/catalog/src/plugin.tsx import React from 'react'; import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; -import { indexRouteRef, createComponentRouteRef } from './routes'; +import { indexRouteRef, detailsRouteRef, createComponentRouteRef } from './routes'; -const indexPage = createPageExtension({ +const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { - const href = useRouteRef(createComponentRouteRef)(); + const createComponentLink = useRouteRef(createComponentRouteRef); return (
-

Catalog Entities

+

Index Page

{/* Linking to a create component page without direct reference */} - Create Component + Create Component
); } }); +const catalogDetailsPage = createPageExtension({ + name: 'details', + defaultPath: '/entities/:namespace/:kind/:name', + routeRef: detailsRouteRef, + loader: async () => { + const Component = () => { + // Getting the parameters from the URL + const params = useParams(); + return ( +
+

Details Page

+
    +
  • Kind: {params.kind}
  • +
  • Namespace: {params.namespace}
  • +
  • Name: {params.name}
  • +
+
+ ); + }; + return ; + }, +}); + export default createPlugin({ id: 'catalog', routes: { index: indexRouteRef, + details: detailsRouteRef, } externalRoutes: { createComponent: createComponentRouteRef, }, - extensions: [indexPage] + extensions: [catalogIndexPage] }); // plugins/catalog/src/index.ts @@ -254,7 +285,7 @@ _Scaffolder Plugin_ // plugins/scaffolder/src/routes.ts import { createRouteRef } from '@backstage/frontend-plugin-api'; -const indexRouteRef = createRouteRef(); +export const indexRouteRef = createRouteRef(); // plugins/scaffolder/src/plugin.tsx import React from 'react'; @@ -264,7 +295,7 @@ import { } from '@backstage/frontend-plugin-api'; import { indexRouteRef } from './routes'; -const indexPage = createPageExtension({ +const scaffolderIndexPage = createPageExtension({ defaultPath: '/create', routeRef: indexRouteRef, loader: async () => ( @@ -279,14 +310,62 @@ export default createPlugin({ routes: { index: indexRouteRef, }, - extensions: [indexPage], + extensions: [scaffolderIndexPage], }); // plugins/scaffolder/src/index.ts export { default } from './plugin'; ``` -Now let's move on and configure the app to point to the Scaffolder create component page when the catalog create component ref be used. +One last thing is that external routes can also be parametrize, so if Scaffolder wants to redirect to the Catalog entity details page whenever a new component is created, the Scaffolder plugin can also expose a external route like this: + +```ts +// plugins/scaffolder/src/routes.ts +import { + createRouteRef, + createExternalRouteRef, +} from '@backstage/frontend-plugin-api'; + +export const indexRouteRef = createRouteRef(); +export const entityDetailsExternalRouteRef = createExternalRouteRef({ + // The parameters that must be included in the path of this route reference + params: ['kind', 'namespace', 'name'], +}); + +// plugins/scaffolder/src/plugin.tsx +import React from 'react'; +import { + createPlugin, + createPageExtension, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef, entityDetailsExternalRouteRef } from './routes'; + +const scaffolderIndexPage = createPageExtension({ + defaultPath: '/create', + routeRef: indexRouteRef, + loader: async () => ( +
+

Create Component

+
+ ), +}); + +export default createPlugin({ + id: 'scaffolder', + routes: { + index: indexRouteRef, + }, + externalRoutes: { + entityDetails: entityDetailsExternalRouteRef, + }, + extensions: [scaffolderIndexPage], +}); + +// plugins/scaffolder/src/index.ts +export { default } from './plugin'; +``` + +Now let's move on and configure the app to point to the Scaffolder create component page when the Catalog create component ref be used and also to poin to the Catalog entity details page when the Scaffolder entity details ref be used. ### Binding External Route References @@ -300,6 +379,7 @@ app: routes: bindings: plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index + plugin.scaffolder.externalRoutes.entityDetails: plugin.catalog.routes.details ``` Or via code, in the file where the app is created: @@ -315,6 +395,9 @@ const app = createApp({ bind(catalog.externalRoutes, { createComponent: scaffolder.routes.createComponent, }); + bind(scaffolder.externalRoutes, { + entityDetails: catalog.routes.details, + }); }, }); @@ -325,11 +408,11 @@ Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This is a new convention that was introduced to provide better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. -Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters.parameters. +Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters. ### Optional External Route References -It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used used to decide whether a certain link should be displayed or if an action should be taken: +It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: ```tsx // plugins/catalog/src/routes.ts @@ -338,8 +421,8 @@ import { createExternalRouteRef, } from '@backstage/frontend-plugin-api'; -const indexRouteRef = createRouteRef(); -const createComponentRouteRef = createExternalRouteRef({ +export const indexRouteRef = createRouteRef(); +export const createComponentRouteRef = createExternalRouteRef({ optional: true, }); @@ -352,17 +435,17 @@ import { } from '@backstage/frontend-plugin-api'; import { indexRouteRef, createComponentRouteRef } from './routes'; -const indexPage = createPageExtension({ +const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { - const link = useRouteRef(createComponentRouteRef); + const createComponentLink = useRouteRef(createComponentRouteRef); return (
-

Catalog Entities

+

Index Page

{/* Since the route is optional, rendering the link only if the href is defined */} - {link && Create Component} + {link && Create Component}
); }, @@ -376,7 +459,7 @@ export default createPlugin({ externalRoutes: { createComponent: createComponentRouteRef, }, - extensions: [indexPage], + extensions: [catalogIndexPage], }); // index.ts @@ -396,8 +479,8 @@ import { createSubRouteRef, } from '@backstage/frontend-plugin-api'; -const indexRouteRef = createRouteRef(); -const detailsSubRouteRef = createSubRouteRef({ +export const indexRouteRef = createRouteRef(); +export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, path: '/details', }); @@ -418,22 +501,22 @@ const DetailsPage = () => (
); -const indexPage = createPageExtension({ +const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { const Component = () => { const { pathname } = useLocation(); - const indexPath = useRouteRef(indexRouteRef)(); - const detailsPath = useRouteRef(detailsSubRouteRef)(); + const indexLink = useRouteRef(indexRouteRef); + const detailsLink = useRouteRef(detailsSubRouteRef); return (

Index Page

{/* Linking to the index route */} - {pathname === detailsPath && Hide details} + {pathname === detailsLink() && Hide details} {/* Linking to the details sub route */} - {pathname === indexPath && Show details} + {pathname === indexLink() && Show details} {/* Registering the details sub route */} } /> @@ -452,7 +535,7 @@ export default createPlugin({ index: indexRouteRef, details: detailsSubRouteRef, }, - extensions: [indexPage], + extensions: [catalogIndexPage], }); // plugins/catalog/src/index.ts @@ -468,8 +551,8 @@ import { createSubRouteRef, } from '@backstage/frontend-plugin-api'; -const indexRouteRef = createRouteRef(); -const detailsSubRouteRef = createSubRouteRef({ +export const indexRouteRef = createRouteRef(); +export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, path: '/:name/:namespace/:kind', }); @@ -499,14 +582,17 @@ const DetailsPage = () => { ); }; -const indexPage = createPageExtension({ +const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, loader: async () => { const Component = () => { const { pathname } = useLocation(); - const indexPath = useRouteRef(indexRouteRef)(); - const detailsPath = useRouteRef(detailsSubRouteRef)({ + const indexLink = useRouteRef(indexRouteRef)(); + const detailsLink = useRouteRef(detailsSubRouteRef); + + const indexPath = indexLink(); + const detailsPath = detailsLink({ // Setting the details subroute params kind: 'component', namespace: 'default', @@ -557,7 +643,7 @@ export default createPlugin({ index: indexRouteRef, details: detailsSubRouteRef, }, - extensions: [indexPage], + extensions: [catalogIndexPage], }); // plugins/catalog/src/index.ts From 07a79c695c147b2361dbcad62379083fc1927d74 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 10:32:54 +0100 Subject: [PATCH 22/42] docs(frontend-system): use promise resolve on the route examples Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 149 +++++++++++------- 1 file changed, 96 insertions(+), 53 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 0fe2a8cc85..723649d7af 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -41,7 +41,7 @@ Note that you almost always want to create the route references themselves in a Route refs do not have any behavior, in other words, they are an opaque type that represents route targets in an app, which are bound to specific paths at runtime, but they provide a level of indirection to help mix together different plugins that otherwise wouldn't know how to route to each other. -The code snippet of the previous section does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension. When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the the extension. Here's what we need to do: +The previous section code snippet does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension. When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the extension. Here's what we need to do: ```tsx // plugins/catalog/src/routes.ts @@ -62,7 +62,8 @@ const catalogIndexPage = createPageExtension({ // [2] defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () =>
Index Page
, + // Promise.resolve is a replacement for `import(...)` + loader: () => Promise.resolve(
Index Page
), }); export default createPlugin({ @@ -88,7 +89,7 @@ It may be unclear why we need to pass the route to the plugin once it has alread ### Route Path Parameters -The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` params, like in this path `/entities/:kind/:namespace/:name`: +The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a kind, namespace and name `params`, like in this path `/entities/:kind/:namespace/:name`: ```tsx // plugins/catalog/src/routes.ts @@ -136,7 +137,7 @@ import { rootRouteRef, detailsRouteRef } from './routes'; const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () => { + loader: () => { const Component = () => { const detailsLink = useRouteRef(detailsRouteRef); const detailsPath = detailsLink({ @@ -153,7 +154,7 @@ const catalogIndexPage = createPageExtension({ ); }; - return ; + return Promise.resolve(); }, }); @@ -163,7 +164,7 @@ const catalogDetailsPage = createPageExtension({ // It will be their responsibility to make sure that it contains the same parameters, although they don't necessarily need to be in the same order. defaultPath: '/entities/:namespace/:kind/:name', routeRef: detailsRouteRef, - loader: async () => { + loader: () => { const Component = () => { // Getting the parameters from the URL const params = useParams(); @@ -178,7 +179,8 @@ const catalogDetailsPage = createPageExtension({
); }; - return ; + + return Promise.resolve(); }, }); @@ -221,30 +223,42 @@ export const createComponentRouteRef = createExternalRouteRef(); // plugins/catalog/src/plugin.tsx import React from 'react'; -import { createPlugin, createPageExtension, useRouteRef } from '@backstage/frontend-plugin-api'; -import { indexRouteRef, detailsRouteRef, createComponentRouteRef } from './routes'; +import { + createPlugin, + createPageExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import { + indexRouteRef, + detailsRouteRef, + createComponentRouteRef, +} from './routes'; const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () => { - const createComponentLink = useRouteRef(createComponentRouteRef); + loader: () => { + const Component = () => { + const createComponentLink = useRouteRef(createComponentRouteRef); - return ( -
-

Index Page

- {/* Linking to a create component page without direct reference */} - Create Component -
- ); - } + return ( +
+

Index Page

+ {/* Linking to a create component page without direct reference */} + Create Component +
+ ); + }; + + return Promise.resolve(); + }, }); const catalogDetailsPage = createPageExtension({ name: 'details', defaultPath: '/entities/:namespace/:kind/:name', routeRef: detailsRouteRef, - loader: async () => { + loader: () => { const Component = () => { // Getting the parameters from the URL const params = useParams(); @@ -259,7 +273,8 @@ const catalogDetailsPage = createPageExtension({
); }; - return ; + + return Promise.resolve(); }, }); @@ -268,11 +283,11 @@ export default createPlugin({ routes: { index: indexRouteRef, details: detailsRouteRef, - } + }, externalRoutes: { createComponent: createComponentRouteRef, }, - extensions: [catalogIndexPage] + extensions: [catalogIndexPage, catalogDetailsPage], }); // plugins/catalog/src/index.ts @@ -298,11 +313,12 @@ import { indexRouteRef } from './routes'; const scaffolderIndexPage = createPageExtension({ defaultPath: '/create', routeRef: indexRouteRef, - loader: async () => ( -
-

Create Component

-
- ), + loader: () => + Promise.resolve( +
+

Create Component

+
, + ), }); export default createPlugin({ @@ -317,7 +333,7 @@ export default createPlugin({ export { default } from './plugin'; ``` -One last thing is that external routes can also be parametrize, so if Scaffolder wants to redirect to the Catalog entity details page whenever a new component is created, the Scaffolder plugin can also expose a external route like this: +The Scaffolder plugin can also expose an external route that redirects to the Catalog entity details page whenever a new component is created, such as: ```ts // plugins/scaffolder/src/routes.ts @@ -327,7 +343,7 @@ import { } from '@backstage/frontend-plugin-api'; export const indexRouteRef = createRouteRef(); -export const entityDetailsExternalRouteRef = createExternalRouteRef({ +export const componentDetailsExternalRouteRef = createExternalRouteRef({ // The parameters that must be included in the path of this route reference params: ['kind', 'namespace', 'name'], }); @@ -337,17 +353,37 @@ import React from 'react'; import { createPlugin, createPageExtension, + useRouteRef, } from '@backstage/frontend-plugin-api'; -import { indexRouteRef, entityDetailsExternalRouteRef } from './routes'; +import { indexRouteRef, componentDetailsExternalRouteRef } from './routes'; const scaffolderIndexPage = createPageExtension({ defaultPath: '/create', routeRef: indexRouteRef, - loader: async () => ( -
-

Create Component

-
- ), + loader: () => { + const Component = () => { + const componentDetailsLink = useRouteRef( + componentDetailsExternalRouteRef, + ); + + return ( +
+

Create Component

+ + See component details + +
+ ); + }; + + return Promise.resolve(); + }, }); export default createPlugin({ @@ -356,7 +392,7 @@ export default createPlugin({ index: indexRouteRef, }, externalRoutes: { - entityDetails: entityDetailsExternalRouteRef, + componentDetails: componentDetailsExternalRouteRef, }, extensions: [scaffolderIndexPage], }); @@ -365,7 +401,8 @@ export default createPlugin({ export { default } from './plugin'; ``` -Now let's move on and configure the app to point to the Scaffolder create component page when the Catalog create component ref be used and also to poin to the Catalog entity details page when the Scaffolder entity details ref be used. +Note that external routes can also have path parameters! +Now let's move on and configure the app to point to the Scaffolder create component page when the Catalog create component ref be used and also point to the Catalog entity details page when the Scaffolder component details ref be used. ### Binding External Route References @@ -378,8 +415,10 @@ Using the above example of the Catalog entities list page to the Scaffolder crea app: routes: bindings: + # point to the Scaffolder create component page when the Catalog create component ref be used plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index - plugin.scaffolder.externalRoutes.entityDetails: plugin.catalog.routes.details + # point to the Catalog details page when the Scaffolder component details ref be used + plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details ``` Or via code, in the file where the app is created: @@ -396,7 +435,7 @@ const app = createApp({ createComponent: scaffolder.routes.createComponent, }); bind(scaffolder.externalRoutes, { - entityDetails: catalog.routes.details, + componentDetails: catalog.routes.details, }); }, }); @@ -438,16 +477,20 @@ import { indexRouteRef, createComponentRouteRef } from './routes'; const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () => { - const createComponentLink = useRouteRef(createComponentRouteRef); + loader: () => { + const Component = () => { + const createComponentLink = useRouteRef(createComponentRouteRef); - return ( -
-

Index Page

- {/* Since the route is optional, rendering the link only if the href is defined */} - {link && Create Component} -
- ); + return ( +
+

Index Page

+ {/* Since the route is optional, rendering the link only if the href is defined */} + {link && Create Component} +
+ ); + }; + + return Promise.resolve(); }, }); @@ -504,7 +547,7 @@ const DetailsPage = () => ( const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () => { + loader: () => { const Component = () => { const { pathname } = useLocation(); const indexLink = useRouteRef(indexRouteRef); @@ -525,7 +568,7 @@ const catalogIndexPage = createPageExtension({ ); }; - return ; + return Promise.resolve(); }, }); @@ -585,7 +628,7 @@ const DetailsPage = () => { const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: async () => { + loader: () => { const Component = () => { const { pathname } = useLocation(); const indexLink = useRouteRef(indexRouteRef)(); @@ -633,7 +676,7 @@ const catalogIndexPage = createPageExtension({ ); }; - return ; + return Promise.resolve(); }, }); From 93fd34024280f612b11e024dc5bedc3ca06e44dc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 14:24:44 +0100 Subject: [PATCH 23/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 723649d7af..305e200a24 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -18,7 +18,7 @@ The frontend system isn't a single API surface. It is a collection of patterns, ## Route References -In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at instance level (by application integrators). +In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sessions). From c16323db068dd05f8f1c8d38e7fe29fb6da73c15 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 14:24:52 +0100 Subject: [PATCH 24/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 305e200a24..ea839f27b4 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -20,7 +20,7 @@ The frontend system isn't a single API surface. It is a collection of patterns, In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). -Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sessions). +Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sections). There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each. Keep reading 🙂! From ef87f165992be5a21dcaa056b4ad327a7832e94b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 14:25:43 +0100 Subject: [PATCH 25/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index ea839f27b4..4b8b98a3e7 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -26,7 +26,7 @@ There are 3 types of route references: regular route, sub route, and external ro ### Creating a Route Reference -Route references, also known as plugins' index pages or regular routes, are created as follows: +Route references, also known as "absolute" or "regular" routes, are created as follows: ```tsx // plugins/catalog/src/routes.ts From ed670cec063f9ab7eaf5349b6113836f8670b08e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 14:25:58 +0100 Subject: [PATCH 26/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 4b8b98a3e7..15bfe615ac 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -445,7 +445,7 @@ export default app.createRoot(); Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. -Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This is a new convention that was introduced to provide better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. +Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This provides better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters. From fa379b6333b60a7bd3987d70109f1c77f5562539 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:12:43 +0100 Subject: [PATCH 27/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 15bfe615ac..2f8d3a5b7a 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -421,7 +421,7 @@ app: plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details ``` -Or via code, in the file where the app is created: +We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods: ```tsx // packages/app/src/App.tsx From 6f4fbb06ea9d473c6ef697e75571918e4f16b198 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:50:48 +0100 Subject: [PATCH 28/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 2f8d3a5b7a..4dd06ffb90 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -35,7 +35,7 @@ import { createRouteRef } from '@backstage/frontend-plugin-api'; export const indexRouteRef = createRouteRef(); ``` -Note that you almost always want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. +Note that you often want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. ### Providing Route References to Plugins From 1c7ca6f643ab389e2f601c63eb9c04b6956b2175 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:51:10 +0100 Subject: [PATCH 29/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 4dd06ffb90..99438dc868 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -39,7 +39,7 @@ Note that you often want to create the route references themselves in a differen ### Providing Route References to Plugins -Route refs do not have any behavior, in other words, they are an opaque type that represents route targets in an app, which are bound to specific paths at runtime, but they provide a level of indirection to help mix together different plugins that otherwise wouldn't know how to route to each other. +Route refs do not have any behavior themselves. They are an opaque value that represents route targets in an app, which are bound to specific paths at runtime. Their role is to provide a level of indirection to help link together different pages that otherwise wouldn't know how to route to each other. The previous section code snippet does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension. When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the extension. Here's what we need to do: From 88cf8a665ea82f60987b9e84e0c8367d644a8afe Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:51:56 +0100 Subject: [PATCH 30/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 99438dc868..efdebaebd0 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -85,7 +85,7 @@ We have completed our journey of creating a plugin page route. This is what the - [2] We associate our route reference with our page by providing it as an option during creation of the page extension. - [3] Finally, our plugin provides both routes and extensions. -It may be unclear why we need to pass the route to the plugin once it has already been passed to the extension, but the explanation can be found in the (Binding External Route References)[#building-external-route-references] section, wait a bit, keep reading and you'll understand why. +It may be unclear why we need to pass the route to the plugin when it has already been passed to the extension. We do that to make it possible for other plugins to route to our page, which is explained in detail in the (Binding External Route References)[#building-external-route-references] section. ### Route Path Parameters From c01052ec3278f349c600c3486fca454efd29b00d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:52:16 +0100 Subject: [PATCH 31/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index efdebaebd0..b069743ef9 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -89,7 +89,7 @@ It may be unclear why we need to pass the route to the plugin when it has alread ### Route Path Parameters -The referenced route can also accepts `params`. Here is how you create a reference for a route that requires a kind, namespace and name `params`, like in this path `/entities/:kind/:namespace/:name`: +Route references optionally accept a `params` option, which will require the listed parameter names to be present in the route path. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` parameters, like in this path `/entities/:kind/:namespace/:name`: ```tsx // plugins/catalog/src/routes.ts From 6d88c626c0667109001582328d279363506bac52 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:52:32 +0100 Subject: [PATCH 32/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index b069743ef9..b4e562aca9 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -103,7 +103,7 @@ export const detailsRouteRef = createRouteRef({ ### Using a Route Reference -You can link to the routes from other pages in the same plugin or you can also link between different plugins pages. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. +Route references can be used to link to page in the same plugin, or to pages in a different plugins. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. Alright, let's presume that we have a plugin that renders two different pages, and these pages link to each other. From e083d88a23256e7cfccc10491f598902f4c526e3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:52:58 +0100 Subject: [PATCH 33/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index b4e562aca9..3e8c29ddfb 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -105,7 +105,7 @@ export const detailsRouteRef = createRouteRef({ Route references can be used to link to page in the same plugin, or to pages in a different plugins. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. -Alright, let's presume that we have a plugin that renders two different pages, and these pages link to each other. +Let's presume that we have a plugin that renders two different pages, and these pages link to each other. First lets create the routes references: From 513919d2669b0c8a30985194f1f340250ab3f47e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:54:06 +0100 Subject: [PATCH 34/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 3e8c29ddfb..a07df23d70 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -402,7 +402,7 @@ export { default } from './plugin'; ``` Note that external routes can also have path parameters! -Now let's move on and configure the app to point to the Scaffolder create component page when the Catalog create component ref be used and also point to the Catalog entity details page when the Scaffolder component details ref be used. +Now let's move on and configure the app to resolve these external routes, so that the Scaffolder links to the Catalog entity page, and the Catalog links to the Scaffolder page. ### Binding External Route References From 9487a419139fbbdb79302fff1e42ee2eb40c6260 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 15:55:12 +0100 Subject: [PATCH 35/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index a07df23d70..c1fd424ae3 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -415,9 +415,9 @@ Using the above example of the Catalog entities list page to the Scaffolder crea app: routes: bindings: - # point to the Scaffolder create component page when the Catalog create component ref be used + # point to the Scaffolder create component page when the Catalog create component ref is used plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index - # point to the Catalog details page when the Scaffolder component details ref be used + # point to the Catalog details page when the Scaffolder component details ref is used plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details ``` From 9f949e27ba57cb86d71a6ca6f1bb4c625aced087 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 12 Jan 2024 17:03:22 +0100 Subject: [PATCH 36/42] docs(routing): try to reduce code snippets Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 664 +++++------------- 1 file changed, 191 insertions(+), 473 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index c1fd424ae3..0aa6980713 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -10,46 +10,38 @@ description: Frontend routes ## Introduction -This page describes the frontend system that helps bring together content from a multitude of plugins into one Backstage application. +In Backstage, plug-in boundaries and connections must be clearly defined. The app should allow navigation between plug-ins while isolating faults within them. -The core principle of the frontend system is that plugins should have clear boundaries and connections. It should isolate crashes within a plugin, but allow navigation between them. It should allow for plugins to be loaded only when needed, and enable plugins to provide extension points for other plugins to build upon. The frontend system is also built with an app-first mindset, prioritizing simplicity and clarity in the app over that in the plugins and core APIs. - -The frontend system isn't a single API surface. It is a collection of patterns, primitives, and APIs. At the core is the concept of extensions, which are exported by plugins for use in the app. One of the concepts is `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. +In order to address the problem outlined above, we introduced the concept of `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. ## Route References -In order to address the problem outlined above, we introduced the concept of route references. A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). +A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sections). -There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each. Keep reading 🙂! +There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each. ### Creating a Route Reference Route references, also known as "absolute" or "regular" routes, are created as follows: -```tsx -// plugins/catalog/src/routes.ts +```tsx title="plugins/catalog/src/routes.ts" showLineNumbers import { createRouteRef } from '@backstage/frontend-plugin-api'; +// Creates a route reference, which is not yet associated with any plugin page export const indexRouteRef = createRouteRef(); ``` Note that you often want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level routes.ts. This is to avoid circular imports when you use the route references from other parts of the same plugin. -### Providing Route References to Plugins - Route refs do not have any behavior themselves. They are an opaque value that represents route targets in an app, which are bound to specific paths at runtime. Their role is to provide a level of indirection to help link together different pages that otherwise wouldn't know how to route to each other. -The previous section code snippet does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension. When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the extension. Here's what we need to do: +### Providing Route References to Plugins -```tsx -// plugins/catalog/src/routes.ts -import { createRouteRef } from '@backstage/frontend-plugin-api'; +The previous section code snippet does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension: -export const indexRouteRef = createRouteRef(); // [1] - -// plugins/catalog/src/plugin.tsx +```tsx title="plugins/catalog/src/plugin.tsx" {11,17-19} showLineNumbers import React from 'react'; import { createPlugin, @@ -57,42 +49,36 @@ import { } from '@backstage/frontend-plugin-api'; import { indexRouteRef } from './routes'; -// The `name` option is omitted since this is the index page const catalogIndexPage = createPageExtension({ - // [2] + // The `name` option is omitted because this is an index page defaultPath: '/entities', routeRef: indexRouteRef, - // Promise.resolve is a replacement for `import(...)` - loader: () => Promise.resolve(
Index Page
), + loader: () => import('./components').then(m => ), }); export default createPlugin({ - // [3] id: 'catalog', routes: { index: indexRouteRef, }, extensions: [catalogIndexPage], }); - -// plugins/catalog/src/index.ts -export { default } from './plugin'; ``` -We have completed our journey of creating a plugin page route. This is what the code does: +In the example above we have associated a route ref with an plugin page. This is what the code does: -- [1] The line 1 creates a route reference, which is not yet associated with any plugin page; -- [2] We associate our route reference with our page by providing it as an option during creation of the page extension. -- [3] Finally, our plugin provides both routes and extensions. +- Line 11: Associates the `indexRouteRef` with the `catalogIndexPage` extension; +- Line 17 to 19: Provides both the `indexRouteRef` and `catalogIndexPage` via the Catalog plugin. -It may be unclear why we need to pass the route to the plugin when it has already been passed to the extension. We do that to make it possible for other plugins to route to our page, which is explained in detail in the (Binding External Route References)[#building-external-route-references] section. +When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the extension. -### Route Path Parameters +It may be unclear why we configure the routes option when creating a plugin as the route has already been passed to the extension. We do that to make it possible for other plugins to route to our page, which is explained in detail in the [binding routes](#binding-external-route-references) section. + +### Defining References with Path Parameters Route references optionally accept a `params` option, which will require the listed parameter names to be present in the route path. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` parameters, like in this path `/entities/:kind/:namespace/:name`: -```tsx -// plugins/catalog/src/routes.ts +```tsx title="plugins/catalog/src/routes.ts" {5} showLineNumbers import { createRouteRef } from '@backstage/frontend-plugin-api'; export const detailsRouteRef = createRouteRef({ @@ -105,303 +91,134 @@ export const detailsRouteRef = createRouteRef({ Route references can be used to link to page in the same plugin, or to pages in a different plugins. In this section we will cover the first scenario, if you are interested in link to a page of a different plugin, please go to the [external routes](#external-router-references) section below. -Let's presume that we have a plugin that renders two different pages, and these pages link to each other. +Suppose we are creating a plugin that renders a Catalog index page with a link to a "Foo" component details page. Here is the code for the index page: -First lets create the routes references: - -```tsx -// plugins/catalog/src/routes.ts -import { createRouteRef } from '@backstage/frontend-plugin-api'; - -export const indexRouteRef = createRouteRef(); - -export const detailsRouteRef = createRouteRef({ - // The parameters that must be included in the path of this route reference - params: ['kind', 'namespace', 'name'], -}); -``` - -Now we are ready to provide these routes via plugin extensions and link between pages: - -```tsx -// plugins/catalog/src/plugin.tsx +```tsx title="plugins/catalog/src/components/IndexPage.tsx" {6,11-15} showLineNumbers import React from 'react'; -import { useParams } from 'react-router'; -import { - createPlugin, - createPageExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { rootRouteRef, detailsRouteRef } from './routes'; +import { useRouteRef } from '@backstage/frontend-plugin-api'; +import { detailsRouteRef } from '../routes'; -const catalogIndexPage = createPageExtension({ - defaultPath: '/entities', - routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const detailsLink = useRouteRef(detailsRouteRef); - const detailsPath = detailsLink({ - kind: 'component', - namespace: 'default', - name: 'foo', - }); - - return ( -
-

Index Page

- See details -
- ); - }; - - return Promise.resolve(); - }, -}); - -const catalogDetailsPage = createPageExtension({ - name: 'details', - // It is important to mention here if an integrator configures a different path for this page via config file - // It will be their responsibility to make sure that it contains the same parameters, although they don't necessarily need to be in the same order. - defaultPath: '/entities/:namespace/:kind/:name', - routeRef: detailsRouteRef, - loader: () => { - const Component = () => { - // Getting the parameters from the URL - const params = useParams(); - return ( -
-

Details Page

-
    -
  • Kind: {params.kind}
  • -
  • Namespace: {params.namespace}
  • -
  • Name: {params.name}
  • -
-
- ); - }; - - return Promise.resolve(); - }, -}); - -export default createPlugin({ - id: 'catalog', - routes: { - index: indexRouteRef, - details: detailsRouteRef, - }, - extensions: [catalogIndexPage, catalogDetailsPage], -}); - -// plugins/catalog/src/index.ts -export { default } from './plugin'; +export const IndexPage = () => { + const getDetailsPath = useRouteRef(detailsRouteRef); + return ( +
+

Index Page

+ + See "Foo" details + +
+ ); +}; ``` -During runtime, in the index page, we used a hook `useRouteRef` to get the path to the details page. Because we are linking to pages of the same plugin, we are currently accessing the reference directly, but in the following sections, you will see how to link to pages of different plugins. +Line 6 uses a hook called `useRouteRef` to access a getter function that returns the details page path. On line 11, we call the getter, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to details the "Foo" details page. + +Let's see how the details page can get the parameters from the URL: + +```tsx title="plugins/catalog/src/components/DetailsPage.tsx" {6,11-13} showLineNumbers +import React from 'react'; +import { useRouteRefParams } from '@backstage/frontend-plugin-api'; +import { detailsRouteRef } from '../routes'; + +export const DetailsPage = () => { + const params = useRouteRefParams(detailsRouteRef); + return ( +
+

Details Page

+
    +
  • Kind: {params.kind}
  • +
  • Namespace: {params.namespace}
  • +
  • Name: {params.name}
  • +
+
+ ); +}; +``` + +On line 6, we are using the `useRouteRefParams` hook to retrieve the entity-composed id from the URL. The parameter object contains three values: kind, namespace, and name. We can display these values or call an API using them. + +Since we are linking to pages of the same package, we are using a route ref directly. However, in the following sections, you will see how to link to pages of different plugins. ## External Router References -Now let's assume that we want to link from the Catalog entities list page to the Scaffolder create component page. We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. +External routes are made for linking to a page of an external plugin. +For this section example, let's assume that we want to link from the Catalog entities list page to the Scaffolder create component page. + +We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provided little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app. We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this: -_Catalog Plugin_ +```tsx title="plugins/catalog/src/routes.ts" showLineNumbers +import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; -```tsx -// plugins/catalog/src/routes.ts -import { - createRouteRef, - createExternalRouteRef, -} from '@backstage/frontend-plugin-api'; +export const createComponentExternalRouteRef = createExternalRouteRef(); +``` -export const indexRouteRef = createRouteRef(); -export const detailsRouteRef = createRouteRef({ - // The parameters that must be included in the path of this route reference - params: ['kind', 'namespace', 'name'], -}); -export const createComponentRouteRef = createExternalRouteRef(); +External routes are also used in a similar way as regular routes: -// plugins/catalog/src/plugin.tsx +```tsx title="plugins/catalog/src/components/IndexPage.tsx" {6,10} showLineNumbers +import React from 'react'; +import { useRouteRef } from '@backstage/frontend-plugin-api'; +import { createComponentExternalRouteRef } from '../routes'; + +export const IndexPage = () => { + const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); + return ( +
+

Index Page

+ Create Component +
+ ); +}; +``` + +Given the above binding, using `useRouteRef(createComponentExternalRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. Note that there is no direct dependency between the Catalog plugin and Scaffolder, that is, we are not importing the `createComponentExternalRouteRef` from the Scaffolder package. + +Now the only thing left is to provide the page and external route via a plugin: + +```tsx title="plugins/catalog/src/plugin.tsx" {20-23} showLineNumbers import React from 'react'; import { createPlugin, createPageExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { - indexRouteRef, - detailsRouteRef, - createComponentRouteRef, -} from './routes'; +import { indexRouteRef, createComponentExternalRouteRef } from './routes'; const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const createComponentLink = useRouteRef(createComponentRouteRef); - - return ( -
-

Index Page

- {/* Linking to a create component page without direct reference */} - Create Component -
- ); - }; - - return Promise.resolve(); - }, -}); - -const catalogDetailsPage = createPageExtension({ - name: 'details', - defaultPath: '/entities/:namespace/:kind/:name', - routeRef: detailsRouteRef, - loader: () => { - const Component = () => { - // Getting the parameters from the URL - const params = useParams(); - return ( -
-

Details Page

-
    -
  • Kind: {params.kind}
  • -
  • Namespace: {params.namespace}
  • -
  • Name: {params.name}
  • -
-
- ); - }; - - return Promise.resolve(); - }, + loader: () => import('./components').then(m => ), }); export default createPlugin({ id: 'catalog', routes: { index: indexRouteRef, - details: detailsRouteRef, }, externalRoutes: { - createComponent: createComponentRouteRef, + createComponent: createComponentExternalRouteRef, }, - extensions: [catalogIndexPage, catalogDetailsPage], + extensions: [catalogIndexPage], }); - -// plugins/catalog/src/index.ts -export { default } from './plugin'; ``` -_Scaffolder Plugin_ +External routes can also have parameters. For example, if you want to link to an entity's details page from Scaffolder, you'll need to create an external route that receives the same parameters the Catalog details page expects: -```tsx -// plugins/scaffolder/src/routes.ts -import { createRouteRef } from '@backstage/frontend-plugin-api'; +```tsx title="plugins/scaffolder/src/routes.ts" {4} showLineNumbers +import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; -export const indexRouteRef = createRouteRef(); - -// plugins/scaffolder/src/plugin.tsx -import React from 'react'; -import { - createPlugin, - createPageExtension, -} from '@backstage/frontend-plugin-api'; -import { indexRouteRef } from './routes'; - -const scaffolderIndexPage = createPageExtension({ - defaultPath: '/create', - routeRef: indexRouteRef, - loader: () => - Promise.resolve( -
-

Create Component

-
, - ), -}); - -export default createPlugin({ - id: 'scaffolder', - routes: { - index: indexRouteRef, - }, - extensions: [scaffolderIndexPage], -}); - -// plugins/scaffolder/src/index.ts -export { default } from './plugin'; -``` - -The Scaffolder plugin can also expose an external route that redirects to the Catalog entity details page whenever a new component is created, such as: - -```ts -// plugins/scaffolder/src/routes.ts -import { - createRouteRef, - createExternalRouteRef, -} from '@backstage/frontend-plugin-api'; - -export const indexRouteRef = createRouteRef(); -export const componentDetailsExternalRouteRef = createExternalRouteRef({ - // The parameters that must be included in the path of this route reference +export const entityDetailsExternalRouteRef = createExternalRouteRef({ params: ['kind', 'namespace', 'name'], }); - -// plugins/scaffolder/src/plugin.tsx -import React from 'react'; -import { - createPlugin, - createPageExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { indexRouteRef, componentDetailsExternalRouteRef } from './routes'; - -const scaffolderIndexPage = createPageExtension({ - defaultPath: '/create', - routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const componentDetailsLink = useRouteRef( - componentDetailsExternalRouteRef, - ); - - return ( -
-

Create Component

- - See component details - -
- ); - }; - - return Promise.resolve(); - }, -}); - -export default createPlugin({ - id: 'scaffolder', - routes: { - index: indexRouteRef, - }, - externalRoutes: { - componentDetails: componentDetailsExternalRouteRef, - }, - extensions: [scaffolderIndexPage], -}); - -// plugins/scaffolder/src/index.ts -export { default } from './plugin'; ``` -Note that external routes can also have path parameters! Now let's move on and configure the app to resolve these external routes, so that the Scaffolder links to the Catalog entity page, and the Catalog links to the Scaffolder page. ### Binding External Route References @@ -410,8 +227,7 @@ The association of external routes is controlled by the app. Each `ExternalRoute Using the above example of the Catalog entities list page to the Scaffolder create component page, we might do something like this in the app configuration file: -```yaml -# app-config.yaml +```yaml title="app-config.yaml" {5,7} showLineNumbers app: routes: bindings: @@ -423,8 +239,7 @@ app: We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods: -```tsx -// packages/app/src/App.tsx +```tsx title="packages/app/src/App.tsx" {6-13} showLineNumbers import { createApp } from '@backstage/frontend-app-api'; import catalog from '@backstage/plugin-catalog'; import scaffolder from '@backstage/plugin-scaffolder'; @@ -443,70 +258,41 @@ const app = createApp({ export default app.createRoot(); ``` -Given the above binding, using `useRouteRef(createComponentRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. - Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This provides better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package. Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to leave flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to go the route of direct imports or even use concrete routes directly. Although there can be some benefits to using the full routing system even in internal plugins. It can help you structure your routes, and as you will see further down it also helps you manage route parameters. ### Optional External Route References -It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: +It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. -```tsx -// plugins/catalog/src/routes.ts -import { - createRouteRef, - createExternalRouteRef, -} from '@backstage/frontend-plugin-api'; +```tsx title="plugins/catalog/src/routes.ts" {4} showLineNumbers +import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; -export const indexRouteRef = createRouteRef(); -export const createComponentRouteRef = createExternalRouteRef({ +export const createComponentExternalRouteRef = createExternalRouteRef({ optional: true, }); +``` -// plugins/catalog/src/plugin.tsx +When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: + +```tsx title="plugins/catalog/src/components/IndexPage.tsx" {11} showLineNumbers import React from 'react'; -import { - createPlugin, - createPageExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { indexRouteRef, createComponentRouteRef } from './routes'; +import { useRouteRef } from '@backstage/frontend-plugin-api'; +import { createComponentExternalRouteRef } from '../routes'; -const catalogIndexPage = createPageExtension({ - defaultPath: '/entities', - routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const createComponentLink = useRouteRef(createComponentRouteRef); - - return ( -
-

Index Page

- {/* Since the route is optional, rendering the link only if the href is defined */} - {link && Create Component} -
- ); - }; - - return Promise.resolve(); - }, -}); - -export default createPlugin({ - id: 'catalog', - routes: { - index: indexRouteRef, - }, - externalRoutes: { - createComponent: createComponentRouteRef, - }, - extensions: [catalogIndexPage], -}); - -// index.ts -export { default } from './plugin'; +export const IndexPage = () => { + const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); + return ( +
+

Index Page

+ {/* Rendering the link only if the getCreateComponentPath is defined */} + {getCreateComponentPath && ( + Create Component + )} +
+ ); +}; ``` ## Sub Route References @@ -515,8 +301,7 @@ The last kind of route refs that can be created are `SubRouteRef`s, which can be For example: -```tsx -// plugins/catalog/src/routes.ts +```tsx title ="plugins/catalog/src/routes.ts" {4,7} showLineNumbers import { createRouteRef, createSubRouteRef, @@ -527,91 +312,65 @@ export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, path: '/details', }); - -// plugins/catalog/src/plugin.ts -import React from 'react'; -import { Routes, Route, useLocation } from 'react-router-dom'; -import { - createPlugin, - createPageExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { indexRouteRef, detailsSubRouteRef } from './routes.ts'; - -const DetailsPage = () => ( -
-

Details Sub Page

-
-); - -const catalogIndexPage = createPageExtension({ - defaultPath: '/entities', - routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const { pathname } = useLocation(); - const indexLink = useRouteRef(indexRouteRef); - const detailsLink = useRouteRef(detailsSubRouteRef); - - return ( -
-

Index Page

- {/* Linking to the index route */} - {pathname === detailsLink() && Hide details} - {/* Linking to the details sub route */} - {pathname === indexLink() && Show details} - {/* Registering the details sub route */} - - } /> - -
- ); - }; - - return Promise.resolve(); - }, -}); - -export default createPlugin({ - id: 'catalog', - routes: { - index: indexRouteRef, - details: detailsSubRouteRef, - }, - extensions: [catalogIndexPage], -}); - -// plugins/catalog/src/index.ts -export { default } from './plugin'; ``` -Subroutes can also receive parameters, here is an example: +There are substantial differences between creating subroutes and regular or external routes because subroutes are associated with regular routes, and the sub route path must be specified. The path string must include the parameters if this sub route has them: -```tsx -// plugins/catalog/src/routes.ts -import { - createRouteRef, - createSubRouteRef, -} from '@backstage/frontend-plugin-api'; - -export const indexRouteRef = createRouteRef(); +```tsx title ="plugins/catalog/src/routes.ts" {4} showLineNumbers +// Omitting rest of the previous example file export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, path: '/:name/:namespace/:kind', }); +``` -// plugins/catalog/src/plugin.ts +Using subroutes in a page extension is as simple as this: + +```tsx title="plugins/catalog/src/components/IndexPage.ts" showLineNumbers import React from 'react'; -import { Routes, Route, Link, useParams, useLocation } from 'react-router-dom'; -import { - createPlugin, - createPageExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; -import { indexRouteRef, detailsSubRouteRef } from './routes.ts'; +import { Routes, Route, useLocation } from 'react-router-dom'; +import { useRouteRef } from '@backstage/frontend-plugin-api'; +import { indexRouteRef, detailsSubRouteRef } from '../routes'; +import { DetailsPage } from './DetailsPage'; -const DetailsPage = () => { - // Getting the parameters from the URL +export const IndexPage = () => { + const { pathname } = useLocation(); + const getIndexPath = useRouteRef(indexRouteRef); + const getDetailsPath = useRouteRef(detailsSubRouteRef); + return ( +
+

Index Page

+ {/* Linking to the details sub route */} + {pathname === getIndexPath() ? ( + // Setting the details sub route params + + Show details + + ) : ( + Hide details + )} + {/* Registering the details sub route */} + + } /> + +
+ ); +}; +``` + +This is how you can get the parameters of a sub route URL: + +```tsx title="plugins/catalog/src/components/DetailsPage.ts" {5} showLineNumbers +import React from 'react'; +import { useParams } from 'react-router-dom'; + +export const DetailsPage = () => { const params = useParams(); return (
@@ -624,60 +383,22 @@ const DetailsPage = () => {
); }; +``` + +Finally, see how a plugin can provide subroutes: + +```tsx title="plugins/catalog/src/plugin.ts" {18} showLineNumbers +import React from 'react'; +import { + createPlugin, + createPageExtension, +} from '@backstage/frontend-plugin-api'; +import { indexRouteRef, detailsSubRouteRef } from './routes'; const catalogIndexPage = createPageExtension({ defaultPath: '/entities', routeRef: indexRouteRef, - loader: () => { - const Component = () => { - const { pathname } = useLocation(); - const indexLink = useRouteRef(indexRouteRef)(); - const detailsLink = useRouteRef(detailsSubRouteRef); - - const indexPath = indexLink(); - const detailsPath = detailsLink({ - // Setting the details subroute params - kind: 'component', - namespace: 'default', - name: 'foo', - }); - - return ( -
-

Index Page

- - - - - - - - - - - - - -
NameDetails
Foo - {/* Linking to the index route */} - {pathname === detailsPath && ( - Hide details - )} - {/* Linking to the details sub route */} - {pathname === indexPath && ( - Show details - )} -
- {/* Registering the details subroute */} - - } /> - -
- ); - }; - - return Promise.resolve(); - }, + loader: () => import('./components').then(m => ), }); export default createPlugin({ @@ -688,7 +409,4 @@ export default createPlugin({ }, extensions: [catalogIndexPage], }); - -// plugins/catalog/src/index.ts -export { default } from './plugin'; ``` From f7a737e52f678a921f975cb3a97b2587f7f726cf Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 14:19:29 +0100 Subject: [PATCH 37/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 0aa6980713..631ea87f97 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -117,7 +117,7 @@ export const IndexPage = () => { }; ``` -Line 6 uses a hook called `useRouteRef` to access a getter function that returns the details page path. On line 11, we call the getter, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to details the "Foo" details page. +We use the `useRouteRef` hook to create a link generator function that returns the details page path. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to details the "Foo" details page. Let's see how the details page can get the parameters from the URL: From 6b022bf6c76e30073f2dfe547d98def5602c11c5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 14:20:37 +0100 Subject: [PATCH 38/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 631ea87f97..2585053683 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -10,9 +10,9 @@ description: Frontend routes ## Introduction -In Backstage, plug-in boundaries and connections must be clearly defined. The app should allow navigation between plug-ins while isolating faults within them. +Each Backstage plugin is an isolated piece of functionality that doesn't typically communicate directly with other plugins. In order to achieve this, there are many parts of the frontend system that provide a layer of indirection for cross-plugin communication, and they routing system is one of them. -In order to address the problem outlined above, we introduced the concept of `RouteRef` which enables us route between pages in a flexible way, and it is especially important when bringing together different open source plugins. +The Backstage routing system makes it possible to implement navigation across plugin boundaries, without each individual plugin knowing the concrete path or location of other plugins in the routing hierarchy, or even its own. This is achieved through the concept of route references, which are opaque reference values that can be shared and used to create concrete links to different parts of an app. ## Route References From 4082cd192a1a14c46208eeb60ba1c0b1c4df01fd Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 14:21:09 +0100 Subject: [PATCH 39/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 2585053683..6326f848d5 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -39,7 +39,7 @@ Route refs do not have any behavior themselves. They are an opaque value that re ### Providing Route References to Plugins -The previous section code snippet does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension: +The code snippet in the previous section does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension: ```tsx title="plugins/catalog/src/plugin.tsx" {11,17-19} showLineNumbers import React from 'react'; From 01167e332dff4b27066517fc57fffcedce19e365 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 14:31:32 +0100 Subject: [PATCH 40/42] Update docs/frontend-system/architecture/07-routes.md Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 6326f848d5..918fddb43c 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -16,7 +16,7 @@ The Backstage routing system makes it possible to implement navigation across pl ## Route References -A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). +A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). It is up to plugin developers to create route references for any page content in their plugin that they want it to be possible to link to or from. Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sections). From 3bca2252d8a610a9ceaa01997727740689e503e1 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 15:13:57 +0100 Subject: [PATCH 41/42] refactor(docs-frontend): do not show line numbers Signed-off-by: Camila Belo --- .../frontend-system/architecture/07-routes.md | 79 +++++++++++++------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 918fddb43c..1cf5ed8c68 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -26,7 +26,7 @@ There are 3 types of route references: regular route, sub route, and external ro Route references, also known as "absolute" or "regular" routes, are created as follows: -```tsx title="plugins/catalog/src/routes.ts" showLineNumbers +```tsx title="plugins/catalog/src/routes.ts" import { createRouteRef } from '@backstage/frontend-plugin-api'; // Creates a route reference, which is not yet associated with any plugin page @@ -41,7 +41,7 @@ Route refs do not have any behavior themselves. They are an opaque value that re The code snippet in the previous section does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension: -```tsx title="plugins/catalog/src/plugin.tsx" {11,17-19} showLineNumbers +```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { createPlugin, @@ -52,25 +52,23 @@ import { indexRouteRef } from './routes'; const catalogIndexPage = createPageExtension({ // The `name` option is omitted because this is an index page defaultPath: '/entities', + // highlight-next-line routeRef: indexRouteRef, loader: () => import('./components').then(m => ), }); export default createPlugin({ id: 'catalog', + // highlight-start routes: { index: indexRouteRef, }, + // highlight-end extensions: [catalogIndexPage], }); ``` -In the example above we have associated a route ref with an plugin page. This is what the code does: - -- Line 11: Associates the `indexRouteRef` with the `catalogIndexPage` extension; -- Line 17 to 19: Provides both the `indexRouteRef` and `catalogIndexPage` via the Catalog plugin. - -When this extension is installed in the app it will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the extension. +In the example above we associated the `indexRouteRef` with the `catalogIndexPage` extension and provided both the route ref and page via the Catalog plugin. So, When this plugin is installed in the app, the index page will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the page extension. It may be unclear why we configure the routes option when creating a plugin as the route has already been passed to the extension. We do that to make it possible for other plugins to route to our page, which is explained in detail in the [binding routes](#binding-external-route-references) section. @@ -78,11 +76,12 @@ It may be unclear why we configure the routes option when creating a plugin as t Route references optionally accept a `params` option, which will require the listed parameter names to be present in the route path. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` parameters, like in this path `/entities/:kind/:namespace/:name`: -```tsx title="plugins/catalog/src/routes.ts" {5} showLineNumbers +```tsx title="plugins/catalog/src/routes.ts" import { createRouteRef } from '@backstage/frontend-plugin-api'; export const detailsRouteRef = createRouteRef({ // The parameters that must be included in the path of this route reference + // highlight-next-line params: ['kind', 'namespace', 'name'], }); ``` @@ -93,22 +92,25 @@ Route references can be used to link to page in the same plugin, or to pages in Suppose we are creating a plugin that renders a Catalog index page with a link to a "Foo" component details page. Here is the code for the index page: -```tsx title="plugins/catalog/src/components/IndexPage.tsx" {6,11-15} showLineNumbers +```tsx title="plugins/catalog/src/components/IndexPage.tsx" import React from 'react'; import { useRouteRef } from '@backstage/frontend-plugin-api'; import { detailsRouteRef } from '../routes'; export const IndexPage = () => { + // highlight-next-line const getDetailsPath = useRouteRef(detailsRouteRef); return (

Index Page

See "Foo" details @@ -121,27 +123,30 @@ We use the `useRouteRef` hook to create a link generator function that returns t Let's see how the details page can get the parameters from the URL: -```tsx title="plugins/catalog/src/components/DetailsPage.tsx" {6,11-13} showLineNumbers +```tsx title="plugins/catalog/src/components/DetailsPage.tsx" import React from 'react'; import { useRouteRefParams } from '@backstage/frontend-plugin-api'; import { detailsRouteRef } from '../routes'; export const DetailsPage = () => { + // highlight-next-line const params = useRouteRefParams(detailsRouteRef); return (

Details Page

    + {/* highlight-start */}
  • Kind: {params.kind}
  • Namespace: {params.namespace}
  • Name: {params.name}
  • + {/* highlight-end */}
); }; ``` -On line 6, we are using the `useRouteRefParams` hook to retrieve the entity-composed id from the URL. The parameter object contains three values: kind, namespace, and name. We can display these values or call an API using them. +In the code above, we are using the `useRouteRefParams` hook to retrieve the entity-composed id from the URL. The parameter object contains three values: kind, namespace, and name. We can display these values or call an API using them. Since we are linking to pages of the same package, we are using a route ref directly. However, in the following sections, you will see how to link to pages of different plugins. @@ -154,7 +159,7 @@ We don't want to reference the Scaffolder plugin directly, since that would crea We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this: -```tsx title="plugins/catalog/src/routes.ts" showLineNumbers +```tsx title="plugins/catalog/src/routes.ts" import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; export const createComponentExternalRouteRef = createExternalRouteRef(); @@ -162,16 +167,18 @@ export const createComponentExternalRouteRef = createExternalRouteRef(); External routes are also used in a similar way as regular routes: -```tsx title="plugins/catalog/src/components/IndexPage.tsx" {6,10} showLineNumbers +```tsx title="plugins/catalog/src/components/IndexPage.tsx" import React from 'react'; import { useRouteRef } from '@backstage/frontend-plugin-api'; import { createComponentExternalRouteRef } from '../routes'; export const IndexPage = () => { + // highlight-next-line const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); return (

Index Page

+ {/* highlight-next-line */} Create Component
); @@ -182,7 +189,7 @@ Given the above binding, using `useRouteRef(createComponentExternalRouteRef)` wi Now the only thing left is to provide the page and external route via a plugin: -```tsx title="plugins/catalog/src/plugin.tsx" {20-23} showLineNumbers +```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { createPlugin, @@ -202,19 +209,22 @@ export default createPlugin({ routes: { index: indexRouteRef, }, + // highlight-start externalRoutes: { createComponent: createComponentExternalRouteRef, }, extensions: [catalogIndexPage], + // highlight-end }); ``` External routes can also have parameters. For example, if you want to link to an entity's details page from Scaffolder, you'll need to create an external route that receives the same parameters the Catalog details page expects: -```tsx title="plugins/scaffolder/src/routes.ts" {4} showLineNumbers +```tsx title="plugins/scaffolder/src/routes.ts" import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; export const entityDetailsExternalRouteRef = createExternalRouteRef({ + // highlight-next-line params: ['kind', 'namespace', 'name'], }); ``` @@ -227,24 +237,27 @@ The association of external routes is controlled by the app. Each `ExternalRoute Using the above example of the Catalog entities list page to the Scaffolder create component page, we might do something like this in the app configuration file: -```yaml title="app-config.yaml" {5,7} showLineNumbers +```yaml title="app-config.yaml" app: routes: bindings: # point to the Scaffolder create component page when the Catalog create component ref is used + # highlight-next-line plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index # point to the Catalog details page when the Scaffolder component details ref is used + # highlight-next-line plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details ``` We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods: -```tsx title="packages/app/src/App.tsx" {6-13} showLineNumbers +```tsx title="packages/app/src/App.tsx" import { createApp } from '@backstage/frontend-app-api'; import catalog from '@backstage/plugin-catalog'; import scaffolder from '@backstage/plugin-scaffolder'; const app = createApp({ + // highlight-start bindRoutes({ bind }) { bind(catalog.externalRoutes, { createComponent: scaffolder.routes.createComponent, @@ -253,6 +266,7 @@ const app = createApp({ componentDetails: catalog.routes.details, }); }, + // highlight-end }); export default app.createRoot(); @@ -266,17 +280,18 @@ Another thing to note is that this indirection in the routing is particularly us It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. -```tsx title="plugins/catalog/src/routes.ts" {4} showLineNumbers +```tsx title="plugins/catalog/src/routes.ts" import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; export const createComponentExternalRouteRef = createExternalRouteRef({ + // highlight-next-line optional: true, }); ``` When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: -```tsx title="plugins/catalog/src/components/IndexPage.tsx" {11} showLineNumbers +```tsx title="plugins/catalog/src/components/IndexPage.tsx" import React from 'react'; import { useRouteRef } from '@backstage/frontend-plugin-api'; import { createComponentExternalRouteRef } from '../routes'; @@ -287,9 +302,11 @@ export const IndexPage = () => {

Index Page

{/* Rendering the link only if the getCreateComponentPath is defined */} + {/* highlight-start */} {getCreateComponentPath && ( Create Component )} + {/* highlight-end */}
); }; @@ -301,32 +318,35 @@ The last kind of route refs that can be created are `SubRouteRef`s, which can be For example: -```tsx title ="plugins/catalog/src/routes.ts" {4,7} showLineNumbers +```tsx title ="plugins/catalog/src/routes.ts" import { createRouteRef, createSubRouteRef, } from '@backstage/frontend-plugin-api'; export const indexRouteRef = createRouteRef(); +// highlight-start export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, path: '/details', }); +// highlight-end ``` There are substantial differences between creating subroutes and regular or external routes because subroutes are associated with regular routes, and the sub route path must be specified. The path string must include the parameters if this sub route has them: -```tsx title ="plugins/catalog/src/routes.ts" {4} showLineNumbers +```tsx title ="plugins/catalog/src/routes.ts" // Omitting rest of the previous example file export const detailsSubRouteRef = createSubRouteRef({ parent: indexRouteRef, + // highlight-next-line path: '/:name/:namespace/:kind', }); ``` Using subroutes in a page extension is as simple as this: -```tsx title="plugins/catalog/src/components/IndexPage.ts" showLineNumbers +```tsx title="plugins/catalog/src/components/IndexPage.ts" import React from 'react'; import { Routes, Route, useLocation } from 'react-router-dom'; import { useRouteRef } from '@backstage/frontend-plugin-api'; @@ -342,8 +362,9 @@ export const IndexPage = () => {

Index Page

{/* Linking to the details sub route */} {pathname === getIndexPath() ? ( - // Setting the details sub route params + // highlight-start { > Show details + // highlight-end ) : ( + // highlight-next-line Hide details )} {/* Registering the details sub route */} @@ -366,19 +389,22 @@ export const IndexPage = () => { This is how you can get the parameters of a sub route URL: -```tsx title="plugins/catalog/src/components/DetailsPage.ts" {5} showLineNumbers +```tsx title="plugins/catalog/src/components/DetailsPage.ts" import React from 'react'; import { useParams } from 'react-router-dom'; export const DetailsPage = () => { + // highlight-next-line const params = useParams(); return (

Details Sub Page

    + {/* highlight-start */}
  • Kind: {params.kind}
  • Namespace: {params.namespace}
  • Name: {params.name}
  • + {/* highlight-end */}
); @@ -387,7 +413,7 @@ export const DetailsPage = () => { Finally, see how a plugin can provide subroutes: -```tsx title="plugins/catalog/src/plugin.ts" {18} showLineNumbers +```tsx title="plugins/catalog/src/plugin.ts" import React from 'react'; import { createPlugin, @@ -405,6 +431,7 @@ export default createPlugin({ id: 'catalog', routes: { index: indexRouteRef, + // highlight-next-line details: detailsSubRouteRef, }, extensions: [catalogIndexPage], From a66ae33632d33343c3204ceb3afb31c5e089d4f6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 17 Jan 2024 15:20:25 +0100 Subject: [PATCH 42/42] refactor(docs-frontend): update intro section Signed-off-by: Camila Belo --- docs/frontend-system/architecture/07-routes.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 1cf5ed8c68..60bf335158 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -12,12 +12,10 @@ description: Frontend routes Each Backstage plugin is an isolated piece of functionality that doesn't typically communicate directly with other plugins. In order to achieve this, there are many parts of the frontend system that provide a layer of indirection for cross-plugin communication, and they routing system is one of them. -The Backstage routing system makes it possible to implement navigation across plugin boundaries, without each individual plugin knowing the concrete path or location of other plugins in the routing hierarchy, or even its own. This is achieved through the concept of route references, which are opaque reference values that can be shared and used to create concrete links to different parts of an app. +The Backstage routing system makes it possible to implement navigation across plugin boundaries, without each individual plugin knowing the concrete path or location of other plugins in the routing hierarchy, or even its own. This is achieved through the concept of route references, which are opaque reference values that can be shared and used to create concrete links to different parts of an app. The route ref paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). It is up to plugin developers to create route references for any page content in their plugin that they want it to be possible to link to or from. ## Route References -A `RouteRef` is an abstract path in a Backstage app, and these paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). It is up to plugin developers to create route references for any page content in their plugin that they want it to be possible to link to or from. - Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sections). There are 3 types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each.