diff --git a/.changeset/tricky-islands-wonder.md b/.changeset/tricky-islands-wonder.md
new file mode 100644
index 0000000000..5b9c37281b
--- /dev/null
+++ b/.changeset/tricky-islands-wonder.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog': minor
+---
+
+The `columns` prop can be an array or a function that returns an array in order to override the default columns of the `CatalogIndexPage`.
diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx
index 0ced712344..a4b1148a6f 100644
--- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx
+++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx
@@ -44,6 +44,8 @@ import React from 'react';
import { createComponentRouteRef } from '../../routes';
import { CatalogTableRow } from '../CatalogTable';
import { DefaultCatalogPage } from './DefaultCatalogPage';
+import { ColumnsFunc } from '../CatalogTable/CatalogTable';
+import { Entity } from '@backstage/catalog-model/';
describe('DefaultCatalogPage', () => {
const origReplaceState = window.history.replaceState;
@@ -217,6 +219,28 @@ describe('DefaultCatalogPage', () => {
expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
}, 20_000);
+ it('should render the custom column function passed as prop', async () => {
+ const columns: ColumnsFunc = (
+ kind: string | undefined,
+ entities: Entity[],
+ ) => {
+ return kind === 'component' && entities.length
+ ? [
+ { title: 'Foo', field: 'entity.foo' },
+ { title: 'Bar', field: 'entity.bar' },
+ { title: 'Baz', field: 'entity.spec.lifecycle' },
+ ]
+ : [];
+ };
+ await renderWrapped();
+
+ const columnHeader = screen
+ .getAllByRole('button')
+ .filter(c => c.tagName === 'SPAN');
+ const columnHeaderLabels = columnHeader.map(c => c.textContent);
+ expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']);
+ }, 20_000);
+
it('should render the default actions of an item in the grid', async () => {
await renderWrapped();
await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled());
diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx
index e50c69b8ec..bcabbed991 100644
--- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx
@@ -43,6 +43,7 @@ import { createComponentRouteRef } from '../../routes';
import { CatalogTable, CatalogTableRow } from '../CatalogTable';
import { catalogTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
+import { ColumnsFunc } from '../CatalogTable/CatalogTable';
/** @internal */
export interface BaseCatalogPageProps {
@@ -86,7 +87,7 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) {
*/
export interface DefaultCatalogPageProps {
initiallySelectedFilter?: UserListFilterKind;
- columns?: TableColumn[];
+ columns?: TableColumn[] | ColumnsFunc;
actions?: TableProps['actions'];
initialKind?: string;
tableOptions?: TableProps['options'];
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
index c242372a97..35f7a94355 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
@@ -31,7 +31,7 @@ import {
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { act, fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
-import { CatalogTable } from './CatalogTable';
+import { CatalogTable, ColumnsFunc } from './CatalogTable';
const entities: Entity[] = [
{
@@ -379,4 +379,60 @@ describe('CatalogTable component', () => {
const labelCellValue = screen.getByText('generic');
expect(labelCellValue).toBeInTheDocument();
});
+
+ it('should render the label column with customised title and value as specified using function', async () => {
+ const columns: ColumnsFunc = (kind, entities1) => {
+ return kind === 'api' && entities1.length
+ ? [
+ CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
+ CatalogTable.columns.createLabelColumn('category', {
+ title: 'Category',
+ }),
+ ]
+ : [];
+ };
+
+ const entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'API',
+ metadata: {
+ name: 'APIWithLabel',
+ labels: { category: 'generic' },
+ },
+ };
+ const expectedColumns = ['Name', 'Category', 'Actions'];
+
+ await renderInTestApp(
+
+ ({ kind: 'api' }),
+ toQueryValue: () => 'api',
+ },
+ },
+ }}
+ >
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ const columnHeader = screen
+ .getAllByRole('button')
+ .filter(c => c.tagName === 'SPAN');
+ const columnHeaderLabels = columnHeader.map(c => c.textContent);
+ expect(columnHeaderLabels).toEqual(expectedColumns);
+
+ const labelCellValue = screen.getByText('generic');
+ expect(labelCellValue).toBeInTheDocument();
+ });
});
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
index 24ccb22908..25a3535d22 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -40,19 +40,29 @@ import Edit from '@material-ui/icons/Edit';
import OpenInNew from '@material-ui/icons/OpenInNew';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
-import { capitalize } from 'lodash';
+import { capitalize, isFunction } from 'lodash';
import React, { ReactNode, useMemo } from 'react';
import { columnFactories } from './columns';
import { CatalogTableRow } from './types';
import pluralize from 'pluralize';
+/**
+ * Typed columns function to dynamically render columns based on entities and chosen kind.
+ *
+ * @public
+ */
+export type ColumnsFunc = (
+ kind: string | undefined,
+ entities: Entity[],
+) => TableColumn[];
+
/**
* Props for {@link CatalogTable}.
*
* @public
*/
export interface CatalogTableProps {
- columns?: TableColumn[];
+ columns?: TableColumn[] | ColumnsFunc;
actions?: TableProps['actions'];
tableOptions?: TableProps['options'];
emptyContent?: ReactNode;
@@ -121,6 +131,12 @@ export const CatalogTable = (props: CatalogTableProps) => {
}
}, [filters.kind?.value, entities]);
+ const overrideColumns = useMemo(() => {
+ return isFunction(columns)
+ ? columns(filters.kind?.value, entities)
+ : columns;
+ }, [columns, filters.kind?.value, entities]);
+
const showTypeColumn = filters.type === undefined;
// TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar
const titlePreamble = capitalize(filters.user?.value ?? 'all');
@@ -227,7 +243,9 @@ export const CatalogTable = (props: CatalogTableProps) => {
};
});
- const typeColumn = (columns || defaultColumns).find(c => c.title === 'Type');
+ const typeColumn = (overrideColumns || defaultColumns).find(
+ c => c.title === 'Type',
+ );
if (typeColumn) {
typeColumn.hidden = !showTypeColumn;
}
@@ -241,7 +259,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
return (
isLoading={loading}
- columns={columns || defaultColumns}
+ columns={overrideColumns || defaultColumns}
options={{
paging: showPagination,
pageSize: 20,