docs: update extension config schema examples to new configSchema format

Switch all documentation examples from the deprecated
`config: { schema: { field: z => z.type() } }` pattern to the new
`configSchema: { field: z.type() }` format using zod v4 imports.
Also update catalog-graph README to use current blueprint APIs.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-10 10:58:21 +02:00
parent 1ef7954725
commit 3218028048
6 changed files with 60 additions and 78 deletions
+10 -12
View File
@@ -78,17 +78,16 @@ _Example creating a custom `TechDocsSearchResultItemExtension`_
```tsx
// plugins/techdocs/alpha.tsx
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
import { z } from 'zod';
/** @alpha */
export const TechDocsSearchResultListItemExtension =
createSearchResultListItemExtension({
id: 'techdocs',
configSchema: createSchemaFromZod(z =>
z.object({
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
}),
),
configSchema: {
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
},
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
@@ -188,17 +187,16 @@ Here is the `plugins/techdocs/alpha/index.tsx` final version, and you can also t
// plugins/techdocs/alpha.tsx
import { createPlugin } from '@backstage/frontend-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
import { z } from 'zod';
/** @alpha */
export const TechDocsSearchResultListItemExtension =
createSearchResultListItemExtension({
id: 'techdocs',
configSchema: createSchemaFromZod(z =>
z.object({
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
}),
),
configSchema: {
noTrack: z.boolean().default(false),
lineClamp: z.number().default(5),
},
predicate: result => result.type === 'techdocs',
component: async ({ config }) => {
const { TechDocsSearchResultListItem } = await import(
@@ -278,15 +278,15 @@ In addition to being able to access data passed through the input, you also have
With the `app-config.yaml` there is already the option to pass configuration to plugins or the app to e.g. define the `baseURL` of your app. For extensions this concept would be limiting as an extension can be independent of the plugin & initiated several times. Therefore we created a possibility to configure each extension individually through config. The extension config schema is created using the [`zod`](https://zod.dev/) library, which in addition to TypeScript type checking also provides runtime validation and coercion. If we continue with the example of the `navigationExtension` and now want it to contain a configurable title, we could make it available like the following:
```tsx
import { z } from 'zod';
const navigationExtension = createExtension({
// ...
namespace: 'app',
name: 'nav',
// [3]: Extension `id` will be `app/nav` following the extension naming pattern
config: {
schema: {
title: z => z.string().default('Sidebar Title'),
},
configSchema: {
title: z.string().default('Sidebar Title'),
},
factory({ config }) {
return [
@@ -321,12 +321,12 @@ In all examples so far we have defined the extension factory as a regular functi
For example, this is how we could define an extension where its output depends on the configuration:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
// ...
config: {
schema: {
disableIcon: z.boolean().default(false),
},
configSchema: {
disableIcon: z.boolean().default(false),
},
output: [coreExtensionData.reactElement, iconDataRef.optional()],
*factory({ config }) {
@@ -31,12 +31,12 @@ Every extension blueprint also provides a `makeWithOverrides` method. It is usef
The following is an example of how one might use the blueprint `makeWithOverrides` method to create a new extension:
```tsx
import { z } from 'zod';
const myPageExtension = PageBlueprint.makeWithOverrides({
// This defines additional configuration options for the extension.
config: {
schema: {
layout: z => z.enum(['grid', 'rows']).default('grid'),
},
configSchema: {
layout: z.enum(['grid', 'rows']).default('grid'),
},
// This defines additional inputs for the extension.
inputs: {
@@ -118,10 +118,8 @@ export interface MyWidgetBlueprintParams {
export const MyWidgetBlueprint = createExtensionBlueprint({
kind: 'my-widget',
attachTo: { id: 'page:my-plugin', input: 'widgets' },
config: {
schema: {
title: z.string().optional(),
},
configSchema: {
title: z.string().optional(),
},
output: [coreExtensionData.reactElement],
factory(params: MyWidgetBlueprintParams, { config }) {
@@ -196,10 +194,8 @@ const widgetTitleRef = createExtensionDataRef<string>().with({
export const MyWidgetBlueprint = createExtensionBlueprint({
kind: 'my-widget',
attachTo: { id: 'page:my-plugin', input: 'widgets' },
config: {
schema: {
title: z.string().optional(),
},
configSchema: {
title: z.string().optional(),
},
output: [widgetTitleRef, coreExtensionData.reactElement],
factory(params: MyWidgetBlueprintParams, { config }) {
@@ -182,20 +182,18 @@ const myOverrideExtension = myExtension.override({
Overriding the configuration schema works very similarly to overriding the declared inputs. You can define new configuration fields that will be merged with the existing ones, but you can not re-declare existing fields. The following example shows how to override an extension and add a new configuration field:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
config: {
schema: {
foo: z => z.string(),
},
configSchema: {
foo: z.string(),
},
// ...
});
const overrideExtension = exampleExtension.override({
config: {
schema: {
bar: z => z.string(),
},
configSchema: {
bar: z.string(),
},
factory(originalFactory, { config }) {
//
@@ -210,12 +208,12 @@ const overrideExtension = exampleExtension.override({
In all examples so far we have called the `originalFactory` callback without any arguments. It is however possible to override parts of the factory context for the original factory using the first parameter of the original factory. This can be useful if you want to override the provided configuration or change the inputs in some way. Note that if you are implementing a `factory` for a blueprint, the override factory context will instead be the second parameter of the original factory function. The following is an example of how to override the configuration for the original factory:
```tsx
import { z } from 'zod';
const exampleExtension = createExtension({
name: 'example',
config: {
schema: {
layout: z => z.enum(['grid', 'list']).optional(),
},
configSchema: {
layout: z.enum(['grid', 'list']).optional(),
},
output: [coreExtensionData.reactElement],
factory: ({ config }) => [
@@ -94,11 +94,11 @@ The extension ID of the work API will be the kind `api:` followed by the plugin
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's refactor the example above to also accept configuration, which will require us to use the [override method of the blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides).
```tsx title="in @internal/plugin-example"
import { z } from 'zod';
const exampleWorkApi = ApiBlueprint.makeWithOverrides({
config: {
schema: {
goSlow: z => z.boolean().default(false),
},
configSchema: {
goSlow: z.boolean().default(false),
},
factory(originalFactory, { config }) {
return originalFactory({
+20 -30
View File
@@ -189,31 +189,20 @@ app:
Overriding the card extension allows you to modify how it is implemented.
> [!CAUTION]
> To maintain the same level of configuration, you should define the same or an extended configuration schema.
Here is an example overriding the card extension with a custom component:
```tsx
import { createFrontendModule } from '@backstage/backstage-plugin-api';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha';
export default createFrontendModule({
pluginId: 'catalog-graph',
extensions: [
EntityCardBlueprint.makeWithOverrides({
EntityCardBlueprint.make({
name: 'entity-relations',
config: {
schema: {
filter: z => z.string().optional(),
// Omitting the rest of default configs for simplicity in this example
},
},
factory(originalFactory, { config }) {
return originalFactory({
loader: () =>
import('./components').then(m => <m.MyEntityRelationsCard />),
});
params: {
loader: () =>
import('./components').then(m => <m.MyEntityRelationsCard />),
},
}),
],
@@ -291,29 +280,30 @@ app:
Overriding the page extension allows you to modify how it is implemented.
> [!CAUTION]
> To maintain the same level of configuration, you need to define the same or an extended configuration schema. In order to avoid side effects on external plugins that expect this page to be associated with the default path and route reference, remember to use the same default path so that applications that use the default path still point to the same page and the same route reference.
> In order to avoid side effects on external plugins that expect this page to be associated with the default path and route reference, remember to use the same default path so that applications that use the default path still point to the same page and the same route reference.
Here is example overriding the page extension with a custom component:
Here is an example overriding the page extension with a custom component:
```tsx
import { createFrontendModule, createPageExtension, createSchemaFromZod } from '@backstage/backstage-plugin-api';
import {
createFrontendModule,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { convertLegacyRouteRef } from '@backstage/core-compat-api';
import { catalogGraphRouteRef } from '@backstage/plugin-catalog-graph';
export default createFrontendModule({
pluginId: 'catalog-graph',
extensions: [
createPageExtension({
// Omitting name since it is an index page
path: '/catalog-graph',
routeRef: convertLegacyRouteRef(catalogGraphRouteRef),
createSchemaFromZod(z => z.object({
path: z.string().default('/catalog-graph')
// Omitting the rest of default configs for simplicity in this example
})),
loader: () => import('./components').then(m => <m.CustomEntityRelationsPage />)
})
]
PageBlueprint.make({
params: {
path: '/catalog-graph',
routeRef: convertLegacyRouteRef(catalogGraphRouteRef),
loader: () =>
import('./components').then(m => <m.CustomEntityRelationsPage />),
},
}),
],
});
```