From b5a82087a7164879ca6c6717329b3e29cc20c3dd Mon Sep 17 00:00:00 2001 From: Jackson Chen <53205189+PeaWarrior@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:39:45 -0500 Subject: [PATCH] techdocs: add extensions for techdocs addons (#28644) * techdocs: add extensions for techdocs addons Signed-off-by: Jackson Chen * techdocs: add blueprint extension for techdocs addons Signed-off-by: Jackson Chen * techdocs: move addons blueprint to alpha Signed-off-by: Jackson Chen * techdocs: add addon extensions for new frontend system and add docs Signed-off-by: Jackson Chen * techdocs: fix addon modules naming patterns Signed-off-by: Jackson Chen * techdocs: update test utils with entity presentation api Signed-off-by: Jackson Chen --------- Signed-off-by: Jackson Chen --- .changeset/strange-masks-type.md | 7 + docs/features/techdocs/addons--new.md | 204 ++++++++++++++++++ docs/features/techdocs/addons.md | 4 + .../src/test-utils.tsx | 15 +- .../package.json | 20 +- .../report-alpha.api.md | 21 ++ .../{LigthBox => LightBox}/LightBox.test.tsx | 0 .../src/{LigthBox => LightBox}/LightBox.tsx | 0 .../src/{LigthBox => LightBox}/index.ts | 0 .../src/{LigthBox => LightBox}/lightbox.css | 0 .../src/alpha.ts | 86 ++++++++ .../src/plugin.ts | 2 +- plugins/techdocs-react/package.json | 20 +- plugins/techdocs-react/report-alpha.api.md | 63 ++++++ plugins/techdocs-react/src/addons.tsx | 2 +- plugins/techdocs-react/src/alpha.ts | 58 +++++ plugins/techdocs/report-alpha.api.md | 30 ++- plugins/techdocs/src/Router.tsx | 21 +- plugins/techdocs/src/alpha.tsx | 64 ++++-- yarn.lock | 2 + 20 files changed, 589 insertions(+), 30 deletions(-) create mode 100644 .changeset/strange-masks-type.md create mode 100644 docs/features/techdocs/addons--new.md create mode 100644 plugins/techdocs-module-addons-contrib/report-alpha.api.md rename plugins/techdocs-module-addons-contrib/src/{LigthBox => LightBox}/LightBox.test.tsx (100%) rename plugins/techdocs-module-addons-contrib/src/{LigthBox => LightBox}/LightBox.tsx (100%) rename plugins/techdocs-module-addons-contrib/src/{LigthBox => LightBox}/index.ts (100%) rename plugins/techdocs-module-addons-contrib/src/{LigthBox => LightBox}/lightbox.css (100%) create mode 100644 plugins/techdocs-module-addons-contrib/src/alpha.ts create mode 100644 plugins/techdocs-react/report-alpha.api.md create mode 100644 plugins/techdocs-react/src/alpha.ts diff --git a/.changeset/strange-masks-type.md b/.changeset/strange-masks-type.md new file mode 100644 index 0000000000..6b4edf9c5e --- /dev/null +++ b/.changeset/strange-masks-type.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Added `TechDocsAddonsBlueprint` extension to allow adding of techdocs addons. diff --git a/docs/features/techdocs/addons--new.md b/docs/features/techdocs/addons--new.md new file mode 100644 index 0000000000..07138d4b8e --- /dev/null +++ b/docs/features/techdocs/addons--new.md @@ -0,0 +1,204 @@ +--- +id: addons +title: TechDocs Addons +description: How to find, use, or create TechDocs Addons. +--- + +:::info +This documentation is written for [the new frontend system](../../frontend-system/index.md) which is still in alpha and is only supported by a small number of plugins. +::: + +## Concepts + +TechDocs is a centralized platform for publishing, viewing, and discovering +technical documentation across an entire organization. It's a solid foundation! +But it doesn't solve higher-order documentation needs on its own: how do you +create and reinforce a culture of documentation? How do you build trust in the +quality of technical documentation? + +TechDocs Addons are a mechanism by which you can customize the TechDocs +experience in order to try and address some of these higher-order needs. + +### Addons + +An Addon is just a react component. Like any react component, it can retrieve +and render data using normal Backstage or native hooks, APIs, and components. +Props can be used to configure its behavior, where appropriate. + +### Locations + +Addons declare a `location` where they will be rendered. Most locations are +representative of physical spaces in the TechDocs UI: + +- `Header`: For Addons which fill up the header from the right, on the same + line as the title. +- `Subheader`: For Addons that sit below the header but above all content. + This is a great location for tooling/configuration of TechDocs display. +- `Settings`: These addons are items added to the settings menu list and are designed to make + the reader experience customizable, for example accessibility options. +- `PrimarySidebar`: Left of the content, above of the navigation. +- `SecondarySidebar`: Right of the content, above the table of contents. +- `Content`: A special location intended for Addons which augment the + statically generated content of the documentation itself. +- `Component`: A [proposed-but-not-yet-implemented](https://github.com/backstage/backstage/issues/11109) + virtual location, aimed at simplifying a common type of Addon. + + + +![TechDocs Addon Location Guide](../../assets/techdocs/addon-locations.png) + +### Addon Registry + +The installation and configuration of Addons happens within a Backstage app's +frontend. Addons are imported from plugins and registered as a plugin extension which +are configured for both the TechDocs Reader page as well as the Entity docs page. + +Addons are rendered in the order in which they are registered. + +## Installing and using Addons + +To start using Addons you need to add the `@backstage/plugin-techdocs-module-addons-contrib` package to your app. You can do that by running this command from the root of your project: `yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib` + +Addons can then be installed as a module in your `App.tsx`: + +```tsx +// packages/app/src/App.tsx + +import { createApp } from '@backstage/frontend-defaults'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { techDocsReportIssueAddonModule } from '@backstage/plugin-techdocs-module-addons-contrib/alpha'; + +// ... + +const app = createApp({ + features: [ + // ... + techDocsReportIssueAddonModule, + // ...other techdocs addon modules + ], +}); + +export default app.createRoot(); +``` + +Note that on the entity page, because the Catalog plugin is responsible for the +page header, TechDocs Addons whose location is `Header` will not be rendered. + +## Available Addons + +Addons can, in principle, be provided by any plugin! To make it easier to +discover available Addons, we've compiled a list of them here: + +| Addon | Package/Plugin | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`techDocsExpandableNavigationAddonModule`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | +| [`techDocsReportIssueAddonModule`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | +| [`techDocsTextSizeAddonModule`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [`techDocsLightBoxAddonModule`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib/alpha` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). | + +Got an Addon to contribute? Feel free to add a row above! + +## Creating an Addon + +The simplest Addons are plain old react components that get rendered in +specific locations within a TechDocs site. To package such a react component as +an Addon, follow these steps: + +1. Write the component in your plugin like any other component +2. Create the addon extension using the `TechDocsAddonBlueprint` +3. Create and export the addon module from your plugin + +```ts +// plugins/your-plugin/src/plugin.ts + +import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { CatGifComponent } from './addons'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +// ... + +const techDocsCatGifAddon = AddonBlueprint.make({ + name: 'cat-gif', + params: { + name: 'CatGif', + location: TechDocsAddonLocations.Header, + component: CatGifComponent, + }, +}); + +export const techDocsCatGifAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsCatGifAddon], +}); +``` + +### Addons in the Content location + +Beyond the "render a component in a region" use-case, it's also possible for +Addons to access and manipulate a TechDocs site's DOM; this could be used to, +for example, load and instantiate client-side diagramming libraries, replace +elements with dynamically loaded content, etc. + +This type of Addon is still expressed as a react component, but instead of +returning a react element to be rendered, it updates the DOM via side-effects +(e.g. with `useEffect`). Access to the DOM is made available via utility hooks +provided by the Addon framework. + +```tsx +// plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx + +import React, { useEffect } from 'react'; +import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; + +// This is a normal react component; in order to make it an Addon, you would +// still create and provide it via your plugin as described above. The only +// difference is that you'd set `location` to `TechDocsAddonLocations.Content`. +export const MakeAllImagesCatGifsAddon = () => { + // This hook can be used to get references to specific elements. If you need + // access to the whole shadow DOM, use the underlying useShadowRoot() + // hook instead. + const images = useShadowRootElements(['img']); + + useEffect(() => { + images.forEach(img => { + if (img.src !== 'https://example.com/cat.gif') { + img.src = 'https://example.com/cat.gif'; + } + }); + }, [images]); + + // Nothing to render directly, so we can just return null. + return null; +}; +``` + +### Testing Addons + +Install `@backstage/plugin-techdocs-addons-test-utils` as a `devDependency` in +your plugin for access to utilities that make testing such Addons easier. + +A test for the above Addon might look something like this: + +```tsx +// plugins/your-plugin/src/addons/MakeAllImagesCatGifs.test.tsx +import { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; + +// Note: import your actual addon (the one provided by your plugin). +import { MakeAllImagesCatGifs } from '../plugin.ts'; + +describe('MakeAllImagesCatGifs', () => { + it('replaces img srcs with cat gif', async () => { + const { getByTestId } = await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom() + .renderWithEffects(); + + expect(getByTestId('fixture')).toHaveAttribute( + 'src', + 'https://example.com/cat.gif', + ); + }); +}); +``` diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index fff1f09d71..a34c2f1162 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -4,6 +4,10 @@ title: TechDocs Addons description: How to find, use, or create TechDocs Addons. --- +:::info +This documentation is written for [the old frontend system](./getting-started.md#adding-techdocs-frontend-plugin). If you are on the [new frontend system](../../frontend-system/index.md) you may want to read [its own article](./addons--new.md) instead. +::: + ## Concepts TechDocs is a centralized platform for publishing, viewing, and discovering diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx index 2066acbb48..1f6337fc23 100644 --- a/plugins/techdocs-addons-test-utils/src/test-utils.tsx +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -38,7 +38,11 @@ import { techdocsStorageApiRef, } from '@backstage/plugin-techdocs-react'; import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; -import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { + EntityPresentationApi, + entityPresentationApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; import { searchApiRef } from '@backstage/plugin-search-react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; @@ -223,8 +227,17 @@ export class TechDocsAddonTester { }), }; + const entityPresentationApi: EntityPresentationApi = { + forEntity: jest.fn().mockReturnValue({ + snapshot: { + primaryTitle: 'Test Entity', + }, + }), + }; + const apis: TechdocsAddonTesterApis = [ [fetchApiRef, fetchApi], + [entityPresentationApiRef, entityPresentationApi], [discoveryApiRef, discoveryApi], [techdocsApiRef, techdocsApi], [techdocsStorageApiRef, techdocsStorageApi], diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 52d74439ce..202051dc62 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -8,9 +8,7 @@ "pluginPackage": "@backstage/plugin-techdocs" }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage", @@ -24,8 +22,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -41,6 +54,7 @@ "dependencies": { "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/integration-react": "workspace:^", "@backstage/plugin-techdocs-react": "workspace:^", diff --git a/plugins/techdocs-module-addons-contrib/report-alpha.api.md b/plugins/techdocs-module-addons-contrib/report-alpha.api.md new file mode 100644 index 0000000000..1e203eb306 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/report-alpha.api.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-techdocs-module-addons-contrib" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { FrontendModule } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +export const techDocsExpandableNavigationAddonModule: FrontendModule; + +// @alpha (undocumented) +export const techDocsLightBoxAddonModule: FrontendModule; + +// @alpha (undocumented) +export const techDocsReportIssueAddonModule: FrontendModule; + +// @alpha (undocumented) +export const techDocsTextSizeAddonModule: FrontendModule; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx similarity index 100% rename from plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx rename to plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.test.tsx diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx b/plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx similarity index 100% rename from plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx rename to plugins/techdocs-module-addons-contrib/src/LightBox/LightBox.tsx diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts b/plugins/techdocs-module-addons-contrib/src/LightBox/index.ts similarity index 100% rename from plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts rename to plugins/techdocs-module-addons-contrib/src/LightBox/index.ts diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css b/plugins/techdocs-module-addons-contrib/src/LightBox/lightbox.css similarity index 100% rename from plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css rename to plugins/techdocs-module-addons-contrib/src/LightBox/lightbox.css diff --git a/plugins/techdocs-module-addons-contrib/src/alpha.ts b/plugins/techdocs-module-addons-contrib/src/alpha.ts new file mode 100644 index 0000000000..acbb808a55 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/alpha.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { TextSizeAddon } from './TextSize'; +import { ReportIssueAddon } from './ReportIssue'; +import { ExpandableNavigationAddon } from './ExpandableNavigation'; +import { LightBoxAddon } from './LightBox'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +const techDocsExpandableNavigationAddon = AddonBlueprint.make({ + name: 'expandable-navigation', + params: { + name: 'ExpandableNavigation', + location: TechDocsAddonLocations.PrimarySidebar, + component: ExpandableNavigationAddon, + }, +}); + +/** @alpha */ +export const techDocsExpandableNavigationAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsExpandableNavigationAddon], +}); + +/** @alpha */ +const techDocsReportIssueAddon = AddonBlueprint.make({ + name: 'report-issue', + params: { + name: 'ReportIssue', + location: TechDocsAddonLocations.Content, + component: ReportIssueAddon, + }, +}); + +/** @alpha */ +export const techDocsReportIssueAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsReportIssueAddon], +}); + +/** @alpha */ +const techDocsTextSizeAddon = AddonBlueprint.make({ + name: 'text-size', + params: { + name: 'TextSize', + location: TechDocsAddonLocations.Settings, + component: TextSizeAddon, + }, +}); + +/** @alpha */ +export const techDocsTextSizeAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsTextSizeAddon], +}); + +/** @alpha */ +const techDocsLightBoxAddon = AddonBlueprint.make({ + name: 'light-box', + params: { + name: 'LightBox', + location: TechDocsAddonLocations.Content, + component: LightBoxAddon, + }, +}); + +/** @alpha */ +export const techDocsLightBoxAddonModule = createFrontendModule({ + pluginId: 'techdocs', + extensions: [techDocsLightBoxAddon], +}); diff --git a/plugins/techdocs-module-addons-contrib/src/plugin.ts b/plugins/techdocs-module-addons-contrib/src/plugin.ts index c4023a72af..ac19d682de 100644 --- a/plugins/techdocs-module-addons-contrib/src/plugin.ts +++ b/plugins/techdocs-module-addons-contrib/src/plugin.ts @@ -22,7 +22,7 @@ import { import { ExpandableNavigationAddon } from './ExpandableNavigation'; import { ReportIssueAddon, ReportIssueProps } from './ReportIssue'; import { TextSizeAddon } from './TextSize'; -import { LightBoxAddon } from './LigthBox'; +import { LightBoxAddon } from './LightBox'; /** * The TechDocs addons contrib plugin diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index d7676ac66a..39b648c64d 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -14,9 +14,7 @@ ] }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage", @@ -30,8 +28,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -49,6 +62,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/version-bridge": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/styles": "^4.11.0", diff --git a/plugins/techdocs-react/report-alpha.api.md b/plugins/techdocs-react/report-alpha.api.md new file mode 100644 index 0000000000..d916d1fcb0 --- /dev/null +++ b/plugins/techdocs-react/report-alpha.api.md @@ -0,0 +1,63 @@ +## API Report File for "@backstage/plugin-techdocs-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ComponentType } from 'react'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; + +// @alpha +export const AddonBlueprint: ExtensionBlueprint<{ + kind: 'addon'; + name: undefined; + params: TechDocsAddonOptions; + output: ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + addon: ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >; + }; +}>; + +// @alpha (undocumented) +export const attachTechDocsAddonComponentData:

( + techDocsAddon: ComponentType

, + data: TechDocsAddonOptions, +) => void; + +// @alpha (undocumented) +export const techDocsAddonDataRef: ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} +>; + +// @public +export const TechDocsAddonLocations: Readonly<{ + readonly Header: 'Header'; + readonly Subheader: 'Subheader'; + readonly Settings: 'Settings'; + readonly PrimarySidebar: 'PrimarySidebar'; + readonly SecondarySidebar: 'SecondarySidebar'; + readonly Content: 'Content'; +}>; + +// @public +export type TechDocsAddonOptions = { + name: string; + location: keyof typeof TechDocsAddonLocations; + component: ComponentType; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/techdocs-react/src/addons.tsx b/plugins/techdocs-react/src/addons.tsx index 54aea0352d..a012ce9d36 100644 --- a/plugins/techdocs-react/src/addons.tsx +++ b/plugins/techdocs-react/src/addons.tsx @@ -49,7 +49,7 @@ export const TechDocsAddons: React.ComponentType< attachComponentData(TechDocsAddons, TECHDOCS_ADDONS_WRAPPER_KEY, true); -const getDataKeyByName = (name: string) => { +export const getDataKeyByName = (name: string) => { return `${TECHDOCS_ADDONS_KEY}.${name.toLocaleLowerCase('en-US')}`; }; diff --git a/plugins/techdocs-react/src/alpha.ts b/plugins/techdocs-react/src/alpha.ts new file mode 100644 index 0000000000..689ec41977 --- /dev/null +++ b/plugins/techdocs-react/src/alpha.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TechDocsAddonOptions } from './types'; +import { attachComponentData } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { getDataKeyByName, TECHDOCS_ADDONS_KEY } from './addons'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types'; + +/** @alpha */ +export const techDocsAddonDataRef = + createExtensionDataRef().with({ + id: 'techdocs.addon', + }); + +/** + * Creates an extension to add addons to the TechDocs standalone reader and entity pages. + * @alpha + */ +export const AddonBlueprint = createExtensionBlueprint({ + kind: 'addon', + attachTo: [ + { id: 'page:techdocs/reader', input: 'addons' }, + { id: 'entity-content:techdocs', input: 'addons' }, + ], + output: [techDocsAddonDataRef], + factory: (params: TechDocsAddonOptions) => [techDocsAddonDataRef(params)], + dataRefs: { + addon: techDocsAddonDataRef, + }, +}); + +/** @alpha */ +export const attachTechDocsAddonComponentData =

( + techDocsAddon: ComponentType

, + data: TechDocsAddonOptions, +) => { + attachComponentData(techDocsAddon, TECHDOCS_ADDONS_KEY, data); + attachComponentData(techDocsAddon, getDataKeyByName(data.name), true); +}; diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index 8c2fc08f66..d7a0a3a7d4 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -17,6 +17,7 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; +import { TechDocsAddonOptions } from '@backstage/plugin-techdocs-react'; // @alpha (undocumented) const _default: FrontendPlugin< @@ -151,8 +152,6 @@ const _default: FrontendPlugin< params: SearchResultListItemBlueprintParams; }>; 'page:techdocs/reader': ExtensionDefinition<{ - kind: 'page'; - name: 'reader'; config: { path: string | undefined; }; @@ -173,7 +172,21 @@ const _default: FrontendPlugin< optional: true; } >; - inputs: {}; + inputs: { + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'page'; + name: 'reader'; params: { defaultPath: string; loader: () => Promise; @@ -234,6 +247,17 @@ const _default: FrontendPlugin< } >; inputs: { + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + } + >; emptyState: ExtensionInput< ConfigurableExtensionDataRef< React_2.JSX.Element, diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 6a0bc0e08e..635b3886b6 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -56,6 +56,26 @@ export const Router = () => { ); }; +export const TechDocsReaderRouter = (props: PropsWithChildren) => { + const { children } = props; + + // Using objects instead of elements, otherwise "outlet" will be null on sub-pages and add-ons won't render + const element = useRoutes([ + { + path: '*', + element: , + children: [ + { + path: '*', + element: children, + }, + ], + }, + ]); + + return element; +}; + export const EmbeddedDocsRouter = ( props: PropsWithChildren<{ emptyState?: React.ReactElement; @@ -90,7 +110,6 @@ export const EmbeddedDocsRouter = ( ) ); } - return element; }; diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index a9ac353753..9ec6b733c5 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -38,16 +38,20 @@ import { } from '@backstage/core-compat-api'; import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; -import { - techdocsApiRef, - techdocsStorageApiRef, -} from '@backstage/plugin-techdocs-react'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; import { TechDocsClient, TechDocsStorageClient } from './client'; import { rootCatalogDocsRouteRef, rootDocsRouteRef, rootRouteRef, } from './routes'; +import { TechDocsReaderLayout } from './reader'; +import { attachTechDocsAddonComponentData } from '@backstage/plugin-techdocs-react/alpha'; +import { + TechDocsAddons, + techdocsApiRef, + techdocsStorageApiRef, +} from '@backstage/plugin-techdocs-react'; /** @alpha */ const techDocsStorageApi = ApiBlueprint.make({ @@ -138,15 +142,32 @@ const techDocsPage = PageBlueprint.make({ * * @alpha */ -const techDocsReaderPage = PageBlueprint.make({ +const techDocsReaderPage = PageBlueprint.makeWithOverrides({ name: 'reader', - params: { - defaultPath: '/docs/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(rootDocsRouteRef), - loader: () => - import('./reader/components/TechDocsReaderPage').then(m => - compatWrapper(), - ), + inputs: { + addons: createExtensionInput([AddonBlueprint.dataRefs.addon]), + }, + factory(originalFactory, { inputs }) { + const addons = inputs.addons.map(output => { + const options = output.get(AddonBlueprint.dataRefs.addon); + const Addon = options.component; + attachTechDocsAddonComponentData(Addon, options); + return ; + }); + + return originalFactory({ + defaultPath: '/docs/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(rootDocsRouteRef), + loader: async () => + await import('./Router').then(({ TechDocsReaderRouter }) => { + return compatWrapper( + + + {addons} + , + ); + }), + }); }, }); @@ -157,6 +178,7 @@ const techDocsReaderPage = PageBlueprint.make({ */ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ inputs: { + addons: createExtensionInput([AddonBlueprint.dataRefs.addon]), emptyState: createExtensionInput( [coreExtensionData.reactElement.optional()], { @@ -172,15 +194,23 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ defaultTitle: 'TechDocs', routeRef: convertLegacyRouteRef(rootCatalogDocsRouteRef), loader: () => - import('./Router').then(({ EmbeddedDocsRouter }) => - compatWrapper( + import('./Router').then(({ EmbeddedDocsRouter }) => { + const addons = context.inputs.addons.map(output => { + const options = output.get(AddonBlueprint.dataRefs.addon); + const Addon = options.component; + attachTechDocsAddonComponentData(Addon, options); + return ; + }); + return compatWrapper( , - ), - ), + > + {addons} + , + ); + }), }, context, ); diff --git a/yarn.lock b/yarn.lock index e125f88eee..6df31ca425 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8113,6 +8113,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-techdocs-addons-test-utils": "workspace:^" @@ -8195,6 +8196,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/version-bridge": "workspace:^"