From 22dce2b644c4750aeb13626b2d6bb3abd5434bbf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 26 Jan 2026 11:07:33 +0100 Subject: [PATCH] techdocs: migrate nfs addons to utility API Signed-off-by: Patrik Oldsberg --- .../techdocs-addons-api-migration-react.md | 7 ++ .changeset/techdocs-addons-api-migration.md | 5 ++ plugins/techdocs-react/report-alpha.api.md | 7 -- plugins/techdocs-react/src/alpha.ts | 8 +- plugins/techdocs/report-alpha.api.md | 28 ++++++ plugins/techdocs/src/alpha/addonsApi.ts | 52 +++++++++++ plugins/techdocs/src/alpha/index.tsx | 86 +++++++++++++------ 7 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 .changeset/techdocs-addons-api-migration-react.md create mode 100644 .changeset/techdocs-addons-api-migration.md create mode 100644 plugins/techdocs/src/alpha/addonsApi.ts diff --git a/.changeset/techdocs-addons-api-migration-react.md b/.changeset/techdocs-addons-api-migration-react.md new file mode 100644 index 0000000000..9a2c5f1da9 --- /dev/null +++ b/.changeset/techdocs-addons-api-migration-react.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs-react': patch +--- + +TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. + +As part of this change, the `techDocsAddonDataRef` alpha export was removed. diff --git a/.changeset/techdocs-addons-api-migration.md b/.changeset/techdocs-addons-api-migration.md new file mode 100644 index 0000000000..f2e481de7e --- /dev/null +++ b/.changeset/techdocs-addons-api-migration.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs addons in the new frontend system now use a Utility API pattern instead of multiple attachment points. The `AddonBlueprint` now uses this new approach, and while addons created with older versions still work, they will produce a deprecation warning and will stop working in a future release. diff --git a/plugins/techdocs-react/report-alpha.api.md b/plugins/techdocs-react/report-alpha.api.md index 1fed274f3d..cb2a86a037 100644 --- a/plugins/techdocs-react/report-alpha.api.md +++ b/plugins/techdocs-react/report-alpha.api.md @@ -31,13 +31,6 @@ export const attachTechDocsAddonComponentData:

( data: TechDocsAddonOptions, ) => void; -// @alpha (undocumented) -export const techDocsAddonDataRef: ConfigurableExtensionDataRef< - TechDocsAddonOptions, - 'techdocs.addon', - {} ->; - // @public export const TechDocsAddonLocations: Readonly<{ readonly Header: 'Header'; diff --git a/plugins/techdocs-react/src/alpha.ts b/plugins/techdocs-react/src/alpha.ts index 83c0444753..031ec6711f 100644 --- a/plugins/techdocs-react/src/alpha.ts +++ b/plugins/techdocs-react/src/alpha.ts @@ -29,8 +29,7 @@ import { /** @alpha */ export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types'; -/** @alpha */ -export const techDocsAddonDataRef = +const techDocsAddonDataRef = createExtensionDataRef().with({ id: 'techdocs.addon', }); @@ -41,10 +40,7 @@ export const techDocsAddonDataRef = */ export const AddonBlueprint = createExtensionBlueprint({ kind: 'addon', - attachTo: [ - { id: 'page:techdocs/reader', input: 'addons' }, - { id: 'entity-content:techdocs', input: 'addons' }, - ], + attachTo: { id: 'api:techdocs/addons', input: 'addons' }, output: [techDocsAddonDataRef], factory: (params: TechDocsAddonOptions) => [techDocsAddonDataRef(params)], dataRefs: { diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index cbdb912938..493d1500da 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -53,6 +53,34 @@ const _default: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:techdocs/addons': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: { + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'api'; + name: 'addons'; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:techdocs/storage': OverridableExtensionDefinition<{ kind: 'api'; name: 'storage'; diff --git a/plugins/techdocs/src/alpha/addonsApi.ts b/plugins/techdocs/src/alpha/addonsApi.ts new file mode 100644 index 0000000000..86d4739f46 --- /dev/null +++ b/plugins/techdocs/src/alpha/addonsApi.ts @@ -0,0 +1,52 @@ +/* + * 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 { + ApiBlueprint, + createApiRef, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { TechDocsAddonOptions } from '@backstage/plugin-techdocs-react'; + +interface TechDocsAddonsApi { + getAddons(): TechDocsAddonOptions[]; +} + +export const techdocsAddonsApiRef = createApiRef({ + id: 'plugin.techdocs.addons', +}); + +export const TechDocsAddonsApiExtension = ApiBlueprint.makeWithOverrides({ + name: 'addons', + inputs: { + addons: createExtensionInput([AddonBlueprint.dataRefs.addon]), + }, + factory(originalFactory, { inputs }) { + const addons = inputs.addons.map(output => + output.get(AddonBlueprint.dataRefs.addon), + ); + return originalFactory(defineParams => + defineParams({ + api: techdocsAddonsApiRef, + deps: {}, + factory: () => ({ + getAddons: () => addons, + }), + }), + ); + }, +}); diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index d3fc94c7a8..b93d5dfb0d 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Suspense } from 'react'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import { createFrontendPlugin, @@ -34,7 +35,11 @@ import { EntityIconLinkBlueprint, } from '@backstage/plugin-catalog-react/alpha'; import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; -import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; +import { + AddonBlueprint, + attachTechDocsAddonComponentData, +} from '@backstage/plugin-techdocs-react/alpha'; +import { TechDocsAddonsApiExtension, techdocsAddonsApiRef } from './addonsApi'; import { TechDocsClient, TechDocsStorageClient } from '../client'; import { rootCatalogDocsRouteRef, @@ -42,7 +47,6 @@ import { rootRouteRef, } from '../routes'; import { TechDocsReaderLayout } from '../reader'; -import { attachTechDocsAddonComponentData } from '@backstage/plugin-techdocs-react/alpha'; import { TechDocsAddons, techdocsApiRef, @@ -152,24 +156,37 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({ 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 ; - }); + factory(originalFactory, { apis, inputs }) { + const addonsApi = apis.get(techdocsAddonsApiRef); return originalFactory({ path: '/docs/:namespace/:kind/:name', routeRef: rootDocsRouteRef, - loader: async () => - await import('../Router').then(({ TechDocsReaderRouter }) => ( + loader: async () => { + // Merge addons from the API with old-style direct attachments + const apiAddons = addonsApi?.getAddons() ?? []; + const directAddons = inputs.addons.map(output => + output.get(AddonBlueprint.dataRefs.addon), + ); + const addonOptions = [...apiAddons, ...directAddons]; + + const addons = addonOptions.map(options => { + const Addon = options.component; + attachTechDocsAddonComponentData(Addon, options); + return ( + + + + ); + }); + + return import('../Router').then(({ TechDocsReaderRouter }) => ( {addons} - )), + )); + }, }); }, }); @@ -191,29 +208,41 @@ const techDocsEntityContent = EntityContentBlueprint.makeWithOverrides({ ), }, factory(originalFactory, context) { + const addonsApi = context.apis.get(techdocsAddonsApiRef); + return originalFactory( { path: 'docs', title: 'TechDocs', routeRef: rootCatalogDocsRouteRef, - loader: () => - 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 ; - }); + loader: () => { + // Merge addons from the API with old-style direct attachments + const apiAddons = addonsApi?.getAddons() ?? []; + const directAddons = context.inputs.addons.map(output => + output.get(AddonBlueprint.dataRefs.addon), + ); + const addonOptions = [...apiAddons, ...directAddons]; + + const addons = addonOptions.map(options => { + const Addon = options.component; + attachTechDocsAddonComponentData(Addon, options); return ( - - {addons} - + + + ); - }), + }); + + return import('../Router').then(({ EmbeddedDocsRouter }) => ( + + {addons} + + )); + }, }, context, ); @@ -244,6 +273,7 @@ export default createFrontendPlugin({ extensions: [ techDocsClientApi, techDocsStorageApi, + TechDocsAddonsApiExtension, techDocsNavItem, techDocsPage, techDocsReaderPage,