Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions docs/dynamic-plugins/frontend-plugin-wiring.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,3 +746,189 @@ The required options mirror the [AppTheme](https://backstage.io/docs/reference/c
- `variant` Whether the theme is `light` or `dark`, can only be one of these values.
- `icon` a string reference to a system or [app icon](#extend-internal-library-of-available-icons)
- `importName` name of the exported theme provider function, the function signature should match `({ children }: { children: ReactNode }): React.JSX.Element`

## Customizing Catalog Table Columns

The catalog table displayed on the `CatalogIndexPage` can be customized to show, hide, or add columns. This allows platform engineers to remove unnecessary columns and surface custom entity metadata directly in the catalog list view.

### Configuration

Column customization is configured in the `app-config.yaml` under the `catalog.table.columns` section:

```yaml
# app-config.yaml
catalog:
table:
columns:
# Option 1: Include only specific columns (replaces defaults)
include:
- name
- owner
- type
- lifecycle

# Option 2: Exclude specific columns from defaults
exclude:
- createdAt

# Add custom columns based on entity metadata
custom:
- title: "Security Tier"
field: "metadata.annotations['custom/security-tier']"
width: 150
sortable: true
defaultValue: "N/A"
```

### Available Built-in Column IDs

The following column IDs can be used in `include` or `exclude` lists:

| Column ID | Description | Maps to |
| ------------- | ---------------------------------------- | ---------------------------------------------------- |
| `name` | Entity name with link | `CatalogTable.columns.createNameColumn()` |
| `owner` | Entity owner | `CatalogTable.columns.createOwnerColumn()` |
| `type` | Entity spec.type | `CatalogTable.columns.createSpecTypeColumn()` |
| `lifecycle` | Entity spec.lifecycle | `CatalogTable.columns.createSpecLifecycleColumn()` |
| `description` | Entity metadata.description | `CatalogTable.columns.createMetadataDescriptionColumn()` |
| `tags` | Entity metadata.tags | `CatalogTable.columns.createTagsColumn()` |
| `namespace` | Entity metadata.namespace | `CatalogTable.columns.createNamespaceColumn()` |
| `system` | Entity spec.system | `CatalogTable.columns.createSystemColumn()` |
| `createdAt` | Created timestamp from annotations | Custom RHDH column |

### Configuration Options

#### `include` (array of strings)

When specified, only the listed columns will be displayed. This completely replaces the default column set.

```yaml
catalog:
table:
columns:
include:
- name
- owner
- type
```

#### `exclude` (array of strings)

When specified, the listed columns will be removed from the default set. Cannot be used together with `include` (if both are specified, `include` takes precedence).

```yaml
catalog:
table:
columns:
exclude:
- createdAt
- description
```

#### `custom` (array of custom column definitions)

Define additional columns based on entity metadata fields. Custom columns are appended after the built-in columns.

Each custom column supports the following properties:

| Property | Required | Type | Description |
| -------------- | -------- | ----------------- | --------------------------------------------------------------------------- |
| `title` | Yes | string | The column header text |
| `field` | Yes | string | The entity field path (e.g., `spec.team`, `metadata.annotations['key']`) |
| `width` | No | number | Column width in pixels |
| `sortable` | No | boolean | Whether the column is sortable (default: `true`) |
| `defaultValue` | No | string | Value to display when the field is empty or undefined |
| `kind` | No | string or array | Entity kind(s) to apply this column to (e.g., `API`, `['Component', 'API']`) |

### Field Path Syntax

The `field` property supports accessing nested entity properties using dot notation:

- Simple paths: `spec.type`, `metadata.name`
- Annotation paths: `metadata.annotations['backstage.io/techdocs-ref']`
- Custom annotations: `metadata.annotations['company.com/cost-center']`

### Examples

#### Hide the "Created At" column

```yaml
catalog:
table:
columns:
exclude:
- createdAt
```

#### Minimal view with only essential columns

```yaml
catalog:
table:
columns:
include:
- name
- owner
- type
```

#### Add a custom metadata column

```yaml
catalog:
table:
columns:
custom:
- title: "Cost Center"
field: "metadata.annotations['company.com/cost-center']"
sortable: true
defaultValue: "Unknown"
```

#### Kind-specific columns

Show a column only for specific entity kinds:

```yaml
catalog:
table:
columns:
custom:
- title: "API Version"
field: "spec.definition.version"
kind: API
- title: "Team"
field: "spec.team"
kind:
- Component
- Resource
```

#### Complete customization example

```yaml
catalog:
table:
columns:
exclude:
- createdAt
- description
custom:
- title: "Security Tier"
field: "metadata.annotations['custom/security-tier']"
width: 120
sortable: true
defaultValue: "Not Set"
- title: "Cost Center"
field: "metadata.labels['cost-center']"
sortable: true
```

### Default Behavior

When no `catalog.table.columns` configuration is provided:

1. All default columns from Backstage's `CatalogTable.defaultColumnsFunc()` are displayed
2. The "Created At" column is included (for backward compatibility with RHDH)

This ensures existing RHDH deployments continue to work without any configuration changes.
188 changes: 188 additions & 0 deletions e2e-tests/playwright/e2e/catalog-column-customization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { Page, expect, test } from "@playwright/test";
import { UIhelper } from "../utils/ui-helper";
import { Common, setupBrowser } from "../utils/common";
import {
getTranslations,
getCurrentLanguage,
} from "../e2e/localization/locale";

const t = getTranslations();
const lang = getCurrentLanguage();

let page: Page;

/**
* E2E tests for catalog table column customization feature.
*
* These tests verify that platform engineers can configure catalog table columns
* via app-config.yaml to:
* - Hide default columns (like "Created At")
* - Add custom columns based on entity metadata
* - Sort columns correctly
*
* Note: These tests require specific app-config.yaml configuration to be applied
* before running. The configuration should include catalog.table.columns settings.
*
* Example configuration for testing:
* ```yaml
* catalog:
* table:
* columns:
* exclude:
* - createdAt
* custom:
* - title: "Security Tier"
* field: "metadata.annotations['custom/security-tier']"
* sortable: true
* ```
*/
test.describe("Catalog Column Customization", () => {
let uiHelper: UIhelper;
let common: Common;

test.beforeAll(async ({ browser }, testInfo) => {
test.info().annotations.push({
type: "component",
description: "catalog-columns",
});

page = (await setupBrowser(browser, testInfo)).page;

common = new Common(page);
uiHelper = new UIhelper(page);

await common.loginAsGuest();
});

test.beforeEach(async () => {
await uiHelper.openSidebar(t["rhdh"][lang]["menuItem.catalog"]);
await uiHelper.verifyHeading(
t["catalog"][lang]["indexPage.title"].replace("{{orgName}}", "My Org"),
);
await uiHelper.openCatalogSidebar("Component");
});

test("Default columns are visible in catalog table", async () => {
// Verify that default columns are present
await uiHelper.verifyColumnHeading(["Name"], true);
await uiHelper.verifyColumnHeading(["Owner"], true);
await uiHelper.verifyColumnHeading(["Type"], true);
});

test("Created At column is visible by default", async () => {
// By default (without exclude configuration), Created At should be visible
await uiHelper.verifyColumnHeading(["Created At"], true);
});

test("Column headers are clickable for sorting", async () => {
// Test that the Name column can be clicked for sorting
const nameColumn = page.getByRole("columnheader", {
name: "Name",
exact: true,
});

await expect(nameColumn).toBeVisible();
await nameColumn.click();

// After clicking, the column should show a sort indicator
// The sort functionality is handled by material-table internally
await expect(nameColumn).toBeVisible();
});

test("Catalog table displays entity data correctly", async () => {
// Search for a known entity to verify data is displayed
await uiHelper.searchInputPlaceholder("backstage");

// Verify that search results are displayed
const tableRows = page
.getByRole("row")
.filter({ has: page.getByRole("cell") });
await expect(tableRows.first()).toBeVisible();
});

/**
* Test for custom column visibility.
* This test should be run with app-config that includes custom columns.
*
* Required configuration:
* ```yaml
* catalog:
* table:
* columns:
* custom:
* - title: "Security Tier"
* field: "metadata.annotations['custom/security-tier']"
* ```
*/
test.skip("Custom columns from configuration are visible", async () => {

Check warning on line 117 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected use of the `.skip()` annotation
// This test is skipped by default as it requires specific configuration
// When running with custom column configuration, enable this test

// Verify custom column header is present
await uiHelper.verifyColumnHeading(["Security Tier"], true);
});

/**
* Test for excluded columns.
* This test should be run with app-config that excludes the Created At column.
*
* Required configuration:
* ```yaml
* catalog:
* table:
* columns:
* exclude:
* - createdAt
* ```
*/
test.skip("Excluded columns are not visible", async () => {

Check warning on line 138 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected use of the `.skip()` annotation
// This test is skipped by default as it requires specific configuration
// When running with exclude configuration, enable this test

// Verify Created At column is NOT present when excluded
const createdAtColumn = page.getByRole("columnheader", {
name: "Created At",
exact: true,
});
await expect(createdAtColumn).not.toBeVisible();

Check warning on line 147 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected usage of not.toBeVisible(). Use toBeHidden() instead
});

/**
* Test for include-only mode.
* This test should be run with app-config that specifies only certain columns.
*
* Required configuration:
* ```yaml
* catalog:
* table:
* columns:
* include:
* - name
* - owner
* ```
*/
test.skip("Only included columns are visible in include mode", async () => {

Check warning on line 164 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected use of the `.skip()` annotation
// This test is skipped by default as it requires specific configuration

// Verify only the included columns are present
await uiHelper.verifyColumnHeading(["Name"], true);
await uiHelper.verifyColumnHeading(["Owner"], true);

// Verify other columns are NOT present
const typeColumn = page.getByRole("columnheader", {
name: "Type",
exact: true,
});
await expect(typeColumn).not.toBeVisible();

Check warning on line 176 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected usage of not.toBeVisible(). Use toBeHidden() instead

const createdAtColumn = page.getByRole("columnheader", {
name: "Created At",
exact: true,
});
await expect(createdAtColumn).not.toBeVisible();

Check warning on line 182 in e2e-tests/playwright/e2e/catalog-column-customization.spec.ts

View workflow job for this annotation

GitHub Actions / TSC, ESLint and Prettier

Unexpected usage of not.toBeVisible(). Use toBeHidden() instead
});

test.afterAll(async () => {
await page.close();
});
});
Loading
Loading