From f369994ef6569d05d790d9164da28d0cf2d9fc25 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 14 Dec 2023 16:12:18 +0100 Subject: [PATCH] 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'; ```