docs: plugin integration into catalog
This commit is contained in:
@@ -26,3 +26,10 @@ This helps the community know what plugins are in development.
|
||||
|
||||
You can also use this process if you have an idea for a good plugin but you hope
|
||||
that someone else will pick up the work.
|
||||
|
||||
## Integrate into the catalog service
|
||||
|
||||
If your plugin isn't supposed to live as a standalone page, but rather needs to
|
||||
be presented as a part of a catalog service (e.g. a separate tab or a card on an
|
||||
"Overview" tab), then check out
|
||||
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
id: integrating-plugin-into-service-catalog
|
||||
title: Integrate into the catalog service
|
||||
---
|
||||
|
||||
> This is an advanced use case and currently is an experimental feature. Expect
|
||||
> API to change over time
|
||||
|
||||
## Steps
|
||||
|
||||
1. [Create a plugin](#create-a-plugin)
|
||||
1. [Export a router with relative routes](#export-a-router)
|
||||
1. [Import and use router in the APP](#import-and-use-router-in-the-app)
|
||||
|
||||
### Create a plugin
|
||||
|
||||
Follow the [same process](create-a-plugin.md) as for standalone plugin. You
|
||||
should have a separate package in a folder, which represents your plugin.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
$ yarn create-plugin
|
||||
> ? Enter an ID for the plugin [required] my-plugin
|
||||
> ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]
|
||||
|
||||
Creating the plugin...
|
||||
```
|
||||
|
||||
### Export a router
|
||||
|
||||
Now in the plugin you have a `Router.tsx` file in the `src` folder. By default
|
||||
it contains only one example route. Create a routing structure needed for your
|
||||
plugin, keeping in mind that the whole set of routes defined here are going to
|
||||
be mounted under some different route in the App.
|
||||
|
||||
Example:
|
||||
|
||||
`my-plugin` consists of 2 different views - `/me` and `/about`. I envision
|
||||
people integrating it into plugin catalog as a tab named "MyPlugin". Then, my
|
||||
`Routes.tsx` for the plugin is going to look like:
|
||||
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route path="/me" element={<MePage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
</Routes>
|
||||
```
|
||||
|
||||
(where MePage and AboutPage are 2 components defined in your plugin and imported
|
||||
accordingly inside `Router.tsx`)
|
||||
|
||||
> Pay attention, if your `MePage` references the `AboutPage` it needs to do it
|
||||
> through link to `about`, not `/about`. This allows react-router v6 to enable
|
||||
> its relative routing mechanism. Read more -
|
||||
> https://reacttraining.com/blog/react-router-v6-pre/#relative-route-path-and-link-to
|
||||
|
||||
### Import and use router in the APP
|
||||
|
||||
In the `app/src/components/catalog/EntityPage.tsx` (app === your folder,
|
||||
containing backstage app) import your created Router:
|
||||
|
||||
```tsx
|
||||
import { Router as MyPluginRouter } from '@backstage/plugin-my-plugin;
|
||||
```
|
||||
|
||||
Now, you need to mount `MyPluginRouter` onto some route, for example if you had:
|
||||
|
||||
```tsx
|
||||
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/"
|
||||
title="Overview"
|
||||
element={<OverviewPage entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
after you add your code it becomes:
|
||||
|
||||
```tsx
|
||||
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/"
|
||||
title="Overview"
|
||||
element={<OverviewPage entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/my-plugin"
|
||||
title="My Plugin"
|
||||
element={<MyPluginRouter entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
All of magic happens thanks to the `EntityPageLayout` component, which comes as
|
||||
an export from `@backstage/plugin-catalog` package.
|
||||
|
||||
```tsx
|
||||
type EntityPageLayoutContentProps = {
|
||||
/**
|
||||
* Going to be transformed into react-router v6
|
||||
* path under the hood. Read more at https://reacttraining.com/blog/react-router-v6-pre
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* Gets transformed into the title for the tab
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Element that is rendered when the location
|
||||
* matches the path provided
|
||||
*/
|
||||
element: JSX.Element;
|
||||
};
|
||||
```
|
||||
|
||||
> The recommended pattern is to get the `entity` in the App and then pass it to
|
||||
> the plugin's Router component as a prop. However if it inconvenient for you,
|
||||
> use `useEntity` hook from `@backstage/plugin-catalog` directly inside your
|
||||
> plugin.
|
||||
@@ -199,7 +199,7 @@
|
||||
"title": "Backend plugin"
|
||||
},
|
||||
"plugins/call-existing-api": {
|
||||
"title": "Call existing API"
|
||||
"title": "Call Existing API"
|
||||
},
|
||||
"plugins/create-a-plugin": {
|
||||
"title": "Create a Backstage Plugin"
|
||||
@@ -210,6 +210,9 @@
|
||||
"plugins/index": {
|
||||
"title": "Intro"
|
||||
},
|
||||
"plugins/integrating-plugin-into-service-catalog": {
|
||||
"title": "Integrate into the catalog service"
|
||||
},
|
||||
"plugins/plugin-development": {
|
||||
"title": "Plugin Development in Backstage"
|
||||
},
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"plugins/create-a-plugin",
|
||||
"plugins/plugin-development",
|
||||
"plugins/structure-of-a-plugin",
|
||||
"plugins/integrating-plugin-into-service-catalog",
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Backends and APIs",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { GitHubActionsPage } from '@backstage/plugin-github-actions';
|
||||
import { Router as GitHubActionsRouter } from '@backstage/plugin-github-actions';
|
||||
import React from 'react';
|
||||
import {
|
||||
EntityPageLayout,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const OverviewPage = ({ entity }: { entity: Entity }) => (
|
||||
const OverviewPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityMetadataCard entity={entity} />
|
||||
);
|
||||
|
||||
@@ -36,12 +36,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout.Content
|
||||
path="/ci-cd/*"
|
||||
title="CI/CD"
|
||||
element={<GitHubActionsPage entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/docs/*"
|
||||
title="Documentation"
|
||||
element={<div>Much docs, such wow ({entity.metadata.name})🐶</div>}
|
||||
element={<GitHubActionsRouter entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
@@ -56,12 +51,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout.Content
|
||||
path="/ci-cd/*"
|
||||
title="CI/CD"
|
||||
element={<GitHubActionsPage entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/docs/*"
|
||||
title="Documentation"
|
||||
element={<div>Much docs, such wow ({entity.metadata.name})🐶</div>}
|
||||
element={<GitHubActionsRouter entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
@@ -69,7 +59,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
const DefaultEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="*"
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<OverviewPage entity={entity} />}
|
||||
/>
|
||||
|
||||
@@ -78,10 +78,11 @@ export const Tabbed = {
|
||||
});
|
||||
|
||||
// Add catch-all for incorrect sub-routes
|
||||
routes.push({
|
||||
path: '/*',
|
||||
element: <Navigate to="." />,
|
||||
});
|
||||
if ((routes?.[0]?.path ?? '') !== '')
|
||||
routes.push({
|
||||
path: '/*',
|
||||
element: <Navigate to={routes[0].path!} />,
|
||||
});
|
||||
|
||||
const [matchedRoute] =
|
||||
matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? [];
|
||||
|
||||
+6
-5
@@ -16,17 +16,18 @@
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { rootRouteRef, buildRouteRef } from './plugin';
|
||||
import { WorkflowRunDetails } from './components/WorkflowRunDetails';
|
||||
import { WorkflowRunsTable } from './components/WorkflowRunsTable';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName';
|
||||
import { rootRouteRef, buildRouteRef } from '../plugin';
|
||||
import { WorkflowRunDetails } from './WorkflowRunDetails';
|
||||
import { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
|
||||
const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity?.metadata?.annotations?.[GITHUB_ACTIONS_ANNOTATION]) &&
|
||||
entity?.metadata?.annotations?.[GITHUB_ACTIONS_ANNOTATION] !== '';
|
||||
|
||||
export const GitHubActionsPage = ({ entity }: { entity: Entity }) =>
|
||||
export const Router = ({ entity }: { entity: Entity }) =>
|
||||
// TODO(shmidt-i): move warning to a separate standardized component
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<WarningPanel title=" GitHubActions plugin:">
|
||||
`entity.metadata.annotations['
|
||||
@@ -16,6 +16,5 @@
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export { Widget } from './components/Widget';
|
||||
export { GitHubActionsPage } from './Router';
|
||||
export { Router } from './components/Router';
|
||||
export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName';
|
||||
|
||||
Reference in New Issue
Block a user