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:
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
|
||||

|
||||
|
||||
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)
|
||||
@@ -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 -->
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
|
||||

|
||||
|
||||
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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user