techdocs: migrate nfs addons to utility API

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-01-26 11:07:33 +01:00
parent 9554c36e4c
commit 22dce2b644
7 changed files with 152 additions and 41 deletions
@@ -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.
@@ -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.
@@ -31,13 +31,6 @@ export const attachTechDocsAddonComponentData: <P>(
data: TechDocsAddonOptions,
) => void;
// @alpha (undocumented)
export const techDocsAddonDataRef: ConfigurableExtensionDataRef<
TechDocsAddonOptions,
'techdocs.addon',
{}
>;
// @public
export const TechDocsAddonLocations: Readonly<{
readonly Header: 'Header';
+2 -6
View File
@@ -29,8 +29,7 @@ import {
/** @alpha */
export type { TechDocsAddonOptions, TechDocsAddonLocations } from './types';
/** @alpha */
export const techDocsAddonDataRef =
const techDocsAddonDataRef =
createExtensionDataRef<TechDocsAddonOptions>().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: {
+28
View File
@@ -53,6 +53,34 @@ const _default: OverridableFrontendPlugin<
params: ApiFactory<TApi, TImpl, TDeps>,
) => ExtensionBlueprintParams<AnyApiFactory>;
}>;
'api:techdocs/addons': OverridableExtensionDefinition<{
config: {};
configInput: {};
output: ExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
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<TApi, TImpl, TDeps>,
) => ExtensionBlueprintParams<AnyApiFactory>;
}>;
'api:techdocs/storage': OverridableExtensionDefinition<{
kind: 'api';
name: 'storage';
+52
View File
@@ -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<TechDocsAddonsApi>({
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,
}),
}),
);
},
});
+58 -28
View File
@@ -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 <Addon key={options.name} />;
});
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 (
<Suspense key={options.name} fallback={null}>
<Addon />
</Suspense>
);
});
return import('../Router').then(({ TechDocsReaderRouter }) => (
<TechDocsReaderRouter>
<TechDocsReaderLayout />
<TechDocsAddons>{addons}</TechDocsAddons>
</TechDocsReaderRouter>
)),
));
},
});
},
});
@@ -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 <Addon key={options.name} />;
});
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 (
<EmbeddedDocsRouter
emptyState={context.inputs.emptyState?.get(
coreExtensionData.reactElement,
)}
>
<TechDocsAddons>{addons}</TechDocsAddons>
</EmbeddedDocsRouter>
<Suspense key={options.name} fallback={null}>
<Addon />
</Suspense>
);
}),
});
return import('../Router').then(({ EmbeddedDocsRouter }) => (
<EmbeddedDocsRouter
emptyState={context.inputs.emptyState?.get(
coreExtensionData.reactElement,
)}
>
<TechDocsAddons>{addons}</TechDocsAddons>
</EmbeddedDocsRouter>
));
},
},
context,
);
@@ -244,6 +273,7 @@ export default createFrontendPlugin({
extensions: [
techDocsClientApi,
techDocsStorageApi,
TechDocsAddonsApiExtension,
techDocsNavItem,
techDocsPage,
techDocsReaderPage,