From b8e1eb2c0fa588fd93f50b7e833f47bbd09e0022 Mon Sep 17 00:00:00 2001 From: Paul Stoker Date: Wed, 8 Nov 2023 13:57:14 +0100 Subject: [PATCH] Accept columns prop as an array or a function Signed-off-by: Paul Stoker --- .changeset/tricky-islands-wonder.md | 5 ++ .../CatalogPage/DefaultCatalogPage.test.tsx | 24 ++++++++ .../CatalogPage/DefaultCatalogPage.tsx | 3 +- .../CatalogTable/CatalogTable.test.tsx | 58 ++++++++++++++++++- .../components/CatalogTable/CatalogTable.tsx | 26 +++++++-- 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 .changeset/tricky-islands-wonder.md 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,