docs: migrate feature docs to new frontend system as primary content

Rewrite documentation for TechDocs, Software Templates, Software Catalog,
Search, and Kubernetes features to use the new frontend system as the
primary installation and configuration instructions. Old frontend system
instructions are moved to separate `--old` suffixed files for pages with
substantial legacy content, or updated inline for pages with minimal
old-system content.

Files migrated:
- techdocs/getting-started.md
- techdocs/how-to-guides.md
- software-templates/writing-custom-step-layouts.md
- software-templates/writing-custom-field-extensions.md
- software-templates/index.md
- software-catalog/catalog-customization.md
- search/getting-started.md
- search/how-to-guides.md
- kubernetes/installation.md

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-29 21:47:46 +02:00
parent 0336f92de8
commit 8ec254cd4b
18 changed files with 3269 additions and 1397 deletions
+10 -22
View File
@@ -17,32 +17,20 @@ The first step is to add the Kubernetes frontend plugin to your Backstage applic
yarn --cwd packages/app add @backstage/plugin-kubernetes
```
Once the package has been installed, you need to import the plugin in your app by adding the "Kubernetes" tab to the respective catalog pages.
Once installed, the plugin is automatically available in your app through the default feature discovery. It adds a "Kubernetes" tab to entity pages for entities that have Kubernetes resources associated with them. For more details and alternative installation methods, see [installing plugins](../../frontend-system/building-apps/05-installing-plugins.md).
```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
/* highlight-add-next-line */
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
The Kubernetes tab is shown by default for entities where Kubernetes data is available, based on the entity annotations. You can customize the entity filter for the tab through `app-config.yaml`:
// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityLayout>
{/* other tabs... */}
{/* highlight-add-start */}
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
{/* highlight-add-end */}
</EntityLayout>
);
```yaml title="app-config.yaml"
app:
extensions:
- entity-content:kubernetes:
config:
filter:
metadata.annotations.backstage.io/kubernetes-id:
$exists: true
```
:::note Note
The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
:::
That's it! But now, we need the Kubernetes Backend plugin for the frontend to work.
## Adding Kubernetes Backend plugin
@@ -0,0 +1,351 @@
---
id: getting-started--old
title: Getting Started with Search (Old Frontend System)
description: How to set up and install Backstage Search
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./getting-started.md) instead.
::::
Search functions as a plugin to Backstage, so you will need to use Backstage to
use Search.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
> If you used `npx @backstage/create-app`, and you have a search page defined in
> `packages/app/src/components/search`, skip to
> [`Customizing Search`](#customizing-search) below.
## Adding Search to the Frontend
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react
```
Create a new `packages/app/src/components/search/SearchPage.tsx` file in your
Backstage app with the following contents:
```tsx
import { Content, Header, Page } from '@backstage/core-components';
import { Grid, List, Card, CardContent } from '@material-ui/core';
import {
SearchBar,
SearchResult,
DefaultResultListItem,
SearchFilter,
} from '@backstage/plugin-search-react';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
export const searchPage = (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar />
</Grid>
<Grid item xs={3}>
<Card>
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['experimental', 'production']}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
default:
return (
<DefaultResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
```
Bind the above Search Page to the `/search` route in your
`packages/app/src/App.tsx` file, like this:
```tsx
import { SearchPage } from '@backstage/plugin-search';
import { searchPage } from './components/search/SearchPage';
const routes = (
<FlatRoutes>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
</FlatRoutes>
);
```
### Using the Search Modal
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
## Adding Search to the Backend
Add the following plugins into your backend app:
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-module-pg @backstage/plugin-search-backend-module-catalog @backstage/plugin-search-backend-module-techdocs
```
Then add the following lines:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
// Other plugins...
/* highlight-add-start */
// search plugin
backend.add(import('@backstage/plugin-search-backend'));
// search engines
backend.add(import('@backstage/plugin-search-backend-module-pg'));
// search collators
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));
/* highlight-add-end */
backend.start();
```
With the above setup Search will use the [Lunr](https://github.com/olivernn/lunr.js) in-memory Search Engine but if your have Postgres setup as your database then it will use Postgres as your Search Engine. Learn more in the [Search Engines](./search-engines.md) documentation.
The above also sets up two Collators for you - Catalog and TechDocs - which will index content from these two locations so that you can easily search them. Learn more in the [Collators documentation](./collators.md).
## Customizing Search
### Frontend
The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties,
including `<SearchFilter.Select />` and `<SearchFilter.Checkbox />`. These allow
you to provide values relevant to your Backstage instance that, when selected,
get passed to the backend.
```tsx {2-5,8-11}
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['production', 'experimental']}
/>
</CardContent>
```
If you have advanced filter needs, you can specify your own filter component
like this (although new core filter contributions are welcome):
```tsx
import { useSearch, SearchFilter } from '@backstage/plugin-search-react';
const MyCustomFilter = () => {
// Note: filters contain filter data from other filter components. Be sure
// not to clobber other filters' data!
const { filters, setFilters } = useSearch();
return (/* ... */);
};
// Which could be rendered like this:
<SearchFilter component={MyCustomFilter} />
```
It's good practice for search results to highlight information that was used to
return it in the first place! The code below highlights how you might specify a
custom result item component, using the `<CatalogSearchResultListItem />` component as
an example:
```tsx {7-13}
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
// result.type is the index type defined by the collator.
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
// ...
}
})}
</List>
)}
</SearchResult>
```
> For more advanced customization of the Search frontend, also see how to guides such as [How to implement your own Search API](./how-to-guides.md#how-to-implement-your-own-search-api) and [How to customize search results highlighting styling](./how-to-guides.md#how-to-customize-search-results-highlighting-styling)
### Backend
Backstage Search isn't a search engine itself, rather, it provides an interface
between your Backstage instance and a
[Search Engine](./concepts.md#search-engines) of your choice. Currently, we only
support two engines, an in-memory search Engine called Lunr and Elasticsearch.
See [Search Engines](./search-engines.md) documentation for more information how
to configure these in your Backstage instance.
Backstage Search can be used to power search of anything! Plugins like the
Catalog offer default [collators](./concepts.md#collators) (e.g.
[DefaultCatalogCollator](https://github.com/backstage/backstage/blob/df12cc25aa4934a98bc42ed03c07f64a1a0a9d72/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts))
which are responsible for providing documents
[to be indexed](./concepts.md#documents-and-indices). You can register any
number of collators with the `IndexBuilder` like this:
```typescript
const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine });
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
initialDelay: { seconds: 3 },
});
const everyHourSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { hours: 1 },
timeout: { minutes: 90 },
initialDelay: { seconds: 3 },
});
indexBuilder.addCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
indexBuilder.addCollator({
schedule: everyHourSchedule,
factory: new MyCustomCollatorFactory(),
});
```
Backstage Search builds and maintains its index
[on a schedule](./concepts.md#the-scheduler). You can change how often the
indexes are rebuilt for a given type of document. You may want to do this if
your documents are updated more or less frequently. You can do so by configuring
a scheduled `SchedulerServiceTaskRunner` to pass into the `schedule` value, like this:
```typescript {3}
const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
initialDelay: { seconds: 3 },
});
indexBuilder.addCollator({
schedule: every10MinutesSchedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
```
:::note Note
if you are using the in-memory Lunr search engine, you probably want to
implement a non-distributed `SchedulerServiceTaskRunner` like the following to ensure consistency
if you're running multiple search backend nodes (alternatively, you can configure
the search plugin to use a non-distributed database such as
[SQLite](../../tutorials/configuring-plugin-databases.md#postgresql-and-sqlite-3)):
:::
```typescript
import {
SchedulerServiceTaskRunner,
SchedulerServiceTaskInvocationDefinition,
} from '@backstage/backend-plugin-api';
const schedule: SchedulerServiceTaskRunner = {
run: async (task: SchedulerServiceTaskInvocationDefinition) => {
const startRefresh = async () => {
while (!task.signal?.aborted) {
try {
await task.fn(task.signal);
} catch {
// ignore intentionally
}
await new Promise(resolve => setTimeout(resolve, 600 * 1000));
}
};
startRefresh();
},
};
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
```
> For more advanced customization of the Search backend, also see how to guides such as [How to index TechDocs documents](./how-to-guides.md#how-to-index-techdocs-documents) and [How to limit what can be searched in the Software Catalog](./how-to-guides.md#how-to-limit-what-can-be-searched-in-the-software-catalog)
+60 -159
View File
@@ -4,128 +4,60 @@ title: Getting Started with Search
description: How to set up and install Backstage Search
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./getting-started--old.md)
instead.
::::
Search functions as a plugin to Backstage, so you will need to use Backstage to
use Search.
If you haven't setup Backstage already, start
[here](../../getting-started/index.md).
> If you used `npx @backstage/create-app`, and you have a search page defined in
> `packages/app/src/components/search`, skip to
> [`Customizing Search`](#customizing-search) below.
## Adding Search to the Frontend
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react
```
Create a new `packages/app/src/components/search/SearchPage.tsx` file in your
Backstage app with the following contents:
Once installed, the search plugin is automatically available in your app through
the default feature discovery. It provides a search page at `/search`, a search
navigation item in the sidebar, and a search modal accessible from the sidebar.
For more details and alternative installation methods, see
[installing plugins](../../frontend-system/building-apps/05-installing-plugins.md).
```tsx
import { Content, Header, Page } from '@backstage/core-components';
import { Grid, List, Card, CardContent } from '@material-ui/core';
import {
SearchBar,
SearchResult,
DefaultResultListItem,
SearchFilter,
} from '@backstage/plugin-search-react';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
### Configuring the search page
export const searchPage = (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<SearchBar />
</Grid>
<Grid item xs={3}>
<Card>
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['experimental', 'production']}
/>
</CardContent>
</Card>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
default:
return (
<DefaultResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
The search page can be configured through `app-config.yaml`. For example, to
disable search result tracking:
```yaml title="app-config.yaml"
app:
extensions:
- page:search:
config:
noTrack: true
```
Bind the above Search Page to the `/search` route in your
`packages/app/src/App.tsx` file, like this:
### Search result list items
```tsx
import { SearchPage } from '@backstage/plugin-search';
import { searchPage } from './components/search/SearchPage';
The search page automatically discovers and uses search result list item
extensions provided by installed plugins. For example, the catalog plugin
provides a `CatalogSearchResultListItem` and the TechDocs plugin provides a
`TechDocsSearchResultListItem`. These are automatically registered when the
respective plugins are installed.
const routes = (
<FlatRoutes>
<Route path="/search" element={<SearchPage />}>
{searchPage}
</Route>
</FlatRoutes>
);
```
You can also install additional search result list item extensions using the
`SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`.
### Using the Search Modal
### Search filters
In `Root.tsx`, add the `SidebarSearchModal` component:
```bash
import { SidebarSearchModal } from '@backstage/plugin-search';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
<SidebarSearchModal />
<SidebarDivider />
...
```
For more information about using `Root.tsx`, please see
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
Similarly, search filter extensions are automatically discovered. You can add
custom filters using the `SearchFilterBlueprint` or
`SearchFilterResultTypeBlueprint` from `@backstage/plugin-search-react/alpha`.
## Adding Search to the Backend
@@ -165,70 +97,39 @@ The above also sets up two Collators for you - Catalog and TechDocs - which will
### Frontend
The Search Plugin web library (`@backstage/plugin-search-react`) exposes several default filter types as static properties,
including `<SearchFilter.Select />` and `<SearchFilter.Checkbox />`. These allow
you to provide values relevant to your Backstage instance that, when selected,
get passed to the backend.
The search plugin provides extension points for customizing the search
experience through blueprints. You can add custom search result list items,
filters, and result type filters.
```tsx {2-5,8-11}
<CardContent>
<SearchFilter.Select
name="kind"
values={['Component', 'Template']}
/>
</CardContent>
<CardContent>
<SearchFilter.Checkbox
name="lifecycle"
values={['production', 'experimental']}
/>
</CardContent>
```
If you have advanced filter needs, you can specify your own filter component
like this (although new core filter contributions are welcome):
For example, to create a custom search result list item, use the
`SearchResultListItemBlueprint` from `@backstage/plugin-search-react/alpha`:
```tsx
import { useSearch, SearchFilter } from '@backstage/plugin-search-react';
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
const MyCustomFilter = () => {
// Note: filters contain filter data from other filter components. Be sure
// not to clobber other filters' data!
const { filters, setFilters } = useSearch();
return (/* ... */);
};
// Which could be rendered like this:
<SearchFilter component={MyCustomFilter} />
export const MySearchResultListItem = SearchResultListItemBlueprint.make({
name: 'my-result-item',
params: {
predicate: result => result.type === 'my-custom-type',
component: async () => {
const { MyResultItem } = await import('./components/MyResultItem');
return MyResultItem;
},
},
});
```
It's good practice for search results to highlight information that was used to
return it in the first place! The code below highlights how you might specify a
custom result item component, using the `<CatalogSearchResultListItem />` component as
an example:
Install this in your app by passing it to `createApp`:
```tsx {7-13}
<SearchResult>
{({ results }) => (
<List>
{results.map(result => {
// result.type is the index type defined by the collator.
switch (result.type) {
case 'software-catalog':
return (
<CatalogSearchResultListItem
key={result.document.location}
result={result.document}
highlight={result.highlight}
/>
);
// ...
}
})}
</List>
)}
</SearchResult>
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { MySearchResultListItem } from './search/MySearchResultListItem';
const app = createApp({
features: [MySearchResultListItem],
});
export default app.createRoot();
```
> For more advanced customization of the Search frontend, also see how to guides such as [How to implement your own Search API](./how-to-guides.md#how-to-implement-your-own-search-api) and [How to customize search results highlighting styling](./how-to-guides.md#how-to-customize-search-results-highlighting-styling)
+367
View File
@@ -0,0 +1,367 @@
---
id: how-to-guides--old
title: Search How-To guides (Old Frontend System)
sidebar_label: How-To guides
description: Search How To guides
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./how-to-guides.md) instead.
::::
## How to implement your own Search API
The Search plugin provides implementation of one primary API by default: the
[SearchApi](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L35),
which is responsible for talking to the search-backend to query search results.
There may be occasions where you need to implement this API yourself, to
customize it to your own needs - for example if you have your own search backend
that you want to talk to. The purpose of this guide is to walk you through how
to do that in two steps.
1. Implement the `SearchApi`
[interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31)
according to your needs.
```typescript
export class SearchClient implements SearchApi {
// your implementation
}
```
2. Override the API ref `searchApiRef` with your new implemented API in the
`App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
```
## How to customize fields in the Software Catalog or TechDocs index
Sometimes, you might want to have the ability to control which data passes into the search index
in the catalog collator or customize data for a specific kind. You can easily achieve this
by passing an `entityTransformer` callback to the `DefaultCatalogCollatorFactory`. This behavior
is also possible for the `DefaultTechDocsCollatorFactory`. You can either simply amend the default behavior
or even write an entirely new document (which should still follow some required basic structure).
> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`.
```ts title="packages/backend/src/plugins/search.ts"
const catalogEntityTransformer: CatalogCollatorEntityTransformer = (
entity: Entity,
) => {
if (entity.kind === 'SomeKind') {
return {
// customize here output for 'SomeKind' kind
};
}
return {
// and customize default output
...defaultCatalogCollatorEntityTransformer(entity),
text: 'my super cool text',
};
};
indexBuilder.addCollator({
collator: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: catalogEntityTransformer,
}),
});
const techDocsEntityTransformer: TechDocsCollatorEntityTransformer = (
entity: Entity,
) => {
return {
// add more fields to the index
tags: entity.metadata.tags,
};
};
const techDocsDocumentTransformer: TechDocsCollatorDocumentTransformer = (
doc: MkSearchIndexDoc,
) => {
return {
// add more fields to the index
bost: doc.boost,
};
};
indexBuilder.addCollator({
collator: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: techDocsEntityTransformer,
/* highlight-add-next-line */
documentTransformer: techDocsDocumentTransformer,
}),
});
```
## How to customize search results highlighting styling
The default highlighting styling for matched terms in search results is your
browsers default styles for the `<mark>` HTML tag. If you want to customize
how highlighted terms look you can follow Backstage's guide on how to
[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)
to create an override with your preferred styling.
For example, using the new MUI V4+V5 unified theming method, the following will result
in highlighted words to be bold & underlined:
```typescript jsx title=packages/app/src/theme/theme.ts
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
UnifiedTheme,
} from '@backstage/theme';
export const myLightTheme: UnifiedTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
}),
defaultPageTheme: 'home',
components: {
/** @ts-ignore This is temporarily necessary until MUI V5 transition is completed. */
BackstageHighlightedSearchResultText: {
styleOverrides: {
highlight: {
color: 'inherit',
backgroundColor: 'inherit',
fontWeight: 'bold',
textDecoration: 'underline',
},
},
},
},
});
```
```typescript jsx title= packages/app/src/App.tsx
const app : BackstageApp = createApp({
...
themes: [{
id: 'my-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />)
}]
});
```
Obviously if you wanted a dark theme, you would need to provide that as well.
## How to render search results using extensions
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
### 1. Providing an extension in your plugin package
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
> a rendering function to the `<SearchResult />` component.
Using the example below, you can provide an extension to be used as a search result item:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props:
```tsx
export const YourSearchResultListItemExtension: (
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
) => JSX.Element | null = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
// Only results matching your type will be rendered by this extension
predicate: result => result.type === 'YOUR_RESULT_TYPE',
}),
);
```
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
```tsx title="plugins/your-plugin/src/index.ts"
export { YourSearchResultListItem } from './plugin.ts';
```
For more details, see the [createSearchResultListItemExtension](https://backstage.io/api/stable/functions/_backstage_plugin-search-react.index.createSearchResultListItemExtension.html) API reference.
### 2. Custom search result extension in the SearchPage
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
Here is an example of customizing your `SearchPage`:
```tsx title="packages/app/src/components/searchPage.tsx"
import { Grid, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import {
Page,
Header,
Content,
DocsIcon,
CatalogIcon,
} from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
// This example omits other components, like filter and pagination
const SearchPage = () => (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={12}>
<SearchResult>
<YourSearchResultListItem />
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
export const searchPage = <SearchPage />;
```
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
### 2. Custom search result extension in the SidebarSearchModal
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
```tsx title="packages/app/src/components/Root/Root.tsx"
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
```
### 3. Custom search result extension in a custom SearchModal
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
```tsx title="packages/app/src/components/searchModal.tsx"
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import { DocsIcon, CatalogIcon } from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
<>
<DialogTitle>
<Paper>
<SearchBar />
</Paper>
</DialogTitle>
<DialogContent>
<SearchResult onClick={toggleModal}>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
{/* As a "default" extension, it does not define a predicate function,
so it must be the last child to render results that do not match the above extensions */}
<YourSearchResultListItem />
</SearchResult>
</DialogContent>
</>
);
```
There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions).
+51 -211
View File
@@ -5,6 +5,13 @@ sidebar_label: How-To guides
description: Search How To guides
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./how-to-guides--old.md)
instead.
::::
## How to implement your own Search API
The Search plugin provides implementation of one primary API by default: the
@@ -26,24 +33,9 @@ to do that in two steps.
}
```
2. Override the API ref `searchApiRef` with your new implemented API in the
`App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
```
2. Override the default API extension by creating a custom API extension using
`createApiExtension` from `@backstage/frontend-plugin-api`, and install it
in your app. See the [Utility APIs](../../frontend-system/utility-apis/01-index.md) documentation for details on how to create and install custom API extensions.
## How to customize fields in the Software Catalog or TechDocs index
@@ -119,7 +111,7 @@ how highlighted terms look you can follow Backstage's guide on how to
[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)
to create an override with your preferred styling.
For example, using the new MUI V4+V5 unified theming method, the following will result
For example, using the unified theming method, the following will result
in highlighted words to be bold & underlined:
```typescript jsx title=packages/app/src/theme/theme.ts
@@ -151,211 +143,59 @@ export const myLightTheme: UnifiedTheme = createUnifiedTheme({
});
```
```typescript jsx title= packages/app/src/App.tsx
const app : BackstageApp = createApp({
...
themes: [{
id: 'my-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />)
}]
});
```
Obviously if you wanted a dark theme, you would need to provide that as well.
Custom themes are installed as extensions in the new frontend system. See the
[theming documentation](../../frontend-system/building-apps/02-configuring-extensions.md)
for details on how to install custom themes.
## How to render search results using extensions
Extensions for search results let you customize components used to render search result items, It is possible to provide your own search result item extensions or use the ones provided by plugin packages.
Extensions for search results let you customize components used to render
search result items. It is possible to provide your own search result item
extensions or use the ones provided by plugin packages.
### 1. Providing an extension in your plugin package
### Providing a search result list item extension
> Note: You must use the `plugin.provide()` function to make a search item renderer available. Unlike rendering a list in a standard MUI Table or similar, you cannot simply provide
> a rendering function to the `<SearchResult />` component.
In the new frontend system, search result list item extensions are created
using the `SearchResultListItemBlueprint` from
`@backstage/plugin-search-react/alpha`:
Using the example below, you can provide an extension to be used as a search result item:
```tsx title="plugins/your-plugin/src/extensions.ts"
import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha';
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props:
```tsx
export const YourSearchResultListItemExtension: (
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
) => JSX.Element | null = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
```
Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not:
```tsx title="plugins/your-plugin/src/plugin.ts"
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
// Only results matching your type will be rendered by this extension
export const YourSearchResultListItem = SearchResultListItemBlueprint.make({
name: 'your-result-item',
params: {
predicate: result => result.type === 'YOUR_RESULT_TYPE',
}),
);
component: async () => {
const { YourSearchResultListItem } = await import('./components');
return YourSearchResultListItem;
},
},
});
```
Remember to export your new extension via your plugin's `index.ts` so that it is available from within your app:
The extension is then exported from your plugin's alpha entry point and
automatically discovered when the plugin is installed.
```tsx title="plugins/your-plugin/src/index.ts"
export { YourSearchResultListItem } from './plugin.ts';
If you need to provide a search result list item extension from your app
rather than a plugin, you can install it directly in `createApp`:
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { YourSearchResultListItem } from './search/YourSearchResultListItem';
const app = createApp({
features: [YourSearchResultListItem],
});
export default app.createRoot();
```
For more details, see the [createSearchResultListItemExtension](https://backstage.io/api/stable/functions/_backstage_plugin-search-react.index.createSearchResultListItemExtension.html) API reference.
### Search result item ordering
### 2. Custom search result extension in the SearchPage
Once you have exposed your item renderer via the `plugin.provide()` function, you can now override the default search item renderers and tell the `<SearchResult>` component
which renderers to use. Note that the order of the renderers matters! The first one that matches via its predicate function will be used.
Here is an example of customizing your `SearchPage`:
```tsx title="packages/app/src/components/searchPage.tsx"
import { Grid, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import {
Page,
Header,
Content,
DocsIcon,
CatalogIcon,
} from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
// This example omits other components, like filter and pagination
const SearchPage = () => (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={12}>
<SearchResult>
<YourSearchResultListItem />
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
export const searchPage = <SearchPage />;
```
> **Important**: A default result item extension (one that does not have a predicate) should be placed as the last child, so it can be used only when no other extensions match the result being rendered.
> If a non-default extension is specified, the `DefaultResultListItem` component will be used.
### 2. Custom search result extension in the SidebarSearchModal
You may be using the SidebarSearchModal component. In this case, you can customize the search items in this component as follows:
```tsx title="packages/app/src/components/Root/Root.tsx"
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
```
### 3. Custom search result extension in a custom SearchModal
Assuming you have completely customized your SearchModal, here's an example that renders results with extensions:
```tsx title="packages/app/src/components/searchModal.tsx"
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import { DocsIcon, CatalogIcon } from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
<>
<DialogTitle>
<Paper>
<SearchBar />
</Paper>
</DialogTitle>
<DialogContent>
<SearchResult onClick={toggleModal}>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
{/* As a "default" extension, it does not define a predicate function,
so it must be the last child to render results that do not match the above extensions */}
<YourSearchResultListItem />
</SearchResult>
</DialogContent>
</>
);
```
When multiple search result list item extensions are installed, the search page
uses them to render results based on their predicate functions. The first
extension whose predicate matches a given result is used to render it. Extensions
without a predicate act as fallback renderers and should be ordered last.
There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions).
@@ -0,0 +1,508 @@
---
id: catalog-customization--old
title: Catalog Customization (Old Frontend System)
description: How to add custom filters or interface elements to the Backstage software catalog
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./catalog-customization.md) instead.
::::
The Backstage software catalog comes with a default `CatalogIndexPage` to filter and find catalog entities. This is already set up by default by `@backstage/create-app`. If you want to change the default index page - to set the initially selected filter, adjust columns, add actions, or to add a custom filter to the catalog - the following sections will show you how.
## Pagination
Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of Backstage, so make sure you are on that version or newer to use this feature. To enable pagination you simply need to pass in the `pagination` prop like this:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage pagination />} />
```
## Initially Selected Filter
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage initiallySelectedFilter="all" />}
/>
```
Possible options are: owned, starred, or all
## Initially Selected Kind
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
```
Possible options are all the [default Kinds](system-model.md) as well as any custom Kinds that you have added.
## Owner Picker Mode
The Owner filter by default will only contain a list of Users and/or Groups that actually own an entity in the Catalog, now you may have reason to change this. Here's how:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage ownerPickerMode="all" />} />
```
Possible options are: owners-only or all
## Table Options
The tables used within Backstage are built on top of [`@material-table/core`](https://material-table-core.github.io/) and the `CatalogIndexPage` has a `tableOptions` prop that allows you to customize the underlying table to a certain extent, but there are some hard coded Backstage settings that can't be changed. Here's an example of how to use this prop to disable the search filter field in the table's header:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage tableOptions={{ search: false }} />}
/>
```
There are many options that can be set using `tableOptions`, the full list of settings can be found in the [`@material-table/core` `Options` interface](https://github.com/material-table-core/core/blob/v3.1.0/types/index.d.ts#L323) (this link goes to `v3.1.0` of `@material-table/core` as that is the version currently used by Backstage).
## Customize Columns
The columns you see in the `CatalogIndexPage` were selected to be a good starting point for most, but there may be cases where you would like to add or remove columns from existing or custom Kinds.
### Adding a column to an existing Kind
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* prettier-ignore */ /* highlight-add-end */}
```
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
```tsx title="packages/app/src/App.tsx"
{/* highlight-add-start */}
const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
title: 'User Email',
field: 'entity.spec.profile.email',
render: ({ entity }) => (
<OverflowTooltip
text={entity.spec?.profile?.['email'] || 'N/A'}
placement="bottom-start"
/>
),
});
{/* highlight-add-end */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
{/* highlight-add-next-line */}
createUserEmailColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
```
Finally, we can pass the `myColumnsFunc` to the `CatalogIndexPage` component:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Route
path="/catalog"
element={
<CatalogIndexPage
pagination={{ mode: 'offset', limit: 20 }}
{/* highlight-add-next-line */}
columns={myColumnsFunc}
/>
}
/>
{/* Other routes */}
</FlatRoutes>
)
```
### Adding columns to a custom or specific Kind
Another use case for customization is when adding a custom `Kind`. This feature is available in Backstage >= `v1.23.0`. For example:
```tsx title="packages/app/src/App.tsx"
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
{/* highlight-add-start */}
CatalogTable,
CatalogTableColumnsFunc,
{/* highlight-add-end */}
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'MyKind') {
return [
CatalogTable.columns.createNameColumn(),
CatalogTable.columns.createOwnerColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />
```
:::note Note
In the examples above, the contents of the files have been shortened for simplicity.
:::
## Customize Actions
The `CatalogIndexPage` comes with three default actions - view, edit, and star. You might want to add more.
To do this, first you'll need to add `@mui/utils` to your `packages/app/package.json`:
```sh
yarn --cwd packages/app add @mui/utils
```
Then you'll do the following:
```tsx title="packages/app/src/App.tsx"
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
{/* highlight-add-next-line */}
TableProps,
} from '@backstage/core-components';
import {
CatalogEntityPage,
CatalogIndexPage,
{/* highlight-add-next-line */}
CatalogTableRow,
catalogPlugin,
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
import { Typography } from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { visuallyHidden } from '@mui/utils';
{/* highlight-add-end */}
{/* highlight-add-start */}
const customActions: TableProps<CatalogTableRow>['actions'] = [
({ entity }) => {
const url = 'https://backstage.io/';
const title = `View - ${entity.metadata.name}`;
return {
icon: () => (
<>
<Typography style={visuallyHidden}>{title}</Typography>
<OpenInNew fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
window.open(url, '_blank');
},
};
},
];
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />
```
:::note Note
In the example above, the contents of `App.tsx` has been shortened for simplicity.
:::
The above customization will override the existing actions. Currently, the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168).
## Customize Filters
There are various ways to customize filters: adjusting the existing filters with props, adding or removing default filters, creating brand-new custom filters, etc. The following sections cover these cases:
### Default Filter Props
There are a set of default filters that you can use, which surface all the props mentioned earlier in this document. Here's how they can be used:
```tsx title="packages/app/src/App.tsx"
import { DefaultFilters } from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters
initialKind="Domain"
initiallySelectedFilter="all"
ownerPickerMode="all"
/>
</>
}
/>
}
/>;
```
### Removing Default Filters
If you have reasons not to use the Lifecycle, Tag, and Processing Status filters, here's an example of how to remove them:
```tsx title="packages/app/src/App.tsx"
import {
EntityKindPicker,
EntityTypePicker,
UserListPicker,
EntityOwnerPicker,
EntityNamespacePicker,
} from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityNamespacePicker />
</>
}
/>
}
/>;
```
### Custom Filters
You can add custom filters. For example, suppose that we want to allow filtering by a custom annotation added to entities, `company.com/security-tier`. Here is how we can build a filter to support that need.
First we need to create a new filter that implements the `EntityFilter` interface:
```ts
import { EntityFilter } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
class EntitySecurityTierFilter implements EntityFilter {
constructor(readonly values: string[]) {}
filterEntity(entity: Entity): boolean {
const tier = entity.metadata.annotations?.['company.com/security-tier'];
return tier !== undefined && this.values.includes(tier);
}
}
```
The `EntityFilter` interface permits backend filters, which are passed along to the `catalog-backend` - or frontend filters, which are applied after entities are loaded from the backend.
We'll use this filter to extend the default filters in a type-safe way. Let's create the custom filter shape extending the default somewhere alongside this filter:
```ts
export type CustomFilters = DefaultEntityFilters & {
securityTiers?: EntitySecurityTierFilter;
};
```
To control this filter, we can create a React component that shows checkboxes for the security tiers. This component will make use of the `useEntityList` hook, which accepts this extended filter type as a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) parameter:
```tsx
export const EntitySecurityTierPicker = () => {
// The securityTiers key is recognized due to the CustomFilter generic
const {
filters: { securityTiers },
updateFilters,
} = useEntityList<CustomFilters>();
// Toggles the value, depending on whether it's already selected
function onChange(value: string) {
const newTiers = securityTiers?.values.includes(value)
? securityTiers.values.filter(tier => tier !== value)
: [...(securityTiers?.values ?? []), value];
updateFilters({
securityTiers: newTiers.length
? new EntitySecurityTierFilter(newTiers)
: undefined,
});
}
const tierOptions = ['1', '2', '3'];
return (
<FormControl component="fieldset">
<Typography variant="button">Security Tier</Typography>
<FormGroup>
{tierOptions.map(tier => (
<FormControlLabel
key={tier}
control={
<Checkbox
checked={securityTiers?.values.includes(tier)}
onChange={() => onChange(tier)}
/>
}
label={`Tier ${tier}`}
/>
))}
</FormGroup>
</FormControl>
);
};
```
Now we can add the component to `CatalogIndexPage`:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
import { DefaultFilters } from '@backstage/plugin-catalog-react';
{/* prettier-ignore */ /* highlight-add-end */}
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters />
<EntitySecurityTierPicker />
</>
}
/>
}
/>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
The same method can be used to customize the _default_ filters with a different interface - for such usage, the generic argument isn't needed since the filter shape remains the same as the default.
## Advanced Customization
For those where none of the above fits their needs you can take the option of creating a fully custom `CatalogIndexPage`.
```tsx title="packages/app/src/components/catalog/CustomCatalogIndex.tsx"
import {
PageWithHeader,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { CatalogTable } from '@backstage/plugin-catalog';
import {
EntityListProvider,
CatalogFilterLayout,
EntityKindPicker,
EntityLifecyclePicker,
EntityNamespacePicker,
EntityOwnerPicker,
EntityProcessingStatusPicker,
EntityTagPicker,
EntityTypePicker,
UserListPicker,
} from '@backstage/plugin-catalog-react';
export const CustomCatalogPage = () => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<PageWithHeader title={orgName} themeId="home">
<Content>
<ContentHeader title="">
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider pagination>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
};
```
The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx)
:::note Note
The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
:::
To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route path="/catalog" element={<CatalogIndexPage />}>
<CustomCatalogPage />
</Route>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
@@ -4,512 +4,66 @@ title: Catalog Customization
description: How to add custom filters or interface elements to the Backstage software catalog
---
The Backstage software catalog comes with a default `CatalogIndexPage` to filter and find catalog entities. This is already set up by default by `@backstage/create-app`. If you want to change the default index page - to set the initially selected filter, adjust columns, add actions, or to add a custom filter to the catalog - the following sections will show you how.
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./catalog-customization--old.md)
instead.
::::
## Pagination
The Backstage software catalog comes with a default catalog index page and entity pages that are highly configurable through `app-config.yaml`. This guide covers how to customize the catalog in the new frontend system.
Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of Backstage, so make sure you are on that version or newer to use this feature. To enable pagination you simply need to pass in the `pagination` prop like this:
## Catalog index page
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage pagination />} />
The catalog index page can be configured through extensions in `app-config.yaml`. For example, to enable pagination:
```yaml title="app-config.yaml"
app:
extensions:
- page:catalog:
config:
pagination: true
```
## Initially Selected Filter
You can also configure pagination with additional options:
By default, the initially selected filter defaults to Owned. If you are still building up your catalog this may show an empty list to start. If you would prefer this to show All as the default, here's how you can make that change:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage initiallySelectedFilter="all" />}
/>
```yaml title="app-config.yaml"
app:
extensions:
- page:catalog:
config:
pagination:
mode: offset
limit: 20
```
Possible options are: owned, starred, or all
### Catalog filters
## Initially Selected Kind
The catalog index page includes a set of default filters (kind, type, owner, lifecycle, tag, namespace, processing status). These filters can be configured through extensions. For example, to set the initial kind filter:
By default, the initially selected Kind when viewing the Catalog is Component, but you may have reasons that you want this to be different. Let's say at your Organization they would like it to always default to Domain, here's how you would do that:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />
```yaml title="app-config.yaml"
app:
extensions:
- catalog-filter:catalog/kind:
config:
initialFilter: domain
```
Possible options are all the [default Kinds](system-model.md) as well as any custom Kinds that you have added.
To set the initial list filter to "all" instead of "owned":
## Owner Picker Mode
The Owner filter by default will only contain a list of Users and/or Groups that actually own an entity in the Catalog, now you may have reason to change this. Here's how:
```tsx title="packages/app/src/App.tsx"
<Route path="/catalog" element={<CatalogIndexPage ownerPickerMode="all" />} />
```yaml title="app-config.yaml"
app:
extensions:
- catalog-filter:catalog/list:
config:
initialFilter: all
```
Possible options are: owners-only or all
### Custom filters
## Table Options
You can create custom catalog filters using the `CatalogFilterBlueprint` from `@backstage/plugin-catalog-react/alpha`. See the [extension overrides](../../frontend-system/building-apps/03-extension-overrides.md) documentation for details on how to install custom extensions.
The tables used within Backstage are built on top of [`@material-table/core`](https://material-table-core.github.io/) and the `CatalogIndexPage` has a `tableOptions` prop that allows you to customize the underlying table to a certain extent, but there are some hard coded Backstage settings that can't be changed. Here's an example of how to use this prop to disable the search filter field in the table's header:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage tableOptions={{ search: false }} />}
/>
```
There are many options that can be set using `tableOptions`, the full list of settings can be found in the [`@material-table/core` `Options` interface](https://github.com/material-table-core/core/blob/v3.1.0/types/index.d.ts#L323) (this link goes to `v3.1.0` of `@material-table/core` as that is the version currently used by Backstage).
## Customize Columns
The columns you see in the `CatalogIndexPage` were selected to be a good starting point for most, but there may be cases where you would like to add or remove columns from existing or custom Kinds.
### Adding a column to an existing Kind
Suppose we want to add a new User Email column to the `User` kind in the Catalog. We can do this by overriding the `columns` that we pass into the `CatalogIndexPage` component in our `App.tsx`. First, we need to match the entity kind that we want to override, and then define the columns to show:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* prettier-ignore */ /* highlight-add-end */}
```
Then, we can implement the `createUserEmailColumn` function and add it to the list of columns. `field` is used to access the data from the entity, while `render` lets us customize how we display the data:
```tsx title="packages/app/src/App.tsx"
{/* highlight-add-start */}
const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({
title: 'User Email',
field: 'entity.spec.profile.email',
render: ({ entity }) => (
<OverflowTooltip
text={entity.spec?.profile?.['email'] || 'N/A'}
placement="bottom-start"
/>
),
});
{/* highlight-add-end */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'user') {
return [
// Render existing columns
...CatalogTable.defaultColumnsFunc(entityListContext),
// Add new columns here
{/* highlight-add-next-line */}
createUserEmailColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
```
Finally, we can pass the `myColumnsFunc` to the `CatalogIndexPage` component:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Route
path="/catalog"
element={
<CatalogIndexPage
pagination={{ mode: 'offset', limit: 20 }}
{/* highlight-add-next-line */}
columns={myColumnsFunc}
/>
}
/>
{/* Other routes */}
</FlatRoutes>
)
```
### Adding columns to a custom or specific Kind
Another use case for customization is when adding a custom `Kind`. This feature is available in Backstage >= `v1.23.0`. For example:
```tsx title="packages/app/src/App.tsx"
import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
{/* highlight-add-start */}
CatalogTable,
CatalogTableColumnsFunc,
{/* highlight-add-end */}
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {
if (entityListContext.filters.kind?.value === 'MyKind') {
return [
CatalogTable.columns.createNameColumn(),
CatalogTable.columns.createOwnerColumn(),
];
}
return CatalogTable.defaultColumnsFunc(entityListContext);
};
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />
```
:::note Note
In the examples above, the contents of the files have been shortened for simplicity.
:::
## Customize Actions
The `CatalogIndexPage` comes with three default actions - view, edit, and star. You might want to add more.
To do this, first you'll need to add `@mui/utils` to your `packages/app/package.json`:
```sh
yarn --cwd packages/app add @mui/utils
```
Then you'll do the following:
```tsx title="packages/app/src/App.tsx"
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
{/* highlight-add-next-line */}
TableProps,
} from '@backstage/core-components';
import {
CatalogEntityPage,
CatalogIndexPage,
{/* highlight-add-next-line */}
CatalogTableRow,
catalogPlugin,
} from '@backstage/plugin-catalog';
{/* highlight-add-start */}
import { Typography } from '@material-ui/core';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { visuallyHidden } from '@mui/utils';
{/* highlight-add-end */}
{/* highlight-add-start */}
const customActions: TableProps<CatalogTableRow>['actions'] = [
({ entity }) => {
const url = 'https://backstage.io/';
const title = `View - ${entity.metadata.name}`;
return {
icon: () => (
<>
<Typography style={visuallyHidden}>{title}</Typography>
<OpenInNew fontSize="small" />
</>
),
tooltip: title,
disabled: !url,
onClick: () => {
if (!url) return;
window.open(url, '_blank');
},
};
},
];
{/* highlight-add-end */}
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-next-line */}
<Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />
```
:::note Note
In the example above, the contents of `App.tsx` has been shortened for simplicity.
:::
The above customization will override the existing actions. Currently, the only way to keep them and add your own is to also include the existing actions in your array by copying them from the [`defaultActions`](https://github.com/backstage/backstage/blob/57397e7d6d2d725712c439f4ab93f2ac6aa27bf8/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L113-L168).
## Customize Filters
There are various ways to customize filters: adjusting the existing filters with props, adding or removing default filters, creating brand-new custom filters, etc. The following sections cover these cases:
### Default Filter Props
There are a set of default filters that you can use, which surface all the props mentioned earlier in this document. Here's how they can be used:
```tsx title="packages/app/src/App.tsx"
import { DefaultFilters } from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters
initialKind="Domain"
initiallySelectedFilter="all"
ownerPickerMode="all"
/>
</>
}
/>
}
/>;
```
### Removing Default Filters
If you have reasons not to use the Lifecycle, Tag, and Processing Status filters, here's an example of how to remove them:
```tsx title="packages/app/src/App.tsx"
import {
EntityKindPicker,
EntityTypePicker,
UserListPicker,
EntityOwnerPicker,
EntityNamespacePicker,
} from '@backstage/plugin-catalog-react';
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityNamespacePicker />
</>
}
/>
}
/>;
```
### Custom Filters
You can add custom filters. For example, suppose that we want to allow filtering by a custom annotation added to entities, `company.com/security-tier`. Here is how we can build a filter to support that need.
First we need to create a new filter that implements the `EntityFilter` interface:
```ts
import { EntityFilter } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
class EntitySecurityTierFilter implements EntityFilter {
constructor(readonly values: string[]) {}
filterEntity(entity: Entity): boolean {
const tier = entity.metadata.annotations?.['company.com/security-tier'];
return tier !== undefined && this.values.includes(tier);
}
}
```
The `EntityFilter` interface permits backend filters, which are passed along to the `catalog-backend` - or frontend filters, which are applied after entities are loaded from the backend.
We'll use this filter to extend the default filters in a type-safe way. Let's create the custom filter shape extending the default somewhere alongside this filter:
```ts
export type CustomFilters = DefaultEntityFilters & {
securityTiers?: EntitySecurityTierFilter;
};
```
To control this filter, we can create a React component that shows checkboxes for the security tiers. This component will make use of the `useEntityList` hook, which accepts this extended filter type as a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) parameter:
```tsx
export const EntitySecurityTierPicker = () => {
// The securityTiers key is recognized due to the CustomFilter generic
const {
filters: { securityTiers },
updateFilters,
} = useEntityList<CustomFilters>();
// Toggles the value, depending on whether it's already selected
function onChange(value: string) {
const newTiers = securityTiers?.values.includes(value)
? securityTiers.values.filter(tier => tier !== value)
: [...(securityTiers?.values ?? []), value];
updateFilters({
securityTiers: newTiers.length
? new EntitySecurityTierFilter(newTiers)
: undefined,
});
}
const tierOptions = ['1', '2', '3'];
return (
<FormControl component="fieldset">
<Typography variant="button">Security Tier</Typography>
<FormGroup>
{tierOptions.map(tier => (
<FormControlLabel
key={tier}
control={
<Checkbox
checked={securityTiers?.values.includes(tier)}
onChange={() => onChange(tier)}
/>
}
label={`Tier ${tier}`}
/>
))}
</FormGroup>
</FormControl>
);
};
```
Now we can add the component to `CatalogIndexPage`:
```tsx title="packages/app/src/App.tsx"
{/* prettier-ignore */ /* highlight-add-start */}
import { DefaultFilters } from '@backstage/plugin-catalog-react';
{/* prettier-ignore */ /* highlight-add-end */}
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route
path="/catalog"
element={
<CatalogIndexPage
filters={
<>
<DefaultFilters />
<EntitySecurityTierPicker />
</>
}
/>
}
/>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
The same method can be used to customize the _default_ filters with a different interface - for such usage, the generic argument isn't needed since the filter shape remains the same as the default.
## Advanced Customization
For those where none of the above fits their needs you can take the option of creating a fully custom `CatalogIndexPage`.
```tsx title="packages/app/src/components/catalog/CustomCatalogIndex.tsx"
import {
PageWithHeader,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { CatalogTable } from '@backstage/plugin-catalog';
import {
EntityListProvider,
CatalogFilterLayout,
EntityKindPicker,
EntityLifecyclePicker,
EntityNamespacePicker,
EntityOwnerPicker,
EntityProcessingStatusPicker,
EntityTagPicker,
EntityTypePicker,
UserListPicker,
} from '@backstage/plugin-catalog-react';
export const CustomCatalogPage = () => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<PageWithHeader title={orgName} themeId="home">
<Content>
<ContentHeader title="">
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider pagination>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker />
<EntityTypePicker />
<UserListPicker />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
};
```
The above is a very basic version of a fully custom `CatalogIndexPage`, you'll want to explore the various props to see what you can all do with them. This was built off the building blocks seen in the [`DefaultCatalogPage`](https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx)
:::note Note
The catalog index page is designed to have a minimal code footprint to support easy customization, but creating a replica does introduce a possibility of drifting out of date over time. Be sure to check the catalog [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/catalog/CHANGELOG.md) periodically.
:::
To use this custom `CatalogIndexPage` which we called `CustomCatalogPage`, you'll need to make the following change:
```tsx title="packages/app/src/App.tsx"
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* highlight-remove-next-line */}
<Route path="/catalog" element={<CatalogIndexPage />} />
{/* highlight-add-start */}
<Route path="/catalog" element={<CatalogIndexPage />}>
<CustomCatalogPage />
</Route>
{/* highlight-add-end */}
{/* ... */}
</FlatRoutes>
);
```
## New Frontend System
This section of the documentation explains how to create and configure catalog extensions in the [new frontend system](../../frontend-system/index.md).
:::warning Warning
This section is a work in progress.
:::
## Entity page
### Entity filters
+2 -18
View File
@@ -85,25 +85,9 @@ Once the template has finished running, and from the screenshot above, when its
There could be situations where you would like to disable the
`Register Existing Component` button for your users.
To do so, you need to explicitly disable the default route binding from the `scaffolderPlugin.registerComponent` to the Catalog Import page.
To do so, you can disable the route binding in your `app-config.yaml`:
This can be done in `backstage/packages/app/src/App.tsx`:
```diff
const app = createApp({
apis,
bindRoutes({ bind }) {
bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: false,
- registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
})
```
OR in `app-config.yaml`:
```yaml
```yaml title="app-config.yaml"
app:
routes:
bindings:
@@ -0,0 +1,332 @@
---
id: writing-custom-field-extensions--old
title: Writing Custom Field Extensions (Old Frontend System)
description: How to write your own field extensions
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./writing-custom-field-extensions.md) instead.
::::
Collecting input from the user is a very large part of the scaffolding process
and Software Templates as a whole. Sometimes the built in components and fields
just aren't good enough, and sometimes you want to enrich the form that the
users sees with better inputs that fit better.
This is where `Custom Field Extensions` come in.
With them you can show your own `React` Components and use them to control the
state of the JSON schema, as well as provide your own validation functions to
validate the data too.
## Creating a Field Extension
Field extensions are a way to combine an ID, a `React` Component and a
`validation` function together in a modular way that you can then use to pass to
the `Scaffolder` frontend plugin in your own `App.tsx`.
You can create your own Field Extension by using the
[`createScaffolderFieldExtension`](https://backstage.io/api/stable/variables/_backstage_plugin-scaffolder.index.createScaffolderFieldExtension.html)
`API` like below.
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern:
```tsx
//packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
import type { FieldValidation } from '@rjsf/utils';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
/*
This is the actual component that will get rendered in the form
*/
export const ValidateKebabCase = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<InputLabel htmlFor="validateName">Name</InputLabel>
<Input
id="validateName"
aria-describedby="entityName"
onChange={e => onChange(e.target?.value)}
/>
<FormHelperText id="entityName">
Use only letters, numbers, hyphens and underscores
</FormHelperText>
</FormControl>
);
};
/*
This is a validation function that will run when the form is submitted.
You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\
*/
export const validateKebabCaseValidation = (
value: string,
validation: FieldValidation,
) => {
const kebabCase = /^[a-z0-9-_]+$/g.test(value);
if (kebabCase === false) {
validation.addError(
`Only use letters, numbers, hyphen ("-") and underscore ("_").`,
);
}
};
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts
/*
This is where the magic happens and creates the custom field extension.
Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import {
ValidateKebabCase,
validateKebabCaseValidation,
} from './ValidateKebabCaseExtension';
export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
);
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/index.ts
export { ValidateKebabCaseFieldExtension } from './extensions';
```
Once all these files are in place, you then need to provide your custom
extension to the `scaffolder` plugin.
You do this in `packages/app/src/App.tsx`. You need to provide the
`customFieldExtensions` as children to the `ScaffolderPage`.
```tsx
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />} />
...
</FlatRoutes>
);
```
Should look something like this instead:
```tsx
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<ValidateKebabCaseFieldExtension />
</ScaffolderFieldExtensions>
</Route>
...
</FlatRoutes>
);
```
### Async Validation Function
A validation function can be asynchronous and use [Utility APIs](https://backstage.io/docs/api/utility-apis/) via the `ApiHolder` in the [field validation context](https://backstage.io/api/stable/types/_backstage_plugin-scaffolder-react.index.CustomFieldValidator.html). The example below uses the `catalogApiRef` to check if the submitted value (in this scenario an entity ref) exists in the catalog.
```tsx
import { FieldValidation } from '@rjsf/utils';
import { ApiHolder } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
/*
This validation function checks if the submitted entity ref value is present in the catalog.
*/
export const customFieldExtensionValidator = async (
value: string,
validation: FieldValidation,
context: { apiHolder: ApiHolder },
) => {
const catalogApi = context.apiHolder.get(catalogApiRef);
if ((await catalogApi?.getEntityByRef(value)) === undefined) {
validation.addError('Entity not found');
}
};
```
## Using the Custom Field Extension
Once it's been passed to the `ScaffolderPage` you should now be able to use the
`ui:field` property in your templates to point it to the name of the
`customFieldExtension` that you registered.
Something like this:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: Test template
title: Test template with custom extension
description: Test template
spec:
parameters:
- title: Fill in some steps
required:
- name
properties:
name:
title: Name
type: string
description: My custom name for the component
ui:field: ValidateKebabCase
steps:
[...]
```
## Access Data from other Fields
Custom fields extensions can read data from other fields in the form via the form context. This
is something that we discourage due to the coupling that it creates, but is sometimes still
the most sensible solution.
```tsx
const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => {
const { formData } = props.formContext;
...
};
const CustomFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: ...,
component: CustomFieldExtensionComponent,
validation: ...
})
);
```
## Previewing Custom Field Extensions
You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer
(accessible via the `/create/edit` route by default):
![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png)
In order to make your new custom field extension available in the explorer you will have to define a
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
properties: {
focused: {
description: 'Whether to focus this field',
type: 'boolean',
},
},
},
returnValue: { type: 'string' },
};
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
import { z } from 'zod/v3';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: MyCustomExtensionWithOptionsProps) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
@@ -4,6 +4,13 @@ title: Writing Custom Field Extensions
description: How to write your own field extensions
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./writing-custom-field-extensions--old.md)
instead.
::::
Collecting input from the user is a very large part of the scaffolding process
and Software Templates as a whole. Sometimes the built in components and fields
just aren't good enough, and sometimes you want to enrich the form that the
@@ -18,26 +25,26 @@ validate the data too.
## Creating a Field Extension
Field extensions are a way to combine an ID, a `React` Component and a
`validation` function together in a modular way that you can then use to pass to
the `Scaffolder` frontend plugin in your own `App.tsx`.
`validation` function together in a modular way that you can then register as
an extension in your Backstage app.
You can create your own Field Extension by using the
[`createScaffolderFieldExtension`](https://backstage.io/api/stable/variables/_backstage_plugin-scaffolder.index.createScaffolderFieldExtension.html)
`API` like below.
You can create your own field extension by using the `FormFieldBlueprint` from
`@backstage/plugin-scaffolder-react/alpha` together with `createFormField`, which
types the component, validation, and optional schema.
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern:
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern.
First, create the component and validation function:
```tsx
//packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
// packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
import type { FieldValidation } from '@rjsf/utils';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
/*
This is the actual component that will get rendered in the form
*/
export const ValidateKebabCase = ({
onChange,
rawErrors,
@@ -63,11 +70,6 @@ export const ValidateKebabCase = ({
);
};
/*
This is a validation function that will run when the form is submitted.
You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\
*/
export const validateKebabCaseValidation = (
value: string,
validation: FieldValidation,
@@ -82,71 +84,50 @@ export const validateKebabCaseValidation = (
};
```
Then, create the extension using `FormFieldBlueprint` and `createFormField`:
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts
/*
This is where the magic happens and creates the custom field extension.
Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import {
ValidateKebabCase,
validateKebabCaseValidation,
} from './ValidateKebabCaseExtension';
export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
);
export const ValidateKebabCaseFieldExtension = FormFieldBlueprint.make({
name: 'validate-kebab-case',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
),
},
});
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/index.ts
export { ValidateKebabCaseFieldExtension } from './extensions';
```
Once all these files are in place, you then need to provide your custom
extension to the `scaffolder` plugin.
Once the extension is created, install it in your app by passing it to `createApp`:
You do this in `packages/app/src/App.tsx`. You need to provide the
`customFieldExtensions` as children to the `ScaffolderPage`.
```tsx
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />} />
...
</FlatRoutes>
);
```
Should look something like this instead:
```tsx
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<ValidateKebabCaseFieldExtension />
</ScaffolderFieldExtensions>
</Route>
...
</FlatRoutes>
);
const app = createApp({
features: [ValidateKebabCaseFieldExtension],
});
export default app.createRoot();
```
### Async Validation Function
@@ -158,10 +139,6 @@ import { FieldValidation } from '@rjsf/utils';
import { ApiHolder } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
/*
This validation function checks if the submitted entity ref value is present in the catalog.
*/
export const customFieldExtensionValidator = async (
value: string,
validation: FieldValidation,
@@ -177,11 +154,8 @@ export const customFieldExtensionValidator = async (
## Using the Custom Field Extension
Once it's been passed to the `ScaffolderPage` you should now be able to use the
`ui:field` property in your templates to point it to the name of the
`customFieldExtension` that you registered.
Something like this:
Once registered, you can use the `ui:field` property in your templates to
reference the name of the custom field extension:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
@@ -212,18 +186,30 @@ is something that we discourage due to the coupling that it creates, but is some
the most sensible solution.
```tsx
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => {
const { formData } = props.formContext;
...
};
const CustomFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: ...,
component: CustomFieldExtensionComponent,
validation: ...
})
);
const CustomFieldExtension = FormFieldBlueprint.make({
name: 'custom-field',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'custom-field',
component: CustomFieldExtensionComponent,
validation: ...,
}),
),
},
});
```
## Previewing Custom Field Extensions
@@ -237,7 +223,10 @@ In order to make your new custom field extension available in the explorer you w
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
import FormControl from '@material-ui/core/FormControl';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
@@ -256,7 +245,10 @@ export const MyCustomExtensionWithOptions = ({
rawErrors,
required,
formData,
uiSchema,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
const focused = uiSchema['ui:options']?.focused;
return (
<FormControl
margin="normal"
@@ -271,16 +263,28 @@ export const MyCustomExtensionWithOptions = ({
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import {
MyCustomExtensionWithOptions,
MyCustomExtensionWithOptionsSchema,
} from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
export const MyCustomFieldWithOptionsExtension = FormFieldBlueprint.make({
name: 'MyCustomExtensionWithOptions',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
),
},
});
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
@@ -288,31 +292,33 @@ and the provided `makeFieldSchemaFromZod` helper utility function to generate bo
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
import FormControl from '@material-ui/core/FormControl';
import { z } from 'zod/v3';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
focused: z.boolean().optional().describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
export const MyCustomExtensionWithOptionsSchema =
MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
type MyCustomExtensionWithOptionsProps =
typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
uiSchema,
}: MyCustomExtensionWithOptionsProps) => {
const focused = uiSchema['ui:options']?.focused;
return (
<FormControl
margin="normal"
@@ -0,0 +1,94 @@
---
id: writing-custom-step-layouts--old
title: Writing custom step layouts (Old Frontend System)
description: How to override the default step form layout
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./writing-custom-step-layouts.md) instead.
::::
Every form in each step rendered in the frontend uses the default form layout from [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
This is the same [field](https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-templates#objectfieldtemplate) used by [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component.
## Registering a React component as a custom step layout
The [createScaffolderLayout](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-react.index.createScaffolderLayout.html) function is used to mark a component as a custom step layout:
```tsx
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {
createScaffolderLayout,
LayoutTemplate,
} from '@backstage/plugin-scaffolder-react';
import { Grid } from '@material-ui/core';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
const mid = Math.ceil(properties.length / 2);
return (
<>
<h1>{title}</h1>
<h2>In two column layout!!</h2>
<Grid container justifyContent="flex-end">
{properties.slice(0, mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
{properties.slice(mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
</Grid>
{description}
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
```
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
```tsx
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
...
</FlatRoutes>
);
```
## Using the custom step layout
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
@@ -4,6 +4,13 @@ title: Writing custom step layouts
description: How to override the default step form layout
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./writing-custom-step-layouts--old.md)
instead.
::::
Every form in each step rendered in the frontend uses the default form layout from [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
```yaml
@@ -16,14 +23,12 @@ This is the same [field](https://rjsf-team.github.io/react-jsonschema-form/docs/
## Registering a React component as a custom step layout
The [createScaffolderLayout](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-react.index.createScaffolderLayout.html) function is used to mark a component as a custom step layout:
In the new frontend system, custom step layouts can be registered by creating a scaffolder module plugin that provides the layout through an extension override. Create a new plugin module:
```tsx
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {
createScaffolderLayout,
LayoutTemplate,
} from '@backstage/plugin-scaffolder-react';
```tsx title="packages/app/src/scaffolder/customLayouts.tsx"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import scaffolderPlugin from '@backstage/plugin-scaffolder/alpha';
import { LayoutTemplate } from '@backstage/plugin-scaffolder-react';
import { Grid } from '@material-ui/core';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
@@ -49,37 +54,15 @@ const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
```
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
Use `createScaffolderLayout` from `@backstage/plugin-scaffolder-react` and `scaffolderPlugin.provide` from `@backstage/plugin-scaffolder` to register the layout under the name `TwoColumn`, then install it through a frontend module using `createFrontendModule` together with `scaffolderPlugin.withOverrides` from `@backstage/plugin-scaffolder/alpha`, following the patterns described in the extension overrides guide.
```tsx
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
...
</FlatRoutes>
);
```
For details on how to override and extend extensions in the new frontend system, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
## Using the custom step layout
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
Once the layout is registered, it can be used as a `ui:ObjectFieldTemplate` in your template file:
```yaml
parameters:
@@ -0,0 +1,242 @@
---
id: getting-started--old
title: Getting Started (Old Frontend System)
description: Getting Started Documentation
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./getting-started.md) instead.
::::
TechDocs functions as a plugin in Backstage and ships with it installed out of the box, so you will need to use Backstage to use TechDocs.
If you haven't setup Backstage already, start [here](../../getting-started/index.md).
## Adding TechDocs frontend plugin
The first step is to add the TechDocs plugin to your Backstage application.
Navigate to your new Backstage application directory. And then to your
`packages/app` directory, and install the `@backstage/plugin-techdocs` package.
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-techdocs
```
Once the package has been installed, you need to import the plugin in your app.
In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to
`FlatRoutes`:
```tsx title="packages/app/src/App.tsx"
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
const AppRoutes = () => {
<FlatRoutes>
{/* ... other plugin routes */}
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</FlatRoutes>;
};
```
It would be nice to decorate your pages with something else... Having a link that redirects you to a new issue page when you highlight text in your documentation would be really cool, right? Let's learn how to do this using the TechDocs Addon Framework!
With the [TechDocs Addon framework](https://backstage.io/docs/features/techdocs/addons#installing-and-using-addons), you can render React components in documentation pages and these Addons can be provided by any Backstage plugin. The framework is exported by the [@backstage/plugin-techdocs-react](https://www.npmjs.com/package/@backstage/plugin-techdocs-react) package and there is a `<ReportIssue />` Addon in the [@backstage/plugin-techdocs-module-addons-contrib](https://www.npmjs.com/package/@backstage/plugin-techdocs-module-addons-contrib) package for you to use once you have these two dependencies installed:
```tsx
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
/* highlight-add-start */
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
/* highlight-add-end */
const AppRoutes = () => {
<FlatRoutes>
{/* ... other plugin routes */}
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
{/* highlight-add-start */}
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
{/* highlight-add-end */}
</Route>
</FlatRoutes>;
};
```
I know, you're curious to see how it looks, aren't you? See the image below:
<!-- todo: Needs zoomable plugin -->
![TechDocs Report Issue Add-on](../../assets/techdocs/report-issue-addon.png)
By clicking the open new issue button, you will be redirected to the new issue page according to the source code provider you are using:
<!-- todo: Needs zoomable plugin -->
![TechDocs Report Issue Template](../../assets/techdocs/report-issue-template.png)
That's it! Now, we need the TechDocs Backend plugin for the frontend to work.
## Adding TechDocs Backend plugin
First we need to install the `@backstage/plugin-techdocs-backend` package.
```bash title="From your Backstage root directory"
yarn --cwd packages/backend add @backstage/plugin-techdocs-backend
```
Then in your backend `index.ts` you will add the following line.
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
// Other plugins...
/* highlight-add-start */
backend.add(import('@backstage/plugin-techdocs-backend'));
/* highlight-add-end */
backend.start();
```
That's it! TechDocs frontend and backend have now been added to your Backstage
app. Now let us tweak some configurations to suit your needs.
## Setting the configuration
**See [TechDocs Configuration Options](configuration.md) for complete
configuration reference.**
### Should TechDocs Backend generate docs?
```yaml
techdocs:
builder: 'local'
```
Note that we recommend generating docs on CI/CD instead. Read more in the
"Basic" and "Recommended" sections of the
[TechDocs Architecture](architecture.md). But if you want to get started quickly
set `techdocs.builder` to `'local'` so that TechDocs Backend is responsible for
generating documentation sites. If set to `'external'`, Backstage will assume
that the sites are being generated on each entity's CI/CD pipeline, and are
being stored in a storage somewhere.
When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a
read-only experience where it serves static files from a storage containing all
the generated documentation.
### Choosing storage (publisher)
TechDocs needs to know where to store generated documentation sites and where to
fetch the sites from. This is managed by a
[Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage,
Amazon S3, or local filesystem of Backstage server.
It is okay to use the local filesystem in a "basic" setup when you are trying
out Backstage for the first time. At a later time, review
[Using Cloud Storage](./using-cloud-storage.md).
```yaml
techdocs:
builder: 'local'
publisher:
type: 'local'
```
### Disabling Docker in Docker situation (Optional)
You can skip this if your `techdocs.builder` is set to `'external'`.
The TechDocs Backend plugin runs a docker container with mkdocs installed to
generate the frontend of the docs from source files (Markdown). If you are
deploying Backstage using Docker, this will mean that your Backstage Docker
container will try to run another Docker container for TechDocs Backend.
To avoid this problem, we have a configuration available. You can set a value in
your `app-config.yaml` that tells the techdocs generator if it should run the
`local` mkdocs or run it from `docker`. This defaults to running as `docker` if
no config is provided.
```yaml
techdocs:
builder: 'local'
publisher:
type: 'local'
generator:
runIn: local
```
Setting `generator.runIn` to `local` means you will have to make sure your
environment is compatible with techdocs.
You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from
pip, optionally also `graphviz` and `plantuml` from your OS package manager (e.g.
apt).
You can do so by including the following lines right above `USER node` of your
`Dockerfile`:
```Dockerfile
RUN apt-get update && \
apt-get install -y python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip3 install mkdocs-techdocs-core
```
Please be aware that the version requirement could change, you need to check our
[`Dockerfile`](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
and make sure to match with it.
On a Debian-based Docker container, Python packages must be either installed using
the OS package manager or within a virtual environment (see the
[related PEP](https://peps.python.org/pep-0668/)). Alternative is to use e.g.
[pipx](https://pypa.github.io/pipx/) for installing Python packages in an isolated
environment.
The above Dockerfile snippet installs the latest `mkdocs-techdoc-core` package.
Version numbers can be found in the corresponding
[changelog](https://github.com/backstage/mkdocs-techdocs-core#changelog). In
case you want to pin the version, use the example below:
```Dockerfile
RUN pip3 install mkdocs-techdocs-core==1.2.3
```
Note: We recommend Python version 3.11 or higher.
> Caveat: Please install the `mkdocs-techdocs-core` package after all other
> Python packages. The order is important to make sure we get correct version of
> some of the dependencies.
## Additional reading
- [Creating and publishing your docs](creating-and-publishing.md)
- [Back to README](README.md)
+33 -113
View File
@@ -4,6 +4,13 @@ title: Getting Started
description: Getting Started Documentation
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./getting-started--old.md)
instead.
::::
TechDocs functions as a plugin in Backstage and ships with it installed out of the box, so you will need to use Backstage to use TechDocs.
If you haven't setup Backstage already, start [here](../../getting-started/index.md).
@@ -11,88 +18,37 @@ If you haven't setup Backstage already, start [here](../../getting-started/index
## Adding TechDocs frontend plugin
The first step is to add the TechDocs plugin to your Backstage application.
Navigate to your new Backstage application directory. And then to your
`packages/app` directory, and install the `@backstage/plugin-techdocs` package.
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-techdocs
```
Once the package has been installed, you need to import the plugin in your app.
Once installed, the plugin is automatically available in your app through the default feature discovery. For more details and alternative installation methods, see [installing plugins](../../frontend-system/building-apps/05-installing-plugins.md).
In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to
`FlatRoutes`:
The plugin provides a docs index page at `/docs` and a reader page for individual documentation sites, along with a "Docs" navigation item in the sidebar and a documentation tab on entity pages.
```tsx title="packages/app/src/App.tsx"
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
## Using TechDocs Addons
const AppRoutes = () => {
<FlatRoutes>
{/* ... other plugin routes */}
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</FlatRoutes>;
};
The TechDocs Addon framework lets you render React components in documentation pages. Addons are provided as separate plugin packages that are automatically discovered when installed.
For example, to add the Report Issue addon:
```bash title="From your Backstage root directory"
yarn --cwd packages/app add @backstage/plugin-techdocs-module-addons-contrib
```
It would be nice to decorate your pages with something else... Having a link that redirects you to a new issue page when you highlight text in your documentation would be really cool, right? Let's learn how to do this using the TechDocs Addon Framework!
With the [TechDocs Addon framework](https://backstage.io/docs/features/techdocs/addons#installing-and-using-addons), you can render React components in documentation pages and these Addons can be provided by any Backstage plugin. The framework is exported by the [@backstage/plugin-techdocs-react](https://www.npmjs.com/package/@backstage/plugin-techdocs-react) package and there is a `<ReportIssue />` Addon in the [@backstage/plugin-techdocs-module-addons-contrib](https://www.npmjs.com/package/@backstage/plugin-techdocs-module-addons-contrib) package for you to use once you have these two dependencies installed:
```tsx
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
/* highlight-add-start */
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
/* highlight-add-end */
const AppRoutes = () => {
<FlatRoutes>
{/* ... other plugin routes */}
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
{/* highlight-add-start */}
<TechDocsAddons>
<ReportIssue />
</TechDocsAddons>
{/* highlight-add-end */}
</Route>
</FlatRoutes>;
};
```
I know, you're curious to see how it looks, aren't you? See the image below:
Once installed, the addon is automatically active. You can see it in action when you highlight text in your documentation:
<!-- todo: Needs zoomable plugin -->
![TechDocs Report Issue Add-on](../../assets/techdocs/report-issue-addon.png)
By clicking the open new issue button, you will be redirected to the new issue page according to the source code provider you are using:
By clicking the open new issue button, you are redirected to the new issue page according to the source code provider you are using:
<!-- todo: Needs zoomable plugin -->
![TechDocs Report Issue Template](../../assets/techdocs/report-issue-template.png)
That's it! Now, we need the TechDocs Backend plugin for the frontend to work.
## Adding TechDocs Backend plugin
First we need to install the `@backstage/plugin-techdocs-backend` package.
@@ -115,13 +71,11 @@ backend.add(import('@backstage/plugin-techdocs-backend'));
backend.start();
```
That's it! TechDocs frontend and backend have now been added to your Backstage
app. Now let us tweak some configurations to suit your needs.
That's it! TechDocs frontend and backend have now been added to your Backstage app. Now let us tweak some configurations to suit your needs.
## Setting the configuration
**See [TechDocs Configuration Options](configuration.md) for complete
configuration reference.**
**See [TechDocs Configuration Options](configuration.md) for complete configuration reference.**
### Should TechDocs Backend generate docs?
@@ -130,28 +84,15 @@ techdocs:
builder: 'local'
```
Note that we recommend generating docs on CI/CD instead. Read more in the
"Basic" and "Recommended" sections of the
[TechDocs Architecture](architecture.md). But if you want to get started quickly
set `techdocs.builder` to `'local'` so that TechDocs Backend is responsible for
generating documentation sites. If set to `'external'`, Backstage will assume
that the sites are being generated on each entity's CI/CD pipeline, and are
being stored in a storage somewhere.
Note that we recommend generating docs on CI/CD instead. Read more in the "Basic" and "Recommended" sections of the [TechDocs Architecture](architecture.md). But if you want to get started quickly set `techdocs.builder` to `'local'` so that TechDocs Backend is responsible for generating documentation sites. If set to `'external'`, Backstage will assume that the sites are being generated on each entity's CI/CD pipeline, and are being stored in a storage somewhere.
When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a
read-only experience where it serves static files from a storage containing all
the generated documentation.
When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a read-only experience where it serves static files from a storage containing all the generated documentation.
### Choosing storage (publisher)
TechDocs needs to know where to store generated documentation sites and where to
fetch the sites from. This is managed by a
[Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage,
Amazon S3, or local filesystem of Backstage server.
TechDocs needs to know where to store generated documentation sites and where to fetch the sites from. This is managed by a [Publisher](./concepts.md#techdocs-publisher). Examples: Google Cloud Storage, Amazon S3, or local filesystem of Backstage server.
It is okay to use the local filesystem in a "basic" setup when you are trying
out Backstage for the first time. At a later time, review
[Using Cloud Storage](./using-cloud-storage.md).
It is okay to use the local filesystem in a "basic" setup when you are trying out Backstage for the first time. At a later time, review [Using Cloud Storage](./using-cloud-storage.md).
```yaml
techdocs:
@@ -164,15 +105,9 @@ techdocs:
You can skip this if your `techdocs.builder` is set to `'external'`.
The TechDocs Backend plugin runs a docker container with mkdocs installed to
generate the frontend of the docs from source files (Markdown). If you are
deploying Backstage using Docker, this will mean that your Backstage Docker
container will try to run another Docker container for TechDocs Backend.
The TechDocs Backend plugin runs a docker container with mkdocs installed to generate the frontend of the docs from source files (Markdown). If you are deploying Backstage using Docker, this will mean that your Backstage Docker container will try to run another Docker container for TechDocs Backend.
To avoid this problem, we have a configuration available. You can set a value in
your `app-config.yaml` that tells the techdocs generator if it should run the
`local` mkdocs or run it from `docker`. This defaults to running as `docker` if
no config is provided.
To avoid this problem, we have a configuration available. You can set a value in your `app-config.yaml` that tells the techdocs generator if it should run the `local` mkdocs or run it from `docker`. This defaults to running as `docker` if no config is provided.
```yaml
techdocs:
@@ -183,15 +118,11 @@ techdocs:
runIn: local
```
Setting `generator.runIn` to `local` means you will have to make sure your
environment is compatible with techdocs.
Setting `generator.runIn` to `local` means you will have to make sure your environment is compatible with techdocs.
You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from
pip, optionally also `graphviz` and `plantuml` from your OS package manager (e.g.
apt).
You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from pip, optionally also `graphviz` and `plantuml` from your OS package manager (e.g. apt).
You can do so by including the following lines right above `USER node` of your
`Dockerfile`:
You can do so by including the following lines right above `USER node` of your `Dockerfile`:
```Dockerfile
RUN apt-get update && \
@@ -205,20 +136,11 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip3 install mkdocs-techdocs-core
```
Please be aware that the version requirement could change, you need to check our
[`Dockerfile`](https://github.com/backstage/techdocs-container/blob/main/Dockerfile)
and make sure to match with it.
Please be aware that the version requirement could change, you need to check our [`Dockerfile`](https://github.com/backstage/techdocs-container/blob/main/Dockerfile) and make sure to match with it.
On a Debian-based Docker container, Python packages must be either installed using
the OS package manager or within a virtual environment (see the
[related PEP](https://peps.python.org/pep-0668/)). Alternative is to use e.g.
[pipx](https://pypa.github.io/pipx/) for installing Python packages in an isolated
environment.
On a Debian-based Docker container, Python packages must be either installed using the OS package manager or within a virtual environment (see the [related PEP](https://peps.python.org/pep-0668/)). Alternative is to use e.g. [pipx](https://pypa.github.io/pipx/) for installing Python packages in an isolated environment.
The above Dockerfile snippet installs the latest `mkdocs-techdoc-core` package.
Version numbers can be found in the corresponding
[changelog](https://github.com/backstage/mkdocs-techdocs-core#changelog). In
case you want to pin the version, use the example below:
The above Dockerfile snippet installs the latest `mkdocs-techdoc-core` package. Version numbers can be found in the corresponding [changelog](https://github.com/backstage/mkdocs-techdocs-core#changelog). In case you want to pin the version, use the example below:
```Dockerfile
RUN pip3 install mkdocs-techdocs-core==1.2.3
@@ -226,9 +148,7 @@ RUN pip3 install mkdocs-techdocs-core==1.2.3
Note: We recommend Python version 3.11 or higher.
> Caveat: Please install the `mkdocs-techdocs-core` package after all other
> Python packages. The order is important to make sure we get correct version of
> some of the dependencies.
> Caveat: Please install the `mkdocs-techdocs-core` package after all other Python packages. The order is important to make sure we get correct version of some of the dependencies.
## Additional reading
File diff suppressed because it is too large Load Diff
+32 -252
View File
@@ -5,6 +5,13 @@ sidebar_label: How-To guides
description: TechDocs How-To guides related to TechDocs
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./how-to-guides--old.md)
instead.
::::
## How to migrate from TechDocs Basic to Recommended deployment approach?
The main difference between TechDocs Basic and Recommended deployment approach
@@ -108,241 +115,37 @@ much data git clone has to transfer.
## How to customize the TechDocs home page?
TechDocs uses a composability pattern similar to the Search and Catalog plugins
in Backstage. While a default table experience, similar to the one provided by
the Catalog plugin, is made available for ease-of-use, it's possible for you to
provide a completely custom experience, tailored to the needs of your
organization. For example, TechDocs comes with an alternative grid based layout
(`<EntityListDocsGrid>`) and panel layout (`TechDocsCustomHome`).
in Backstage. The default TechDocs home page provides a table experience
similar to the one provided by the Catalog plugin. TechDocs also comes with
an alternative grid based layout and panel layout.
This is done in your `app` package. By default, you might see something like
this in your `App.tsx`:
```tsx
const AppRoutes = () => {
<FlatRoutes>
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
</FlatRoutes>;
};
```
### Using TechDocsCustomHome
You can easily customize the TechDocs home page using TechDocs panel layout
(`<TechDocsCustomHome />`).
Modify your `App.tsx` as follows:
```tsx
import { Fragment, PropsWithChildren } from 'react';
import { TechDocsCustomHome } from '@backstage/plugin-techdocs';
//...
const options = { emptyRowsWhenPaging: false };
const linkDestination = (entity: Entity): string | undefined => {
return entity.metadata.annotations?.['external-docs'];
};
const techDocsTabsConfig = [
{
label: 'Recommended Documentation',
panels: [
{
title: 'Golden Path',
description: 'Documentation about standards to follow',
panelType: 'DocsCardGrid',
panelProps: { CustomHeader: () => <ContentHeader title='Golden Path'/> },
filterPredicate: entity =>
entity?.metadata?.tags?.includes('golden-path') ?? false,
},
{
title: 'Recommended',
description: 'Useful documentation',
panelType: 'InfoCardGrid',
panelProps: {
CustomHeader: () => <ContentHeader title='Recommended' />
linkDestination: linkDestination,
},
filterPredicate: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
],
},
{
label: 'Browse All',
panels: [
{
description: 'Browse all docs',
filterPredicate: filterEntity,
panelType: 'TechDocsIndexPage',
title: 'All',
panelProps: { PageWrapper: Fragment, CustomHeader: Fragment, options: options },
},
],
},
];
const docsFilter = {
kind: ['Location', 'Resource', 'Component'],
'metadata.annotations.featured-docs': CATALOG_FILTER_EXISTS,
}
const customPageWrapper = ({ children }: PropsWithChildren<{}>) =>
(<PageWithHeader title="Docs" themeId="documentation">{children}</PageWithHeader>)
const AppRoutes = () => {
<FlatRoutes>
<Route
path="/docs"
element={
<TechDocsCustomHome
tabsConfig={techDocsTabsConfig}
filter={docsFilter}
CustomPageWrapper={customPageWrapper}
/>
}
/>
</FlatRoutes>;
};
```
### Building a Custom home page
But you can replace `<DefaultTechDocsHome />` with any React component, which
will be rendered in its place. Most likely, you would want to create and
maintain such a component in a new directory at
`packages/app/src/components/techdocs`, and import and use it in `App.tsx`:
For example, you can define the following Custom home page component:
```tsx
import { ReactNode } from 'react';
import { Content } from '@backstage/core-components';
import {
CatalogFilterLayout,
EntityOwnerPicker,
EntityTagPicker,
UserListPicker,
EntityListProvider,
} from '@backstage/plugin-catalog-react';
import {
TechDocsPageWrapper,
TechDocsPicker,
} from '@backstage/plugin-techdocs';
import { Entity } from '@backstage/catalog-model';
import { EntityListDocsGrid } from '@backstage/plugin-techdocs';
export type CustomTechDocsHomeProps = {
groups?: Array<{
title: ReactNode;
filterPredicate: ((entity: Entity) => boolean) | string;
}>;
};
export const CustomTechDocsHome = ({ groups }: CustomTechDocsHomeProps) => {
return (
<TechDocsPageWrapper>
<Content>
<EntityListProvider>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<TechDocsPicker />
<UserListPicker initialFilter="all" />
<EntityOwnerPicker />
<EntityTagPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<EntityListDocsGrid groups={groups} />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</TechDocsPageWrapper>
);
};
```
Then you can add the following to your `App.tsx`:
```tsx
import { CustomTechDocsHome } from './components/techdocs/CustomTechDocsHome';
// ...
const AppRoutes = () => {
<FlatRoutes>
<Route path="/docs" element={<TechDocsIndexPage />}>
<CustomTechDocsHome
groups={[
{
title: 'Recommended Documentation',
filterPredicate: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
{
title: 'My Docs',
filterPredicate: 'ownedByUser',
},
]}
/>
</Route>
</FlatRoutes>;
};
```
Customization of the TechDocs home page in the new frontend system is done by
overriding the default page extension. For details on how to override
extensions, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
## How to customize the TechDocs reader page?
Similar to how it is possible to customize the TechDocs Home, it is also
possible to customize the TechDocs Reader Page. It is done in your `app`
package. By default, you might see something like this in your `App.tsx`:
The TechDocs reader page can be configured through `app-config.yaml`. For
example, you can disable the in-context search or the header:
```tsx
const AppRoutes = () => {
<Route path="/docs/:namespace/:kind/:name/*" element={<TechDocsReaderPage />}>
{techDocsPage}
</Route>;
};
```yaml title="app-config.yaml"
app:
extensions:
- page:techdocs/reader:
config:
withoutSearch: true
```
The `techDocsPage` is a default techdocs reader page which lives in
`packages/app/src/components/techdocs`. It includes the following without you
having to set anything up.
```tsx
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
<TechDocsReaderPageContent />
</Page>
```yaml title="app-config.yaml"
app:
extensions:
- page:techdocs/reader:
config:
withoutHeader: true
```
If you would like to compose your own `techDocsPage`, you can do so by replacing
the children of TechDocsPage with something else. Maybe you are _just_
interested in replacing the Header:
```tsx
<Page themeId="documentation">
<Header type="documentation" title="Custom Header" />
<TechDocsReaderPageContent />
</Page>
```
Or maybe you want to disable the in-context search
```tsx
<Page themeId="documentation">
<Header type="documentation" title="Custom Header" />
<TechDocsReaderPageContent withSearch={false} />
</Page>
```
Or maybe you want to replace the entire TechDocs Page.
```tsx
<Page themeId="documentation">
<Header type="documentation" title="Custom Header" />
<Content data-testid="techdocs-content">
<p>my own content</p>
</Content>
</Page>
```
For more advanced customization of the reader page, you can override the page
extension. See the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation for details.
## How to migrate from TechDocs Alpha to Beta
@@ -446,32 +249,9 @@ export class TechDocsCustomApiClient implements TechDocsApi {
}
```
2. Override the API refs `techdocsStorageApiRef` and `techdocsApiRef` with your
new implemented APIs in the `App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// TechDocsStorageApi
createApiFactory({
api: techdocsStorageApiRef,
deps: { discoveryApi: discoveryApiRef, configApi: configApiRef },
factory({ discoveryApi, configApi }) {
return new TechDocsCustomStorageApi({ discoveryApi, configApi });
},
}),
// TechDocsApi
createApiFactory({
api: techdocsApiRef,
deps: { discoveryApi: discoveryApiRef },
factory({ discoveryApi }) {
return new TechDocsCustomApiClient({ discoveryApi });
},
}),
],
});
```
2. Override the default API extensions by creating custom API extensions using
`createApiExtension` from `@backstage/frontend-plugin-api`, and install them
in your app. See the [Utility APIs](../../frontend-system/utility-apis/01-index.md) documentation for details on how to create and install custom API extensions.
## How to add the documentation setup to your software templates
+1 -1
View File
@@ -8,7 +8,7 @@ Audience: All
## Overview
The Catalog can be filtered by any combination of owner, kind, type, lifecycle, processing status, namespace, and name. [Customize Filters](../features/software-catalog/catalog-customization.md#customize-filters) provides information on how to modify the available filter criteria.
The Catalog can be filtered by any combination of owner, kind, type, lifecycle, processing status, namespace, and name. [Customize Filters](../features/software-catalog/catalog-customization--old.md#customize-filters) provides information on how to modify the available filter criteria.
![Catalog filter options](../assets/uiguide/catalog-filter-options.png)
+3 -3
View File
@@ -31,7 +31,7 @@ Initially, the Catalog displays registered entities matching the following filte
- `Processing Status` - normal
- `Namespace` - The ID of a [namespace](../features/software-catalog/descriptor-format.md#namespace-optional) to which the entity belongs
You can change the initial setting for the [Owner](../features/software-catalog/catalog-customization.md#initially-selected-filter) and [Kind](../features/software-catalog/catalog-customization.md#initially-selected-kind) filters.
You can change the initial setting for the [Owner](../features/software-catalog/catalog-customization--old.md#initially-selected-filter) and [Kind](../features/software-catalog/catalog-customization--old.md#initially-selected-kind) filters.
## Informational columns for each entity
@@ -55,7 +55,7 @@ For each kind of entity, a set of columns display information regarding the enti
- `Tags` - an optional field that can be used for searching
- `Actions` - see [Catalog Actions](#catalog-actions)
You can modify the columns associated with each kind of entity, following the instructions in [Customize Columns](../features/software-catalog/catalog-customization.md#customize-columns).
You can modify the columns associated with each kind of entity, following the instructions in [Customize Columns](../features/software-catalog/catalog-customization--old.md#customize-columns).
## Catalog Actions
@@ -69,7 +69,7 @@ From left to right, the actions are:
- Edit - Edit the `catalog-info.yaml` file that defines the entity. See [Updating a Component](../getting-started/update-a-component.md)
- Star - Designate the entity as a favorite. You can [filter](../getting-started/filter-catalog.md) the catalog for starred entities.
[Customize Actions](../features/software-catalog/catalog-customization.md#customize-actions) describes how you can modify the actions that are displayed.
[Customize Actions](../features/software-catalog/catalog-customization--old.md#customize-actions) describes how you can modify the actions that are displayed.
## Viewing entity details