|
|
|
@@ -0,0 +1,585 @@
|
|
|
|
|
---
|
|
|
|
|
id: composability
|
|
|
|
|
title: Composability System Migration
|
|
|
|
|
# prettier-ignore
|
|
|
|
|
description: Documentation and migration instructions for new composability APIs.
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
|
|
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 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.
|
|
|
|
|
|
|
|
|
|
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 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 new composability system isn't a single new API surface. It is a collection
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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 = () => <h1>This is my component</h1>;
|
|
|
|
|
attachComponentData(MyComponent, 'my.data', 5);
|
|
|
|
|
|
|
|
|
|
const element = <MyComponent />;
|
|
|
|
|
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. 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.
|
|
|
|
|
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 and interpreted at once.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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 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 a simple one:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
export type Extension<T> = {
|
|
|
|
|
expose(plugin: BackstagePlugin<any, any>): 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, 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 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.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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, as it is the only way to provide
|
|
|
|
|
a component:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
export const FooPage = plugin.provide(
|
|
|
|
|
createRoutableExtension({
|
|
|
|
|
component: () => import('./components/FooPage').then(m => m.FooPage),
|
|
|
|
|
mountPoint: fooPageRouteRef,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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 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 = () => (
|
|
|
|
|
<Routes>
|
|
|
|
|
<Route path="/foo" element={<FooPage />} />
|
|
|
|
|
<Route path="/bar" element={<BarPage />} />
|
|
|
|
|
</Routes>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const App = () => (
|
|
|
|
|
<AppProvider>
|
|
|
|
|
<AppRouter>
|
|
|
|
|
<Root>
|
|
|
|
|
<AppRoutes />
|
|
|
|
|
</Root>
|
|
|
|
|
</AppRouter>
|
|
|
|
|
</AppProvider>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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 = (
|
|
|
|
|
<Routes>
|
|
|
|
|
<Route path="/foo" element={<FooPage />} />
|
|
|
|
|
<Route path="/bar" element={<BarPage />} />
|
|
|
|
|
</Routes>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const App = () => (
|
|
|
|
|
<AppProvider>
|
|
|
|
|
<AppRouter>
|
|
|
|
|
<Root>{appRoutes}</Root>
|
|
|
|
|
</AppRouter>
|
|
|
|
|
</AppProvider>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
const appRoutes = (
|
|
|
|
|
<Routes>
|
|
|
|
|
<Route path="/foo" element={<FooPage />} />
|
|
|
|
|
<Route path="/bar" element={<BarPage />} />
|
|
|
|
|
</Routes>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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 `fooPageRouteRef`.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
the page. The `useRouteRef` hook takes a single `RouteRef` as its only
|
|
|
|
|
parameter, and returns a function that is called to create the URL. For example
|
|
|
|
|
like this:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
const MyComponent = () => {
|
|
|
|
|
const fooRoute = useRouteRef(fooPageRouteRef);
|
|
|
|
|
return <a href={fooRoute()}>Link to Foo</a>;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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` 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
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
|
|
|
|
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(headerLinkRouteRef)` within the
|
|
|
|
|
`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
|
|
|
|
|
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
|
|
|
|
|
export const fooPlugin = createPlugin({
|
|
|
|
|
routes: {
|
|
|
|
|
root: fooPageRouteRef,
|
|
|
|
|
},
|
|
|
|
|
...
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// In bar-plugin
|
|
|
|
|
export const barPlugin = createPlugin({
|
|
|
|
|
externalRoutes: {
|
|
|
|
|
headerLink: headerLinkRouteRef,
|
|
|
|
|
},
|
|
|
|
|
...
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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 same plugin.
|
|
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
<Route path='/my-page/:name' element={<MyPage />}/>
|
|
|
|
|
|
|
|
|
|
// Usage within a component
|
|
|
|
|
const myRoute = useRouteRef(myRouteRef)
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<a href={myRoute({name: 'a'})}>A</a>
|
|
|
|
|
<a href={myRoute({name: 'b'})}>B</a>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
This pattern will no longer work with the new composability system, and in
|
|
|
|
|
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 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`
|
|
|
|
|
component, you would do the following:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
<EntitySwitch>
|
|
|
|
|
<EntitySwitch.Case if={isKind('template')}>
|
|
|
|
|
<MyTemplate />
|
|
|
|
|
</EntitySwitch.Case>
|
|
|
|
|
|
|
|
|
|
<EntitySwitch.Case>
|
|
|
|
|
<MyTemplate />
|
|
|
|
|
</EntitySwitch.Case>
|
|
|
|
|
</EntitySwitch>
|
|
|
|
|
|
|
|
|
|
// Shorter form if desired:
|
|
|
|
|
<EntitySwitch>
|
|
|
|
|
<EntitySwitch.Case if={isKind('template')} children={<MyTemplate />}/>
|
|
|
|
|
<EntitySwitch.Case children={<MyTemplate />}/>
|
|
|
|
|
</EntitySwitch>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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 = () => (
|
|
|
|
|
- <Routes>
|
|
|
|
|
+ <FlatRoutes>
|
|
|
|
|
...
|
|
|
|
|
- <Route path="/docs/*" element={<DocsRouter />} />
|
|
|
|
|
+ <Route path="/docs" element={<DocsRouter />} />
|
|
|
|
|
...
|
|
|
|
|
- </Routes>
|
|
|
|
|
+ </FlatRoutes>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
-<Route
|
|
|
|
|
- path={`${catalogRouteRef.path}/*`}
|
|
|
|
|
- element={<CatalogRouter EntityPage={EntityPage} />}
|
|
|
|
|
-/>
|
|
|
|
|
+<Route path="/catalog" element={<CatalogIndexPage />} />
|
|
|
|
|
+<Route
|
|
|
|
|
+ path="/catalog/:namespace/:kind/:name"
|
|
|
|
|
+ element={<CatalogEntityPage />}
|
|
|
|
|
+>
|
|
|
|
|
+ <EntityPage />
|
|
|
|
|
+</Route>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
<EntityPageLayout>
|
|
|
|
|
<EntityPageLayout.Content
|
|
|
|
|
path="/"
|
|
|
|
|
title="Overview"
|
|
|
|
|
element={<ComponentOverviewContent entity={entity} />}
|
|
|
|
|
/>
|
|
|
|
|
<EntityPageLayout.Content
|
|
|
|
|
path="/sentry"
|
|
|
|
|
title="Sentry"
|
|
|
|
|
element={<SentryRouter entity={entity} />}
|
|
|
|
|
/>
|
|
|
|
|
<EntityPageLayout.Content
|
|
|
|
|
path="/kubernetes/*"
|
|
|
|
|
title="Kubernetes"
|
|
|
|
|
element={<KubernetesRouter entity={entity} />}
|
|
|
|
|
/>
|
|
|
|
|
</EntityPageLayout>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It would be ported to this:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
<EntityLayout>
|
|
|
|
|
<EntityLayout.Route path="/" title="Overview">
|
|
|
|
|
<ComponentOverviewContent entity={entity} />
|
|
|
|
|
</EntityLayout.Route>
|
|
|
|
|
|
|
|
|
|
<EntityLayout.Route path="/sentry" title="Sentry">
|
|
|
|
|
<SentryRouter entity={entity} />
|
|
|
|
|
</EntityLayout.Route>
|
|
|
|
|
|
|
|
|
|
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
|
|
|
|
|
<KubernetesRouter entity={entity} />
|
|
|
|
|
</EntityLayout.Route>
|
|
|
|
|
</EntityLayout>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
In addition to the renaming, the `element` prop has been moved to `children`.
|
|
|
|
|
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
|
|
|
|
|
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 <ComponentEntityPage entity={entity} />;
|
|
|
|
|
case 'api':
|
|
|
|
|
return <ApiEntityPage entity={entity} />;
|
|
|
|
|
case 'group':
|
|
|
|
|
return <GroupEntityPage entity={entity} />;
|
|
|
|
|
case 'user':
|
|
|
|
|
return <UserEntityPage entity={entity} />;
|
|
|
|
|
default:
|
|
|
|
|
return <DefaultEntityPage entity={entity} />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It would be migrated to this:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
export const entityPage = (
|
|
|
|
|
<EntitySwitch>
|
|
|
|
|
<EntitySwitch.Case if={isKind('component')} children={componentPage} />
|
|
|
|
|
<EntitySwitch.Case if={isKind('api')} children={apiPage} />
|
|
|
|
|
<EntitySwitch.Case if={isKind('group')} children={groupPage} />
|
|
|
|
|
<EntitySwitch.Case if={isKind('user')} children={userPage} />
|
|
|
|
|
<EntitySwitch.Case children={defaultPage} />
|
|
|
|
|
</EntitySwitch>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Note that for example `<ComponentEntityPage ... />` 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.
|