feat: catalog export feature
Signed-off-by: the-serious-programmer <19777147+the-serious-programmer@users.noreply.github.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user