diff --git a/docs/plugins/index.md b/docs/plugins/index.md index bb8951f959..857b8f7fcf 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -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. diff --git a/docs/plugins/integrating-plugin-into-service-catalog.md b/docs/plugins/integrating-plugin-into-service-catalog.md new file mode 100644 index 0000000000..7819737c52 --- /dev/null +++ b/docs/plugins/integrating-plugin-into-service-catalog.md @@ -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 + + } /> + } /> + +``` + +(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 }) => ( + + } + /> + +); +``` + +after you add your code it becomes: + +```tsx +const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + +); +``` + +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. diff --git a/microsite/i18n/en.json b/microsite/i18n/en.json index d2593a13ce..1f04f7430b 100644 --- a/microsite/i18n/en.json +++ b/microsite/i18n/en.json @@ -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" }, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index b07f326086..7576296b7a 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -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", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 4368d8e26b..1c7a08e362 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -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 }) => ( ); @@ -36,12 +36,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( } - /> - Much docs, such wow ({entity.metadata.name})🐶} + element={} /> ); @@ -56,12 +51,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( } - /> - Much docs, such wow ({entity.metadata.name})🐶} + element={} /> ); @@ -69,7 +59,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( } /> diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx index 6f3e5e1fcf..ad0b9d067c 100644 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx @@ -78,10 +78,11 @@ export const Tabbed = { }); // Add catch-all for incorrect sub-routes - routes.push({ - path: '/*', - element: , - }); + if ((routes?.[0]?.path ?? '') !== '') + routes.push({ + path: '/*', + element: , + }); const [matchedRoute] = matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? []; diff --git a/plugins/github-actions/src/Router.tsx b/plugins/github-actions/src/components/Router.tsx similarity index 80% rename from plugins/github-actions/src/Router.tsx rename to plugins/github-actions/src/components/Router.tsx index c508302e6d..80637b75f0 100644 --- a/plugins/github-actions/src/Router.tsx +++ b/plugins/github-actions/src/components/Router.tsx @@ -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) ? ( `entity.metadata.annotations[' diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 0bb5862d8b..7f8b968c16 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -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';