Skip to content

Commit 56cbf89

Browse files
authored
feat(app): dynamic Techdocs addons (#2238)
* Introduce techdocsFieldExtensions Signed-off-by: Dominika Zemanovicova <[email protected]> * Introduce report issue dynamic plugin Signed-off-by: Dominika Zemanovicova <[email protected]> * Test out addons loading Signed-off-by: Dominika Zemanovicova <[email protected]> * Add textsize Signed-off-by: Dominika Zemanovicova <[email protected]> * Add dependency on plugin-techdocs-react Signed-off-by: Dominika Zemanovicova <[email protected]> * Clean techdocs plugin Signed-off-by: Dominika Zemanovicova <[email protected]> * Add techdocs addons dummy way Signed-off-by: Dominika Zemanovicova <[email protected]> * Merge addons to addons contrib Signed-off-by: Dominika Zemanovicova <[email protected]> * Add back ReportIssue fix Signed-off-by: Dominika Zemanovicova <[email protected]> * Introduce getTechDocsExtentionsData Signed-off-by: Dominika Zemanovicova <[email protected]> * Revert unwanted changes Signed-off-by: Dominika Zemanovicova <[email protected]> * Add plugin to Dockerfile Signed-off-by: Dominika Zemanovicova <[email protected]> * Revert yarn change for scalprum Signed-off-by: Dominika Zemanovicova <[email protected]> * Unify deps Signed-off-by: Dominika Zemanovicova <[email protected]> * Add DynamicConfig tests Signed-off-by: Dominika Zemanovicova <[email protected]> * Enable config for addons Signed-off-by: Dominika Zemanovicova <[email protected]> * Unify techdocs name Signed-off-by: Dominika Zemanovicova <[email protected]> * Fix ReportIssue wrapper not being used Signed-off-by: Dominika Zemanovicova <[email protected]> * Update docs Signed-off-by: Dominika Zemanovicova <[email protected]> * Use techdocsAddons keywords Signed-off-by: Dominika Zemanovicova <[email protected]> * Add default config Signed-off-by: Dominika Zemanovicova <[email protected]> * Introduce staticJSXContent as function Signed-off-by: Dominika Zemanovicova <[email protected]> * StaticJSXContent gets dynamicConfig as argument Signed-off-by: Dominika Zemanovicova <[email protected]> * Update dependencies Signed-off-by: Dominika Zemanovicova <[email protected]> * Fix url Signed-off-by: Dominika Zemanovicova <[email protected]> * Remove importName changes Signed-off-by: Dominika Zemanovicova <[email protected]> * Update docs information about `staticJSXContent` Signed-off-by: Dominika Zemanovicova <[email protected]> * Remove not needed rename Signed-off-by: Dominika Zemanovicova <[email protected]> * Update addons-contrib deps Signed-off-by: Dominika Zemanovicova <[email protected]> * Update janus mention Signed-off-by: Dominika Zemanovicova <[email protected]> * Update dependencies Signed-off-by: Dominika Zemanovicova <[email protected]> * Add ReportIssue as default Signed-off-by: Dominika Zemanovicova <[email protected]> --------- Signed-off-by: Dominika Zemanovicova <[email protected]>
1 parent 658ccd3 commit 56cbf89

29 files changed

+419
-49
lines changed

.rhdh/docker/Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/pagerduty-backstage-plugin
8585
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab/package.json ./dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab/package.json
8686
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab-backend-dynamic/package.json ./dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab-backend-dynamic/package.json
8787
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs/package.json
88+
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/package.json
8889
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs-backend-dynamic/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs-backend-dynamic/package.json
8990
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-signals/package.json ./dynamic-plugins/wrappers/backstage-plugin-signals/package.json
9091
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-signals-backend-dynamic/package.json ./dynamic-plugins/wrappers/backstage-plugin-signals-backend-dynamic/package.json

app-config.dynamic-plugins.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,9 @@ dynamicPlugins:
473473
props:
474474
name: Documentation
475475
icon: docs
476+
backstage.plugin-techdocs-module-addons-contrib:
477+
techdocsAddons:
478+
- importName: ReportIssue
476479
backstage-community.plugin-tech-radar:
477480
apiFactories:
478481
- importName: TechRadarApi

docker/Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/pagerduty-backstage-plugin
8686
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab/package.json ./dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab/package.json
8787
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab-backend-dynamic/package.json ./dynamic-plugins/wrappers/immobiliarelabs-backstage-plugin-gitlab-backend-dynamic/package.json
8888
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs/package.json
89+
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/package.json
8990
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-techdocs-backend-dynamic/package.json ./dynamic-plugins/wrappers/backstage-plugin-techdocs-backend-dynamic/package.json
9091
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-signals/package.json ./dynamic-plugins/wrappers/backstage-plugin-signals/package.json
9192
COPY $EXTERNAL_SOURCE_NESTED/dynamic-plugins/wrappers/backstage-plugin-signals-backend-dynamic/package.json ./dynamic-plugins/wrappers/backstage-plugin-signals-backend-dynamic/package.json

docs/dynamic-plugins/export-derived-package.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ If you are developing your own plugin that is going to be used as a dynamic plug
4343
To be compatible with the showcase dynamic plugin support, and used as dynamic plugins, existing plugins must be based on, or compatible with, the new backend system, as well as rebuilt with a dedicated CLI command.
4444

4545
The new backend system standard entry point (created using `createBackendPlugin()` or `createBackendModule()`) should be exported as the default export of either the main package or of an `alpha` package (if the new backend support is still provided as `alpha` APIs). This doesn't add any additional requirement on top of the standard plugin development guidelines of the new backend system.
46-
For a practical example of a dynamic plugin entry point built upon the new backend system, please refer to the [Janus plugins repository](https://github.com/backstage/community-plugins/blob/main/workspaces/3scale/plugins/3scale-backend/src/module.ts).
46+
For a practical example of a dynamic plugin entry point built upon the new backend system, please refer to the [Backstage community plugins repository](https://github.com/backstage/community-plugins/blob/main/workspaces/3scale/plugins/3scale-backend/src/module.ts).
4747

4848
The dynamic export mechanism identifies private, non-backstage dependencies, and sets the `bundleDependencies` field in the `package.json` file for them, so that the dynamic plugin package can be published as a self-contained package, along with its private dependencies bundled in a private `node_modules` folder.
4949

@@ -130,4 +130,30 @@ export const DynamicEntityTechdocsContent = {
130130
};
131131
```
132132

133+
To include components provided by other dynamic plugins inside static JSX as element children with your dynamically imported component, you can extract them from [dynamicConfig](https://github.com/redhat-developer/rhdh/blob/main/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx#L115) that is passed to the `staticJSXContent`:
134+
135+
```tsx
136+
// Used by a static plugin
137+
export const EntityTechdocsContent = () => {...}
138+
139+
// Custom function that extracts addon components from dynamic config
140+
function getTechdocsAddonComponents(dynamicConfig: DynamicConfig) {
141+
const techdocsAddonsData = dynamicConfig?.techdocsAddons ?? [];
142+
return techdocsAddonsData.map(
143+
({ scope, module, importName, Component, config }) => (
144+
<Component key={`${scope}-${module}-${importName}`} {...config.props} />
145+
),
146+
);
147+
}
148+
149+
// Used by a dynamic plugin
150+
export const DynamicEntityTechdocsContent = {
151+
element: EntityTechdocsContent,
152+
staticJSXContent: (dynamicConfig: DynamicConfig) => {
153+
const children = getTechdocsAddonComponents(dynamicConfig);
154+
return <TechDocsAddons>{children}</TechDocsAddons>;
155+
},
156+
};
157+
```
158+
133159
Important part of the frontend dynamic plugins is its layout configuration (bindings and routes). For more information on how to configure bindings and routes, see [Frontend Plugin Wiring](frontend-plugin-wiring.md).

docs/dynamic-plugins/frontend-plugin-wiring.md

+33
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,39 @@ A plugin can specify multiple field extensions, in which case each field extensi
581581
- `importName` is an optional import name that should reference the value returned the scaffolder field extension API
582582
- `module` is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module named `PluginRoot` is used. This is the same as the key in `scalprum.exposedModules` key in plugin's `package.json`.
583583

584+
## Provide custom TechDocs addons
585+
586+
The Backstage TechDocs component supports specifying [custom addons](https://backstage.io/docs/features/techdocs/addons/) to extend TechDocs functionality, like rendering a component or accessing and manipulating TechDocs's DOM.
587+
588+
Here is an example of creating an addon:
589+
590+
```typescript
591+
export const ExampleAddon = techdocsPlugin.provide(
592+
createTechDocsAddonExtension({
593+
name: "ExampleAddon",
594+
location: TechDocsAddonLocations.Content,
595+
component: ExampleTestAddon,
596+
}),
597+
);
598+
```
599+
600+
These components can be contributed by plugins by exposing the TechDocs addon component via the `techdocsAddons` configuration:
601+
602+
```yaml
603+
dynamicPlugins:
604+
frontend:
605+
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
606+
techdocsAddons:
607+
- importName: ExampleAddon
608+
config:
609+
props: ... # optional, React props to pass to the addon
610+
```
611+
612+
A plugin can specify multiple addons, in which case each techdocsAddon will need to supply an `importName` for each addon.
613+
614+
- `importName` name of an exported `Addon` component
615+
- `module` is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module named `PluginRoot` is used. This is the same as the key in `scalprum.exposedModules` key in plugin's `package.json`.
616+
584617
## Add a custom Backstage theme or replace the provided theme
585618

586619
The look and feel of a Backstage application is handled by Backstage theming. Out of the box Developer Hub provides a theme with a number of [configuration overrides](../customization.md) that allow for user customization. It's also possible to provide additional Backstage themes as well as replace the out of box Developer Hub themes from a dynamic plugin.

dynamic-plugins.default.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,14 @@ plugins:
655655
props:
656656
name: Documentation
657657
icon: docs
658+
- package: ./dynamic-plugins/dist/backstage-plugin-techdocs-module-addons-contrib
659+
disabled: false
660+
pluginConfig:
661+
dynamicPlugins:
662+
frontend:
663+
backstage.plugin-techdocs-module-addons-contrib:
664+
techdocsAddons:
665+
- importName: ReportIssue
658666

659667
# PagerDuty
660668
- package: ./dynamic-plugins/dist/pagerduty-backstage-plugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist-dynamic
2+
dist-scalprum
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const backstageConfig = require('@backstage/cli/config/eslint-factory')(
2+
__dirname,
3+
);
4+
5+
module.exports = {
6+
...backstageConfig,
7+
rules: {
8+
...backstageConfig.rules,
9+
'react/react-in-jsx-scope': 'off',
10+
'react/jsx-uses-react': 'off',
11+
},
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"name": "backstage-plugin-techdocs-module-addons-contrib",
3+
"version": "1.1.20",
4+
"main": "src/index.ts",
5+
"types": "src/index.ts",
6+
"license": "Apache-2.0",
7+
"publishConfig": {
8+
"access": "public",
9+
"main": "dist/index.cjs.js",
10+
"types": "dist/index.d.ts"
11+
},
12+
"backstage": {
13+
"role": "frontend-plugin-module",
14+
"supported-versions": "1.35.1",
15+
"pluginId": "techdocs",
16+
"pluginPackage": "backstage-plugin-techdocs"
17+
},
18+
"sideEffects": false,
19+
"scripts": {
20+
"tsc": "tsc",
21+
"build": "backstage-cli package build",
22+
"lint:check": "backstage-cli package lint",
23+
"test": "backstage-cli package test --passWithNoTests --coverage",
24+
"clean": "backstage-cli package clean",
25+
"export-dynamic": "janus-cli package export-dynamic-plugin --in-place",
26+
"export-dynamic:clean": "run export-dynamic --clean"
27+
},
28+
"dependencies": {
29+
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.20",
30+
"@backstage/plugin-techdocs-react": "1.2.13",
31+
"@material-ui/core": "4.12.4",
32+
"jss": "10.10.0"
33+
},
34+
"peerDependencies": {
35+
"react": "16.13.1 || ^17.0.0 || ^18.0.0"
36+
},
37+
"devDependencies": {
38+
"@backstage/cli": "0.29.6",
39+
"@janus-idp/cli": "3.2.0",
40+
"react": "18.3.1",
41+
"typescript": "5.7.3"
42+
},
43+
"files": [
44+
"dist",
45+
"dist-scalprum"
46+
],
47+
"scalprum": {
48+
"name": "backstage.plugin-techdocs-module-addons-contrib",
49+
"exposedModules": {
50+
"PluginRoot": "./src/index.ts"
51+
}
52+
},
53+
"repository": {
54+
"type": "git",
55+
"url": "https://github.com/redhat-developer/rhdh",
56+
"directory": "dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib"
57+
},
58+
"maintainers": [
59+
"@janus-idp/maintainers-showcase"
60+
],
61+
"author": "Red Hat",
62+
"homepage": "https://red.ht/rhdh",
63+
"bugs": "https://issues.redhat.com/browse/RHIDP",
64+
"keywords": [
65+
"support:production",
66+
"lifecycle:active"
67+
]
68+
}

dynamic-plugins/wrappers/backstage-plugin-techdocs/src/addons.tsx dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/src/addons.tsx

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import React from 'react';
1+
import {
2+
ReportIssue as ReportIssueBase,
3+
techdocsModuleAddonsContribPlugin,
4+
} from "@backstage/plugin-techdocs-module-addons-contrib";
25

3-
import { techdocsPlugin } from '@backstage/plugin-techdocs';
4-
import { createTechDocsAddonExtension, TechDocsAddonLocations } from '@backstage/plugin-techdocs-react';
6+
import React from "react";
57

6-
import { ReportIssue as ReportIssueBase } from "@backstage/plugin-techdocs-module-addons-contrib";
7-
import { ShadowRootStylesProvider } from './ShadowRootStylesProvider';
8+
import {
9+
createTechDocsAddonExtension,
10+
TechDocsAddonLocations,
11+
} from "@backstage/plugin-techdocs-react";
12+
13+
import { ShadowRootStylesProvider } from "./ShadowRootStylesProvider";
814

915
/**
1016
* Automatically wrap the backstage ReportIssue component with a (JSS)
@@ -44,9 +50,9 @@ const ReportIssueWrapper = () => {
4450
);
4551
};
4652

47-
export const ReportIssue = techdocsPlugin.provide(
53+
export const ReportIssue = techdocsModuleAddonsContribPlugin.provide(
4854
createTechDocsAddonExtension<{}>({
49-
name: 'ReportIssue',
55+
name: "ReportIssue",
5056
location: TechDocsAddonLocations.Content,
5157
component: ReportIssueWrapper,
5258
}),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "@backstage/plugin-techdocs-module-addons-contrib";
2+
export { ReportIssue } from "./addons";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "@backstage/cli/config/tsconfig.json",
3+
"include": ["src", "dev", "migrations"],
4+
"exclude": ["node_modules"],
5+
"compilerOptions": {
6+
"outDir": "../../../dist-types/dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib",
7+
"rootDir": ".",
8+
"jsx": "preserve"
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": ["//"],
3+
"tasks": {
4+
"tsc": {
5+
"outputs": [
6+
"../../../dist-types/dynamic-plugins/wrappers/backstage-plugin-techdocs-module-addons-contrib/**"
7+
]
8+
}
9+
}
10+
}

dynamic-plugins/wrappers/backstage-plugin-techdocs/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@
3333
"@backstage/plugin-catalog-react": "1.15.1",
3434
"@backstage/plugin-search-react": "1.8.5",
3535
"@backstage/plugin-techdocs": "1.12.2",
36-
"@backstage/plugin-techdocs-module-addons-contrib": "1.1.20",
37-
"@backstage/plugin-techdocs-react": "1.2.13",
38-
"@material-ui/core": "4.12.4",
39-
"jss": "10.10.0"
36+
"@backstage/plugin-techdocs-react": "1.2.13"
4037
},
4138
"peerDependencies": {
4239
"react": "16.13.1 || ^17.0.0 || ^18.0.0"
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
export * from '@backstage/plugin-techdocs';
1+
export * from "@backstage/plugin-techdocs";
22
export {
3-
EntityTechdocsContent, TechDocsReaderPage, TechdocsSearchFilter,
4-
techdocsSearchType
5-
} from './wrapped';
6-
3+
EntityTechdocsContent,
4+
TechDocsReaderPage,
5+
TechdocsSearchFilter,
6+
techdocsSearchType,
7+
} from "./wrapped";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type TechdocsAddon = {
2+
scope: string;
3+
module: string;
4+
importName: string;
5+
Component: React.ComponentType<React.PropsWithChildren>;
6+
config: {
7+
props?: Record<string, any>;
8+
};
9+
};
10+
11+
export type DynamicConfig = {
12+
techdocsAddons: TechdocsAddon[];
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { DynamicConfig, TechdocsAddon } from "./types";
2+
3+
function getTechdocsAddonData(dynamicConfig: DynamicConfig): TechdocsAddon[] {
4+
return dynamicConfig?.techdocsAddons ?? [];
5+
}
6+
7+
export function getTechdocsAddonComponents(dynamicConfig: DynamicConfig) {
8+
const techdocsAddonsData = getTechdocsAddonData(dynamicConfig);
9+
return techdocsAddonsData.map(
10+
({ scope, module, importName, Component, config }) => (
11+
<Component key={`${scope}-${module}-${importName}`} {...config.props} />
12+
),
13+
);
14+
}

dynamic-plugins/wrappers/backstage-plugin-techdocs/src/wrapped.tsx

+11-14
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import {
22
EntityTechdocsContent as EntityTechdocsContentBase,
33
TechDocsReaderPage as TechDocsReaderPageBase,
44
} from "@backstage/plugin-techdocs";
5-
import { TechDocsAddons } from "@backstage/plugin-techdocs-react";
6-
75
import { SearchFilter, useSearch } from "@backstage/plugin-search-react";
86

97
import {
@@ -12,25 +10,24 @@ import {
1210
} from "@backstage/plugin-catalog-react";
1311

1412
import { useApi } from "@backstage/core-plugin-api";
15-
16-
import { ReportIssue } from "./addons";
13+
import { TechDocsAddons } from "@backstage/plugin-techdocs-react";
14+
import { getTechdocsAddonComponents } from "./utils";
15+
import { type DynamicConfig } from "./types";
1716

1817
export const TechDocsReaderPage = {
1918
element: TechDocsReaderPageBase,
20-
staticJSXContent: (
21-
<TechDocsAddons>
22-
<ReportIssue />
23-
</TechDocsAddons>
24-
),
19+
staticJSXContent: (dynamicConfig: DynamicConfig) => {
20+
const children = getTechdocsAddonComponents(dynamicConfig);
21+
return <TechDocsAddons>{children}</TechDocsAddons>;
22+
},
2523
};
2624

2725
export const EntityTechdocsContent = {
2826
element: EntityTechdocsContentBase,
29-
staticJSXContent: (
30-
<TechDocsAddons>
31-
<ReportIssue />
32-
</TechDocsAddons>
33-
),
27+
staticJSXContent: (dynamicConfig: DynamicConfig) => {
28+
const children = getTechdocsAddonComponents(dynamicConfig);
29+
return <TechDocsAddons>{children}</TechDocsAddons>;
30+
},
3431
};
3532

3633
export const TechdocsSearchFilter = () => {

0 commit comments

Comments
 (0)