feat: catalog export feature

Signed-off-by: the-serious-programmer <19777147+the-serious-programmer@users.noreply.github.com>
This commit is contained in:
the-serious-programmer
2026-04-18 14:29:30 +02:00
parent c3ca20c864
commit 82cf16f63c
36 changed files with 2935 additions and 55 deletions
@@ -20,6 +20,198 @@ Initial support for pagination of the `CatalogIndexPage` was added in v1.21.0 of
<Route path="/catalog" element={<CatalogIndexPage pagination />} />
```
## Export
To enable the catalog export you need to pass in the `exportSettings` prop with `enabled: true`:
```tsx title="packages/app/src/App.tsx"
<Route
path="/catalog"
element={<CatalogIndexPage exportSettings={{ enabled: true }} />}
/>
```
This will enable a button, which by default contains options to export data from the catalog table in CSV and JSON format. When exporting, a dialog opens that lets the user choose the export format and select which columns to include.
### Customizing export
You can customize the export behavior by configuring the `exportSettings` prop with various options via the `CatalogExportSettings` interface:
```tsx
export interface CatalogExportSettings {
enabled?: boolean;
/**
* Array of columns to include in the export.
*
* Each column requires an `entityFilterKey` (dot-separated path into the entity object that is returned by the catalog api) and an optional `title` for display.
* When `title` is omitted, `entityFilterKey` is used as the display title.
*
* Default columns are: name, type, owner and description.
**/
columns?: CatalogExportSettingsColumn;
/**
* Map of custom export format handlers.
*
* Each map entry provides an exporter function and an optional display label.
* Custom formats appear in the export dialog alongside built-in CSV and JSON options.
**/
exporters?: Record<string, CatalogExporterConfig>;
/** Callback function invoked after successful export completion. Useful for displaying notifications or triggering post-export actions. */
onSuccess?: () => void;
/** Callback function invoked if export fails. Receives an object containing the Error for error handling and user notification. */
onError?: (options: { error: Error }) => void;
/** When true, hides the built-in CSV and JSON export options. Useful when only custom exporters should be available. */
disableBuiltinExporters?: boolean;
}
```
#### Custom export columns
By default, the export includes name, type, owner and description columns.
When the export dialog opens, all configured columns are shown as checkboxes and pre-selected.
The user can deselect any columns they want to exclude before confirming the export.
You can customize the available columns:
```tsx title="packages/app/src/App.tsx"
import { CatalogIndexPage } from '@backstage/plugin-catalog';
const customColumns = [
{ entityFilterKey: 'metadata.name', title: 'Name' },
{ entityFilterKey: 'metadata.namespace', title: 'Namespace' },
{ entityFilterKey: 'spec.owner', title: 'Owner' },
];
<CatalogIndexPage
exportSettings={{
enabled: true,
columns: customColumns,
}}
/>;
```
#### Custom export formats
You can add custom export format types beyond CSV and JSON by providing custom exporter functions.
Custom exporters use **async generators** to enable streaming downloads.
The data is written to disk as it's generated, without buffering the entire export in memory in supported browsers.
```tsx title="packages/app/src/App.tsx"
import {
CatalogIndexPage,
CatalogExporter,
CatalogExporterConfig,
} from '@backstage/plugin-catalog';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
// Custom exporter using async generator for streaming
const xmlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => {
const catalogApi = apis.get(catalogApiRef);
// Return an async generator that yields XML chunks
async function* generateXml() {
yield '<?xml version="1.0" encoding="UTF-8"?>\n<entities>\n';
for await (const page of catalogApi.streamEntities(streamRequest)) {
for (const entity of page) {
// Serialize each entity to XML and yield immediately
yield serializeEntityToXml(entity, columns);
}
}
yield '</entities>';
}
return {
generator: generateXml(),
contentType: 'application/xml',
};
};
const yamlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => {
const catalogApi = apis.get(catalogApiRef);
async function* generateYaml() {
for await (const page of catalogApi.streamEntities(streamRequest)) {
for (const entity of page) {
yield serializeEntityToYaml(entity, columns);
yield '---\n'; // YAML document separator
}
}
}
return {
generator: generateYaml(),
contentType: 'application/x-yaml',
};
};
const exporters: Record<string, CatalogExporterConfig> = {
xml: { exporter: xmlExporter, label: 'XML' },
yaml: { exporter: yamlExporter, label: 'YAML' },
};
<CatalogIndexPage
exportSettings={{
enabled: true,
exporters,
}}
/>;
```
When custom export formats are provided, they will appear in the export dialog alongside the built-in CSV and JSON options.
#### Success/Error callbacks
You can also provide callbacks to handle successful or failed exports:
```tsx title="packages/app/src/App.tsx"
<CatalogIndexPage
exportSettings={{
enabled: true,
onSuccess: () => {
// Handle successful export
notificationApi.success({ message: 'Export completed!' });
},
onError: ({ error }) => {
// Handle export error
notificationApi.error({
message: `Export failed: ${error.message}`,
});
},
}}
/>
```
#### Combined example
Here's an example combining all customization options:
```tsx title="packages/app/src/App.tsx"
<CatalogIndexPage
exportSettings={{
enabled: true,
columns: [
{ entityFilterKey: 'metadata.name', title: 'Name' },
{ entityFilterKey: 'spec.type', title: 'Type' },
{ entityFilterKey: 'spec.owner', title: 'Owner' },
{ entityFilterKey: 'metadata.namespace', title: 'Namespace' },
],
exporters: {
xml: { exporter: xmlExporter, label: 'XML' },
yaml: { exporter: yamlExporter, label: 'YAML' },
},
onSuccess: () => {
notificationApi.success({ message: 'Export completed!' });
},
onError: ({ error }) => {
notificationApi.error({
message: `Export failed: ${error.message}`,
});
},
}}
/>
```
## 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:
@@ -37,6 +37,103 @@ app:
limit: 20
```
### Configuring Catalog Export
The catalog export feature is available in the new frontend system and can be enabled via the `app-config.yaml`.
This will enable a button, which by default contains options to export data from the catalog table in CSV and JSON format.
When exporting, a dialog opens that lets the user choose the export format and select which columns to include.
#### Basic Configuration
To enable catalog export, add the following configuration:
```yaml title="app-config.yaml"
app:
extensions:
- page:catalog:
config:
exportSettings:
enabled: true
# Optional: hide the built-in CSV and JSON formats, showing only your own supplied exporters
disableBuiltinExporters: false
```
This will display an "Export selection" button on the catalog index page that allows users to export the currently filtered catalog entities in CSV or JSON format.
#### Advanced Configuration
For advanced export customization like custom export formats, create a frontend module that provides a catalog export extension:
```tsx title="src/catalogExportExtension.tsx"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { CatalogExportConfigBlueprint } from '@backstage/plugin-catalog/alpha';
import type { CatalogExporter } from '@backstage/plugin-catalog';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
// Define custom export formats using streaming async generators
const yamlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => {
const catalogApi = apis.get(catalogApiRef);
// Return an async generator that yields YAML chunks
async function* generateYaml() {
for await (const page of catalogApi.streamEntities(streamRequest)) {
for (const entity of page) {
// Serialize each entity to YAML and yield immediately
yield serializeEntityToYaml(entity, columns);
yield '---\n'; // YAML document separator
}
}
}
return {
generator: generateYaml(),
contentType: 'application/x-yaml',
};
};
// Create the extension using the blueprint
const catalogExportExtension = CatalogExportConfigBlueprint.make({
params: {
exporters: {
yaml: { fn: yamlExporter, label: 'YAML' },
},
columns: [{ entityFilterKey: 'metadata.name', title: 'Name' }],
onSuccess: () => {
console.log('Export successful!');
},
onError: ({ error }) => {
console.error('Export failed:', error);
},
},
});
// Create the module that provides this extension
export default createFrontendModule({
pluginId: 'catalog',
extensions: [catalogExportExtension],
});
```
Then register this module in your app features:
```tsx title="packages/app-next/src/App.tsx"
import catalogExportExtension from './catalogExportExtension';
const app = createApp({
features: [
// ... other features
catalogExportExtension,
],
});
```
The `CatalogExportConfigBlueprint` supports the following properties:
- **`exporters`** - Record of custom export format configurations (e.g., XML, YAML), each with a `fn` exporter function and optional `label`
- **`columns`** - Custom columns to include in the export. Each column specifies an entity field path (`entityFilterKey`) and an optional display `title`. If not provided, defaults to Name, Type, Owner, and Description
- **`onSuccess`** - Callback function invoked on successful export
- **`onError`** - Callback function invoked if export fails, receives `{ error: Error }`
### Catalog filters
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:
@@ -181,12 +278,12 @@ export const CustomCatalogPage = () => {
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>
<EntityListProvider pagination>
<PageWithHeader title={orgName} themeId="home">
<Content>
<ContentHeader title="">
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker />
@@ -202,9 +299,9 @@ export const CustomCatalogPage = () => {
<CatalogTable />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
</Content>
</PageWithHeader>
</EntityListProvider>
);
};
```
@@ -222,8 +319,6 @@ periodically.
For more details on extension overrides and the different override patterns
available, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
## Entity page
### Entity filters
Many extensions that attach within the catalog entity pages accept a `filter` configuration. The purpose of the `filter` configuration is to select what entities the extension should be applied to or be present on. Many of these extension will have a default filter defined, but you can override it by providing your own. When defining filters in code you can use either a predicate function or a entity predicate query, while in configuration you can only use an entity predicate query.