From 3e9b38e288231d070777cd651cd3727b5705b4b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 10 Jan 2021 18:11:23 +0100 Subject: [PATCH 1/8] docs: initial incomplete composability docs --- docs/plugins/composability.md | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/plugins/composability.md diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md new file mode 100644 index 0000000000..d85a557772 --- /dev/null +++ b/docs/plugins/composability.md @@ -0,0 +1,86 @@ +--- +id: composability +title: New Composability System +description: + Documentation and migration instructions for new composability APIs. +--- + +## Summary + +This page describes the new composability system that was recently introduced in +Backstage. It describes the new system from the perspective of the existing +patterns and APIs. As the new system is solidified and existing code is ported, +this page will removed and replaced with a more direct description of the +composability system. + +The core principle of the new 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 point 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 plugins and core APIs. + +The new composability system isn't a single new API surface. It is a collection +of patterns, new primitives, new APIs, and old APIs used in new ways. At the +core is the new concept of Extensions, are exported by plugins for use in the +app. There is a new primitive called component data, which is used to connect +plugin and the app, and a new hook that provides a practical use of . + +## Component Data + +Component data is a new composability primitive that is introduced as a way to +provide a new data dimension for React components. Data is attached to React +components using a key, and is then readable from any JSX elements created with +those components using the same key, as illustrated by the following example: + +```tsx +const MyComponent = () =>

This is my component

; +attachComponentData(MyComponent, 'my.data', 5); + +const element = ; +const myData = getComponentData(element, 'my.data'); +// myData === 5 +``` + +The purpose of component data is to provide a method for embedding data that can +be inspected before rendering elements. It's a pattern that is quite common +among React libraries, and used for example by `react-router` and `material-ui` +to discover properties of the child elements before rendering. Although in those +libraries only the element type and props are typically inspected, while our +component data adds more structured access and simplifies evolution by allowing +for multiple different versions of a piece of data to be used at once. + +The main use-case + +## Extensions + +Extensions are what plugins export for use in an app. Most typically they are +React components, but in practice they can be any kind of value. They are +created using `create*Extension` functions, and wrapped with `plugin.provide()` +in order to create the actual exported extension. + +The Backstage core API currently provides two different types of extension +creators, `createComponentExtension`, and `createRoutableExtension`. + +### Extensions from a plugin's point of view + +Extensions are one of the primary methods to traverse the plugin boundary, and +the way that plugins provide concrete content for use within an app. They +replace existing component export concepts such as `Router` or `*Card`s for +display on entity overview pages. + +### Using Extensions in an app + +TODO + +## RouteRefs, useRouteRef, and plugin routes and externalRoutes + +TODO + +## Binding external routes in the app + +TODO + +## New catalog components, EntitySwitch & EntityLayout, and how to use those in the app + +TODO From 492258d2a10d83fba72d38e5101dc81b0f8795c7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Jan 2021 20:23:27 +0100 Subject: [PATCH 2/8] docs: lotsa more composability docs --- .github/styles/vocab.txt | 2 + docs/plugins/composability.md | 255 +++++++++++++++++++++++++++++++--- 2 files changed, 234 insertions(+), 23 deletions(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index a71887f796..6c38cf638a 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -183,6 +183,8 @@ rollbar Rollbar Rollup Rosaceae +routable +Routable rst rsync rugvip diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index d85a557772..5c96b88cee 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -26,7 +26,12 @@ core is the new concept of Extensions, are exported by plugins for use in the app. There is a new primitive called component data, which is used to connect plugin and the app, and a new hook that provides a practical use of . -## Component Data +## New Concepts + +This section is a brief look into all the new and updated concepts that were put +in place to support the new composability system. + +### Component Data Component data is a new composability primitive that is introduced as a way to provide a new data dimension for React components. Data is attached to React @@ -43,44 +48,248 @@ const myData = getComponentData(element, 'my.data'); ``` The purpose of component data is to provide a method for embedding data that can -be inspected before rendering elements. It's a pattern that is quite common -among React libraries, and used for example by `react-router` and `material-ui` -to discover properties of the child elements before rendering. Although in those -libraries only the element type and props are typically inspected, while our -component data adds more structured access and simplifies evolution by allowing -for multiple different versions of a piece of data to be used at once. +be inspected before rendering elements. Element inspection is a pattern that is +quite common among React libraries, and used for example by `react-router` and +`material-ui` to discover properties of the child elements before rendering. +Though in those libraries only the element type and props are typically +inspected, while our component data adds more structured access and simplifies +evolution by allowing for multiple different versions of a piece of data to be +used at once. -The main use-case +The initial use-cases for component data is support route and plugin discovery +through elements in the app. Through this we allow for the React element tree in +the app to be the source of truth, both for which plugins are used and all +top-level plugin routes in the app. The use of component data is not limited to +these use-cases though, as it can be used as a primitive to create new +abstractions as well. -## Extensions +### Extensions Extensions are what plugins export for use in an app. Most typically they are React components, but in practice they can be any kind of value. They are created using `create*Extension` functions, and wrapped with `plugin.provide()` in order to create the actual exported extension. -The Backstage core API currently provides two different types of extension -creators, `createComponentExtension`, and `createRoutableExtension`. +The extension type is dead simple: -### Extensions from a plugin's point of view +```ts +export type Extension = { + expose(plugin: BackstagePlugin): T; +}; +``` + +The power of extensions comes from the ability of various actors to hook into +their usage. The creation and plugin wrapping is controlled by whoever owns the +creation function, the Backstage core is able to hook into the process of +exposing the extension outside the plugin, and in the end the app controls the +usage of the extension. + +The Backstage core API currently provides two different types of extension +creators, `createComponentExtension`, and `createRoutableExtension`. Component +extensions are plain react component with no particular requirements, such as +cards for entity overview pages. The component will be exported more or less as +is, but is wrapped up to provide things like an error boundary, lazy loading, +and a plugin context. + +Routable extensions build on top of component extensions and are used for any +component that should be rendered at a specific route path, such as full pages +or entity page tab content. When creating a routable extension you need to +supply a `RouteRef` as `mountPoint`. The mount point will be the handle of the +component for the outside world, and is used by other components and plugins +that wish to link to the routable component. + +As of now there are only two extension creation functions, but it is possible to +add more of them in the future, both in the core library and in plugins that +wish to provide an extension point for other plugins to build upon. Extensions +are also not tied to React, and can both be used to model generic JavaScript +concepts, as well as potentially bridge to rendering libraries and web +frameworks other than React. + +### Extensions from a Plugin's Point of View Extensions are one of the primary methods to traverse the plugin boundary, and the way that plugins provide concrete content for use within an app. They replace existing component export concepts such as `Router` or `*Card`s for display on entity overview pages. -### Using Extensions in an app +It is recommended to create the exported extensions either in the top-level +`plugin.ts` file, or in a dedicated `extensions.ts` (or `.tsx`) file. That file +should not contain the bulk of the implementation though, and in fact, if the +extension is a React component it is recommended to lazy-load the actual +component. Component extensions support lazy loading out of the box using the +`lazy` component declaration, for example: + +```ts +export const EntityFooCard = plugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/FooCard').then(m => m.FooCard), + }, + }), +); +``` + +Routable extensions even enforce lazy loading, for example: + +```ts +export const FooPage = plugin.provide( + createRoutableExtension({ + component: () => import('./components/FooPage').then(m => m.FooPage), + mountPoint: fooRouteRef, + }), +); +``` + +### Using Extensions in an App + +Right now all extensions are modelled as React components. The usage of these +extension is like regular usage of any React components, with one important +difference. Extensions must be all be part of a single React element tree +spanning from the root `AppProvider`. + +For example, the following app code does **NOT** work: + +```tsx +const AppRoutes = () => ( + + } /> + } /> + +); + +const App = () => ( + + + + + + + +); +``` + +But it is simple to fix! Simply make sure that you don't create any intermediate +components in the app, for example like this: + +```tsx +const appRoutes = ( + + } /> + } /> + +); + +const App = () => ( + + + {appRoutes} + + +); +``` + +### New Routing System + +A big piece of what is enabled by moving over to this new composability system +is to make `RouteRef`s useful. The `RouteRef`s no longer have their own path, in +fact the only required parameter is currently a `title`. Instead of assigning a +path to each `RouteRef` and possibly overriding these paths in the app, the +concrete `path` for each `RouteRef` is discovered based on the element tree in +the app. Let's consider the following example: + +```tsx + + } /> + } /> + +``` + +We'll assume that `FooPage` and `BarPage` are routable extensions, exported by +the `fooPlugin` and `barPlugin` respectively. Since the `FooPage` is a routable +extension it has a `RouteRef` assigned as its mount point, which we'll refer to +as `fooRootRouteRef`. + +Given the above example, the `fooRootRouteRef` will be associated with the +`'/foo'` route. The path is no longer accessible via the `path` property of the +`RouteRef` though, as the routing structure is tied to the app's react tree. We +instead use the new `useRouteRef` hook if we want to create a concrete link to +the page. The `useRouteRef` hook takes a single `RouteRef` as its only +parameter, and returns a function that is called to create the URL. + +Now let's assume that we want to link from the `BarPage` to the `FooPage`. +Before the introduction of the new composability system, we would do this by +importing the `fooRootRouteRef` from the `fooPlugin`. This created an +unnecessary dependency on the plugin, and also provided little flexibility +allowing the app to tie plugins together rather than the plugins themselves. To +handle this, we introduce the concept of `ExternalRouteRef`s. Much like regular +route refs, they can be passed to `useRouteRef` to create concrete URLs, but +they can not be used as mount points in routable component and instead have to +be associated with an actual using route bindings in the app. + +The `ExternalRouteRef` inside the `barPlugin` should also not be opinionated +about what it is linking to either, allowing the app to decide the final target. +It should however provide context in how the link is presented or used, to make +it easier to understand the flow of the app. If the `BarPage` for example wants +to link to an external page in the header, it might declare an +`ExternalRouteRef` similar to this: + +```ts +const headerLinkRouteRef = createExternalRouteRef(); +``` + +### Binding External Routes in the App + +The association of external routes are controlled by the app. Each +`ExternalRouteRef` of a plugin is bound to an actual `RouteRef`, usually from +another plugin. The binding process happens once att app startup, and is then +used through the lifetime of the app to help resolve concrete route paths. + +Using the above example of the `BarPage` linking to the `FooPage`, we might do +something like this in the app: + +```ts +createApp({ + bindRoutes({ bind }) { + bind(barPlugin.externalRoutes, { + headerLink: fooPlugin.routes.root, + }); + }, +}); +``` + +Given the above binding, using `useRouteRef(external)` + +Note that we are not importing and using the `RouteRef`s directly, 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 different things exported from each +plugin package. The route references would be supplied to `createPlugin` like +this: + +```ts +// In foo-plugin +export const fooPlugin = createPlugin({ + routes: { + root: fooRootRouteRef, + }, + ... +}) + +// In bar-plugin +export const barPlugin = createPlugin({ + externalRoutes: { + headerLink: headerLinkRouteRef, + }, + ... +}) +``` + +### New Catalog Components + +EntitySwitch & EntityLayout, and how to use those in the app TODO -## RouteRefs, useRouteRef, and plugin routes and externalRoutes +## Porting Existing Plugins -TODO - -## Binding external routes in the app - -TODO - -## New catalog components, EntitySwitch & EntityLayout, and how to use those in the app - -TODO +## Porting Existing Apps From 70d8653bdad5649399bcae8d146922bdc3d7b2d5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 13 Jan 2021 23:52:10 +0100 Subject: [PATCH 3/8] docs/composability: the rest of the f***ing owl --- docs/plugins/composability.md | 403 +++++++++++++++++++++++++++++----- 1 file changed, 346 insertions(+), 57 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 5c96b88cee..3e38f3376e 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -16,15 +16,17 @@ composability system. The core principle of the new 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 point for other plugins to +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 plugins and core APIs. +prioritizing simplicity and clarity in the app over that in the plugins and core +APIs. The new composability system isn't a single new API surface. It is a collection -of patterns, new primitives, new APIs, and old APIs used in new ways. At the -core is the new concept of Extensions, are exported by plugins for use in the -app. There is a new primitive called component data, which is used to connect -plugin and the app, and a new hook that provides a practical use of . +of patterns, primitives, new APIs, and old APIs used in new ways. At the core is +the new concept of extensions, which are exported by plugins for use in the app. +There is also a new primitive called component data, which assists in the +conversion to a more declarative app. The `RouteRef`s now have a clear purpose +as well, and can be used route to pages in a flexible way. ## New Concepts @@ -36,7 +38,7 @@ in place to support the new composability system. Component data is a new composability primitive that is introduced as a way to provide a new data dimension for React components. Data is attached to React components using a key, and is then readable from any JSX elements created with -those components using the same key, as illustrated by the following example: +those components, using the same key, as illustrated by the following example: ```tsx const MyComponent = () =>

This is my component

; @@ -51,26 +53,26 @@ The purpose of component data is to provide a method for embedding data that can be inspected before rendering elements. Element inspection is a pattern that is quite common among React libraries, and used for example by `react-router` and `material-ui` to discover properties of the child elements before rendering. -Though in those libraries only the element type and props are typically +Although in those libraries only the element type and props are typically inspected, while our component data adds more structured access and simplifies evolution by allowing for multiple different versions of a piece of data to be -used at once. +used and interpreted at once. The initial use-cases for component data is support route and plugin discovery through elements in the app. Through this we allow for the React element tree in -the app to be the source of truth, both for which plugins are used and all -top-level plugin routes in the app. The use of component data is not limited to -these use-cases though, as it can be used as a primitive to create new +the app to be the source of truth, both for which plugins are used, as well as +all top-level plugin routes in the app. The use of component data is not limited +to these use-cases though, as it can be used as a primitive to create new abstractions as well. ### Extensions Extensions are what plugins export for use in an app. Most typically they are -React components, but in practice they can be any kind of value. They are -created using `create*Extension` functions, and wrapped with `plugin.provide()` -in order to create the actual exported extension. +React components, but in practice they can be any kind of JavaScript value. They +are created using `create*Extension` functions, and wrapped with +`plugin.provide()` in order to create the actual exported extension. -The extension type is dead simple: +The extension type is a simple one: ```ts export type Extension = { @@ -86,14 +88,14 @@ usage of the extension. The Backstage core API currently provides two different types of extension creators, `createComponentExtension`, and `createRoutableExtension`. Component -extensions are plain react component with no particular requirements, such as -cards for entity overview pages. The component will be exported more or less as -is, but is wrapped up to provide things like an error boundary, lazy loading, -and a plugin context. +extensions are plain React component with no particular requirements, for +example a card for an entity overview page. The component will be exported more +or less as is, but is wrapped to provide things like an error boundary, lazy +loading, and a plugin context. Routable extensions build on top of component extensions and are used for any -component that should be rendered at a specific route path, such as full pages -or entity page tab content. When creating a routable extension you need to +component that should be rendered at a specific route path, such as top-level +pages or entity page tab content. When creating a routable extension you need to supply a `RouteRef` as `mountPoint`. The mount point will be the handle of the component for the outside world, and is used by other components and plugins that wish to link to the routable component. @@ -129,7 +131,8 @@ export const EntityFooCard = plugin.provide( ); ``` -Routable extensions even enforce lazy loading, for example: +Routable extensions even enforce lazy loading, as it is the only way to provide +a component: ```ts export const FooPage = plugin.provide( @@ -144,8 +147,8 @@ export const FooPage = plugin.provide( Right now all extensions are modelled as React components. The usage of these extension is like regular usage of any React components, with one important -difference. Extensions must be all be part of a single React element tree -spanning from the root `AppProvider`. +difference. Extensions must all be part of a single React element tree spanning +from the root `AppProvider`. For example, the following app code does **NOT** work: @@ -168,8 +171,8 @@ const App = () => ( ); ``` -But it is simple to fix! Simply make sure that you don't create any intermediate -components in the app, for example like this: +But in this case it is simple to fix! Simply be sure to not create any +intermediate components in the app, for example like this: ```tsx const appRoutes = ( @@ -198,10 +201,12 @@ concrete `path` for each `RouteRef` is discovered based on the element tree in the app. Let's consider the following example: ```tsx - - } /> - } /> - +const appRoutes = ( + + } /> + } /> + +); ``` We'll assume that `FooPage` and `BarPage` are routable extensions, exported by @@ -214,24 +219,32 @@ Given the above example, the `fooRootRouteRef` will be associated with the `RouteRef` though, as the routing structure is tied to the app's react tree. We instead use the new `useRouteRef` hook if we want to create a concrete link to the page. The `useRouteRef` hook takes a single `RouteRef` as its only -parameter, and returns a function that is called to create the URL. +parameter, and returns a function that is called to create the URL. For example +like this: + +```tsx +const MyComponent = () => { + const fooRoute = useRouteRef(fooRouteRef); + return Link to Foo; +}; +``` Now let's assume that we want to link from the `BarPage` to the `FooPage`. Before the introduction of the new composability system, we would do this by importing the `fooRootRouteRef` from the `fooPlugin`. This created an -unnecessary dependency on the plugin, and also provided little flexibility -allowing the app to tie plugins together rather than the plugins themselves. To -handle this, we introduce the concept of `ExternalRouteRef`s. Much like regular -route refs, they can be passed to `useRouteRef` to create concrete URLs, but -they can not be used as mount points in routable component and instead have to -be associated with an actual using route bindings in the app. +unnecessary dependency on the plugin, and 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 introduce `ExternalRouteRef`s. Much +like regular route references, they can be passed to `useRouteRef` to create +concrete URLs, but they can not be used as mount points in routable component +and instead have to be associated with a target route using route bindings in +the app. -The `ExternalRouteRef` inside the `barPlugin` should also not be opinionated -about what it is linking to either, allowing the app to decide the final target. -It should however provide context in how the link is presented or used, to make -it easier to understand the flow of the app. If the `BarPage` for example wants -to link to an external page in the header, it might declare an -`ExternalRouteRef` similar to this: +We create a new `ExternalRouteRef` inside the `barPlugin`, 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 +`BarPage` for example wants to link to an external page in the header, it might +declare an `ExternalRouteRef` similar to this: ```ts const headerLinkRouteRef = createExternalRouteRef(); @@ -239,10 +252,11 @@ const headerLinkRouteRef = createExternalRouteRef(); ### Binding External Routes in the App -The association of external routes are controlled by the app. Each -`ExternalRouteRef` of a plugin is bound to an actual `RouteRef`, usually from -another plugin. The binding process happens once att app startup, and is then -used through the lifetime of the app to help resolve concrete route paths. +The association of external routes is controlled by the app. Each +`ExternalRouteRef` of a plugin should be<- bound to an actual `RouteRef`, +usually from another plugin. The binding process happens once att app startup, +and is then used through the lifetime of the app to help resolve concrete route +paths. Using the above example of the `BarPage` linking to the `FooPage`, we might do something like this in the app: @@ -257,14 +271,15 @@ createApp({ }); ``` -Given the above binding, using `useRouteRef(external)` +Given the above binding, using `useRouteRef(headerLinkRouteRef)` within the +`barPlugin` will let us create a link whatever path the `FooPage` is mounted at. -Note that we are not importing and using the `RouteRef`s directly, 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 different things exported from each -plugin package. The route references would be supplied to `createPlugin` like -this: +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. The route references would be supplied to `createPlugin` +like this: ```ts // In foo-plugin @@ -284,12 +299,286 @@ export const barPlugin = createPlugin({ }) ``` +Also 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 app. + +### Parameterized Routes + +A new addition to `RouteRef`s is the possibility of adding named and typed +parameters. Parameters are declared at creation, and will enforce presence of +the parameters in the path in the app, and require them as a parameter when +using `useRouteRef`. + +The following is an example of creation and usage of a parameterized route: + +```tsx +// Creation of a parameterized route +const myRouteRef = createRouteRef({ + title: 'My Named Route', + params: ['name'] +}) + +// In the app, where MyPage is a routable extension with myRouteRef set as mountPoint +}/> + +// Usage within a component +const myRoute = useRouteRef(myRouteRef) +return ( +
+ A + B +
+) +``` + +It is currently not possible to have parameterized `ExternalRouteRef`s, or to +bind an external route to a parameterized route, although this may be added in +the future if needed. + ### New Catalog Components -EntitySwitch & EntityLayout, and how to use those in the app +The established pattern for selecting what plugins should be available on each +catalog page is to use custom components in the app, with logic embedded in the +render function. Typically this takes form as a component that either receives +the entity via props or uses the `useEntity` hook to retrieve the selected +entity. A `switch` or `if` / `else if` chain is then used to select what +children should be rendered based on information in the entity. -TODO +This pattern will no longer work with the new composability system, and in +general is very difficult to build any form declarative model around, as it +depends on runtime execution. To help replace existing code, a new +`EntitySwitch` component has been added to the `@backstage/catalog` plugin, +which grabs the selected entity from context, and selects at most one element to +render using a list of `EntitySwitch.Case`s children. + +For example, if you want all entities of kind `"Template"` to be rendered with a +`MyTemplate` component, and all other entities to be rendered with a `MyOther` +component, you would do the following: + +```tsx + + + + + + + + + + +// Shorter form if desired: + + }/> + }/> + +``` + +The `EntitySwitch` component will render the children of the first +`EntitySwitch.Case` that returns `true` when the selected entity is passed to +the function of the `if` prop. If none of the cases match, no children will be +rendered, and if a case doesn't specify an `if` filter function, it will always +match. The `if` property is simply a function of the type +`(entity: Entity) => boolean`, for example, `isKind` can be implemented like +this: + +```ts +function isKind(kind: string) { + return (entity: Entity) => entity.kind.toLowerCase() === kind.toLowerCase(); +} +``` + +The `@backstage/catalog` plugin provides a couple of built-in conditions, +`isKind`, `isComponentType`, and `isNamespace`. + +In addition to the `EntitySwitch` component, the catalog plugin also exports a +new `EntityLayout` component. It is a tweaked version and replacement for the +`EntityPageLayout` component, and is introduced more in depth in the app +migration section below. ## Porting Existing Plugins +There are a couple of high-level steps to porting an existing plugin to the new +composability system: + +- Remove usage of `router.addRoute` or `router.registerRoute` within + `createPlugin`, and export the page components as routable extensions instead. +- Switch any `Router` export to instead be a routable extension. +- Change any plain component exports, such as catalog overview cards, to be + component extensions. +- Stop exporting `RouteRef`s and instead pass them to `createPlugin`. +- Stop accepting `RouteRef`s as props or importing them from other plugins, + instead create an `ExternalRouteRef` as a replacement, and pass it to + `createPlugin.` +- Rename any other exported symbols according to the naming pattern table below. + +Note that removing the existing exports and configuration is a breaking change +in any plugin. If backwards compatibility is needed the existing code be +deprecated while making the new additions, to then be removed at a later point. + +### Naming Patterns + +Many export naming patterns have been changed to avoid import aliases and to +clarify intent. Refer to the following table to formulate the new name: + +| Description | Existing Pattern | New Pattern | Examples | +| -------------------- | -------------------------- | --------------- | ---------------------------------------------- | +| Top-level Pages | Router | \*Page | CatalogIndexPage, SettingsPage, LighthousePage | +| Entity Tab Content | Router | Entity\*Content | EntityJenkinsContent, EntityKubernetesContent | +| Entity Overview Card | \*Card | Entity\*Card | EntitySentryCard, EntityPagerDutyCard | +| Entity Conditional | isPluginApplicableToEntity | is\*Available | isPagerDutyAvailable, isJenkinsAvailable | +| Plugin Instance | plugin | \*Plugin | jenkinsPlugin, catalogPlugin | + ## Porting Existing Apps + +The first step of porting any app is to replace the root `Routes` component with +`FlatRoutes` from `@backstage/core`. As opposed to the `Routes` component, +`FlatRoutes` only considers the first level of `Route` components in its +children, and provides any additional children to the outlet of the route. It +also removes the need to append `"/*"` to paths, as it is added automatically. + +```diff +const AppRoutes = () => ( +- ++ + ... +- } /> ++ } /> + ... +- ++ +); +``` + +The next step should be to switch from using `EntityPageLayout` to +`EntityLayout`, as this can also be done without waiting for plugins to be +ported. You should also replace the top-level `Router` from the catalog plugin +with the separate `CatalogIndexPage` and `CatalogEntityPage` extensions that +have been added to the catalog: + +```diff +-} +-/> ++} /> ++} ++> ++ ++ +``` + +At that point you should flatten out the element tree as much as possible in the +app, removing any intermediate components. At the top level this should usually +be straightforward, but when reaching the catalog entity pages you may need to +wait for some plugins to be migrated. This is because it is no longer possible +to pass in the selected entity through component props, and it should be picked +up from context inside the plugin instead. See the sections below for how to +carry out migrations of some common entity page patterns. + +Once the app element tree doesn't contain any intermediate components, and all +plugin imports have been switched to extensions rather than plain components, +the app has been fully ported. + +### Switching from EntityPageLayout to EntityLayout + +The existing `EntityPageLayout` is replaced by the new `EntityLayout` component, +which has a slightly different pattern for expressing the contents and paths. + +Porting from the old to the new API is just a matter of moving some things +around. For example, given the following existing code: + +```tsx + + } + /> + } + /> + } + /> + +``` + +It would be ported to this: + +```tsx + + + } + + + + } + + + + } + + +``` + +In addition to the renaming, the `element` prop has been moved to `children`. +Also note that the `/*` suffix has been remove from the `"/kubernetes"` path, as +it's now added automatically. + +Usage of the `EntityLayout` component is required to be able to properly +discover routes, and so it is required to apply this change before you can start +using routable entity content extensions from plugins. + +### Porting Entity Pages + +The established pattern in the app is to use custom components in order to +select what plugin components to render for a given entity. The new +`EntitySwitch` component introduced above is what is intended to replace this +pattern, now that the entire app needs to be rendered as a single element tree. +For example, given the following existing code: + +```tsx +export const EntityPage = () => { + const { entity } = useEntity(); + + switch (entity?.kind?.toLowerCase()) { + case 'component': + return ; + case 'api': + return ; + case 'group': + return ; + case 'user': + return ; + default: + return ; + } +}; +``` + +It would be migrated to this: + +```tsx +export const entityPage = ( + + + + + + + +); +``` + +Note that for example `` has been changed to simply +`componentPage`, that is because just like the `EntityPage` component, the +`ComponentEntityPage` also needs to be ported to be an element rather a +component in a similar way. From 72ce0f8b163b4d01a309064cad99f6f5af20e36e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Jan 2021 02:04:57 +0100 Subject: [PATCH 4/8] docs/composability: include in sidebar and add note about purpose --- docs/plugins/composability.md | 11 ++++++----- microsite/sidebars.json | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 3e38f3376e..e6f00d2374 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -1,6 +1,6 @@ --- id: composability -title: New Composability System +title: Composability System Migration description: Documentation and migration instructions for new composability APIs. --- @@ -8,10 +8,11 @@ description: ## Summary This page describes the new composability system that was recently introduced in -Backstage. It describes the new system from the perspective of the existing -patterns and APIs. As the new system is solidified and existing code is ported, -this page will removed and replaced with a more direct description of the -composability system. +Backstage, and it does so from the perspective of the existing patterns and +APIs. As the new system is solidified and existing code is ported, this page +will removed and replaced with a more direct description of the composability +system. For now, the primary purpose of this documentation is to aid in the +migration of existing plugins, but it does cover the migration of apps as well. The core principle of the new composability system is that plugins should have clear boundaries and connections. It should isolate crashes within a plugin, but diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 3d6417cc65..05e421cb19 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -95,6 +95,7 @@ "plugins/plugin-development", "plugins/structure-of-a-plugin", "plugins/integrating-plugin-into-service-catalog", + "plugins/composability", { "type": "subcategory", "label": "Backends and APIs", From 22d2a523475b6e817cd221fc508171afa15a008c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Jan 2021 12:25:58 +0100 Subject: [PATCH 5/8] Apply suggestions from code review Co-authored-by: Himanshu Mishra --- docs/plugins/composability.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index e6f00d2374..5f1be7a561 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -1,8 +1,8 @@ --- id: composability title: Composability System Migration -description: - Documentation and migration instructions for new composability APIs. +# prettier-ignore +description: Documentation and migration instructions for new composability APIs. --- ## Summary @@ -59,7 +59,7 @@ inspected, while our component data adds more structured access and simplifies evolution by allowing for multiple different versions of a piece of data to be used and interpreted at once. -The initial use-cases for component data is support route and plugin discovery +The initial use-case for component data is to support route and plugin discovery through elements in the app. Through this we allow for the React element tree in the app to be the source of truth, both for which plugins are used, as well as all top-level plugin routes in the app. The use of component data is not limited @@ -254,8 +254,8 @@ const headerLinkRouteRef = createExternalRouteRef(); ### Binding External Routes in the App The association of external routes is controlled by the app. Each -`ExternalRouteRef` of a plugin should be<- bound to an actual `RouteRef`, -usually from another plugin. The binding process happens once att app startup, +`ExternalRouteRef` of a plugin should be bound to an actual `RouteRef`, +usually from another plugin. The binding process happens once at app startup, and is then used through the lifetime of the app to help resolve concrete route paths. @@ -517,15 +517,15 @@ It would be ported to this: ```tsx - } + - } + - } + ``` From 151e36504020a747a1e3072781cf2237888c7e60 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Jan 2021 12:53:25 +0100 Subject: [PATCH 6/8] docs/composability: ran prettier + added to mkdocs.yml --- docs/plugins/composability.md | 7 +++---- mkdocs.yml | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 5f1be7a561..c23ab2882d 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -254,10 +254,9 @@ const headerLinkRouteRef = createExternalRouteRef(); ### Binding External Routes in the App The association of external routes is controlled by the app. Each -`ExternalRouteRef` of a plugin should be bound to an actual `RouteRef`, -usually from another plugin. The binding process happens once at app startup, -and is then used through the lifetime of the app to help resolve concrete route -paths. +`ExternalRouteRef` of a plugin should be bound to an actual `RouteRef`, usually +from another plugin. The binding process happens once at app startup, and is +then used through the lifetime of the app to help resolve concrete route paths. Using the above example of the `BarPage` linking to the `FooPage`, we might do something like this in the app: diff --git a/mkdocs.yml b/mkdocs.yml index 29198963e3..105ad721d5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - Creating a new plugin: 'plugins/create-a-plugin.md' - Developing a plugin: 'plugins/plugin-development.md' - Structure of a plugin: 'plugins/structure-of-a-plugin.md' + - Composability System Migration: 'plugins/composability.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backstage backend plugin: 'plugins/backend-plugin.md' From 93be0f55e32d5aadb2ac5daa5b64adb4d882879d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Jan 2021 16:34:59 +0100 Subject: [PATCH 7/8] docs/composability: fooRootRouteRef -> fooPageRouteRef --- docs/plugins/composability.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index c23ab2882d..207540b989 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -139,7 +139,7 @@ a component: export const FooPage = plugin.provide( createRoutableExtension({ component: () => import('./components/FooPage').then(m => m.FooPage), - mountPoint: fooRouteRef, + mountPoint: fooPageRouteRef, }), ); ``` @@ -213,9 +213,9 @@ const appRoutes = ( We'll assume that `FooPage` and `BarPage` are routable extensions, exported by the `fooPlugin` and `barPlugin` respectively. Since the `FooPage` is a routable extension it has a `RouteRef` assigned as its mount point, which we'll refer to -as `fooRootRouteRef`. +as `fooPageRouteRef`. -Given the above example, the `fooRootRouteRef` will be associated with the +Given the above example, the `fooPageRouteRef` will be associated with the `'/foo'` route. The path is no longer accessible via the `path` property of the `RouteRef` though, as the routing structure is tied to the app's react tree. We instead use the new `useRouteRef` hook if we want to create a concrete link to @@ -225,14 +225,14 @@ like this: ```tsx const MyComponent = () => { - const fooRoute = useRouteRef(fooRouteRef); + const fooRoute = useRouteRef(fooPageRouteRef); return Link to Foo; }; ``` Now let's assume that we want to link from the `BarPage` to the `FooPage`. Before the introduction of the new composability system, we would do this by -importing the `fooRootRouteRef` from the `fooPlugin`. This created an +importing the `fooPageRouteRef` from the `fooPlugin`. This created an unnecessary dependency on the plugin, and 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 introduce `ExternalRouteRef`s. Much @@ -285,7 +285,7 @@ like this: // In foo-plugin export const fooPlugin = createPlugin({ routes: { - root: fooRootRouteRef, + root: fooPageRouteRef, }, ... }) From 68cf50c52372d1ce4964465abb108f9ae96285ee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Jan 2021 16:36:41 +0100 Subject: [PATCH 8/8] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw --- docs/plugins/composability.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 207540b989..a496217875 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -10,7 +10,7 @@ description: Documentation and migration instructions for new composability APIs This page describes the new composability system that was recently introduced in Backstage, and it does so from the perspective of the existing patterns and APIs. As the new system is solidified and existing code is ported, this page -will removed and replaced with a more direct description of the composability +will be removed and replaced with a more direct description of the composability system. For now, the primary purpose of this documentation is to aid in the migration of existing plugins, but it does cover the migration of apps as well. @@ -232,7 +232,7 @@ const MyComponent = () => { Now let's assume that we want to link from the `BarPage` to the `FooPage`. Before the introduction of the new composability system, we would do this by -importing the `fooPageRouteRef` from the `fooPlugin`. This created an +importing the `fooPageRouteRef` exported by the `fooPlugin`. This created an unnecessary dependency on the plugin, and 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 introduce `ExternalRouteRef`s. Much @@ -272,7 +272,8 @@ createApp({ ``` Given the above binding, using `useRouteRef(headerLinkRouteRef)` within the -`barPlugin` will let us create a link whatever path the `FooPage` is mounted at. +`barPlugin` will let us create a link to whatever path the `FooPage` 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 @@ -302,7 +303,7 @@ export const barPlugin = createPlugin({ Also 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 app. +references from other parts of the same plugin. ### Parameterized Routes @@ -347,11 +348,11 @@ entity. A `switch` or `if` / `else if` chain is then used to select what children should be rendered based on information in the entity. This pattern will no longer work with the new composability system, and in -general is very difficult to build any form declarative model around, as it +general is very difficult to build any form of declarative model around, as it depends on runtime execution. To help replace existing code, a new `EntitySwitch` component has been added to the `@backstage/catalog` plugin, -which grabs the selected entity from context, and selects at most one element to -render using a list of `EntitySwitch.Case`s children. +which grabs the selected entity from a context, and selects at most one element +to render using a list of `EntitySwitch.Case` children. For example, if you want all entities of kind `"Template"` to be rendered with a `MyTemplate` component, and all other entities to be rendered with a `MyOther` @@ -530,8 +531,8 @@ It would be ported to this: ``` In addition to the renaming, the `element` prop has been moved to `children`. -Also note that the `/*` suffix has been remove from the `"/kubernetes"` path, as -it's now added automatically. +Also note that the `/*` suffix has been removed from the `"/kubernetes"` path, +as it's now added automatically. Usage of the `EntityLayout` component is required to be able to properly discover routes, and so it is required to apply this change before you can start