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/api-report.md b/plugins/catalog/api-report.md index 874d294cdc..e3df51deab 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -11,6 +11,7 @@ import { CatalogApi } from '@backstage/plugin-catalog-react'; import { ComponentEntity } from '@backstage/catalog-model'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; +import { EntityListContextProps } from '@backstage/plugin-catalog-react'; import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { EntityPresentationApi } from '@backstage/plugin-catalog-react'; import { EntityRefPresentation } from '@backstage/plugin-catalog-react'; @@ -181,12 +182,17 @@ export const CatalogTable: { }>; }; +// @public +export type CatalogTableColumnsFunc = ( + entityListContext: EntityListContextProps, +) => TableColumn[]; + // @public export interface CatalogTableProps { // (undocumented) actions?: TableProps['actions']; // (undocumented) - columns?: TableColumn[]; + columns?: TableColumn[] | CatalogTableColumnsFunc; // (undocumented) emptyContent?: ReactNode; // (undocumented) @@ -218,7 +224,7 @@ export interface DefaultCatalogPageProps { // (undocumented) actions?: TableProps['actions']; // (undocumented) - columns?: TableColumn[]; + columns?: TableColumn[] | CatalogTableColumnsFunc; // (undocumented) emptyContent?: ReactNode; // (undocumented) diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 0ced712344..f3cc0cb042 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -45,6 +45,8 @@ import { createComponentRouteRef } from '../../routes'; import { CatalogTableRow } from '../CatalogTable'; import { DefaultCatalogPage } from './DefaultCatalogPage'; +import { CatalogTableColumnsFunc } from '../CatalogTable/types'; + describe('DefaultCatalogPage', () => { const origReplaceState = window.history.replaceState; beforeEach(() => { @@ -217,6 +219,25 @@ describe('DefaultCatalogPage', () => { expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); }, 20_000); + it('should render the custom column function passed as prop', async () => { + const columns: CatalogTableColumnsFunc = ({ filters, entities }) => { + return filters.kind?.value === '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..a50cd4fa2a 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -44,6 +44,8 @@ import { CatalogTable, CatalogTableRow } from '../CatalogTable'; import { catalogTranslationRef } from '../../translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { CatalogTableColumnsFunc } from '../CatalogTable/types'; + /** @internal */ export interface BaseCatalogPageProps { filters: ReactNode; @@ -86,7 +88,7 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) { */ export interface DefaultCatalogPageProps { initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; + columns?: TableColumn[] | CatalogTableColumnsFunc; 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..052d8f4787 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -32,6 +32,7 @@ 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 { CatalogTableColumnsFunc } from './types'; const entities: Entity[] = [ { @@ -379,4 +380,63 @@ 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: CatalogTableColumnsFunc = ({ + filters, + entities: entities1, + }) => { + return filters.kind?.value === '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 04584f688a..4dd3fa257d 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -45,7 +45,7 @@ import { capitalize } from 'lodash'; import pluralize from 'pluralize'; import React, { ReactNode, useMemo } from 'react'; import { columnFactories } from './columns'; -import { CatalogTableRow } from './types'; +import { CatalogTableColumnsFunc, CatalogTableRow } from './types'; /** * Props for {@link CatalogTable}. @@ -53,7 +53,7 @@ import { CatalogTableRow } from './types'; * @public */ export interface CatalogTableProps { - columns?: TableColumn[]; + columns?: TableColumn[] | CatalogTableColumnsFunc; actions?: TableProps['actions']; tableOptions?: TableProps['options']; emptyContent?: ReactNode; @@ -76,51 +76,62 @@ const refCompare = (a: Entity, b: Entity) => { return toRef(a).localeCompare(toRef(b)); }; +const defaultColumnsFunc: CatalogTableColumnsFunc = ({ filters, entities }) => { + return [ + columnFactories.createTitleColumn({ hidden: true }), + columnFactories.createNameColumn({ defaultKind: filters.kind?.value }), + ...createEntitySpecificColumns(), + columnFactories.createMetadataDescriptionColumn(), + columnFactories.createTagsColumn(), + ]; + + function createEntitySpecificColumns(): TableColumn[] { + const baseColumns = [ + columnFactories.createSystemColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecLifecycleColumn(), + ]; + switch (filters.kind?.value) { + case 'user': + return []; + case 'domain': + case 'system': + return [columnFactories.createOwnerColumn()]; + case 'group': + case 'template': + return [columnFactories.createSpecTypeColumn()]; + case 'location': + return [ + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecTargetsColumn(), + ]; + default: + return entities.every(entity => entity.metadata.namespace === 'default') + ? baseColumns + : [...baseColumns, columnFactories.createNamespaceColumn()]; + } + } +}; + /** @public */ export const CatalogTable = (props: CatalogTableProps) => { - const { columns, actions, tableOptions, subtitle, emptyContent } = props; + const { + columns = defaultColumnsFunc, + actions, + tableOptions, + subtitle, + emptyContent, + } = props; const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); - const { loading, error, entities, filters } = useEntityList(); + const entityListContext = useEntityList(); + const { loading, error, entities, filters } = entityListContext; - const defaultColumns: TableColumn[] = useMemo(() => { - return [ - columnFactories.createTitleColumn({ hidden: true }), - columnFactories.createNameColumn({ defaultKind: filters.kind?.value }), - ...createEntitySpecificColumns(), - columnFactories.createMetadataDescriptionColumn(), - columnFactories.createTagsColumn(), - ]; - - function createEntitySpecificColumns(): TableColumn[] { - const baseColumns = [ - columnFactories.createSystemColumn(), - columnFactories.createOwnerColumn(), - columnFactories.createSpecTypeColumn(), - columnFactories.createSpecLifecycleColumn(), - ]; - switch (filters.kind?.value) { - case 'user': - return []; - case 'domain': - case 'system': - return [columnFactories.createOwnerColumn()]; - case 'group': - case 'template': - return [columnFactories.createSpecTypeColumn()]; - case 'location': - return [ - columnFactories.createSpecTypeColumn(), - columnFactories.createSpecTargetsColumn(), - ]; - default: - return entities.every( - entity => entity.metadata.namespace === 'default', - ) - ? baseColumns - : [...baseColumns, columnFactories.createNamespaceColumn()]; - } - } - }, [filters.kind?.value, entities]); + const tableColumns = useMemo( + () => + typeof columns === 'function' ? columns(entityListContext) : columns, + [columns, entityListContext], + ); const showTypeColumn = filters.type === undefined; // TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar @@ -228,7 +239,7 @@ export const CatalogTable = (props: CatalogTableProps) => { }; }); - const typeColumn = (columns || defaultColumns).find(c => c.title === 'Type'); + const typeColumn = tableColumns.find(c => c.title === 'Type'); if (typeColumn) { typeColumn.hidden = !showTypeColumn; } @@ -242,7 +253,7 @@ export const CatalogTable = (props: CatalogTableProps) => { return ( isLoading={loading} - columns={columns || defaultColumns} + columns={tableColumns} options={{ paging: showPagination, pageSize: 20, diff --git a/plugins/catalog/src/components/CatalogTable/index.ts b/plugins/catalog/src/components/CatalogTable/index.ts index 4e1b90f80a..f5d1ca1bb7 100644 --- a/plugins/catalog/src/components/CatalogTable/index.ts +++ b/plugins/catalog/src/components/CatalogTable/index.ts @@ -16,4 +16,4 @@ export { CatalogTable } from './CatalogTable'; export type { CatalogTableProps } from './CatalogTable'; -export type { CatalogTableRow } from './types'; +export type { CatalogTableRow, CatalogTableColumnsFunc } from './types'; diff --git a/plugins/catalog/src/components/CatalogTable/types.ts b/plugins/catalog/src/components/CatalogTable/types.ts index f2b3324169..c991205b11 100644 --- a/plugins/catalog/src/components/CatalogTable/types.ts +++ b/plugins/catalog/src/components/CatalogTable/types.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { EntityListContextProps } from '@backstage/plugin-catalog-react'; +import { TableColumn } from '@backstage/core-components'; /** @public */ export interface CatalogTableRow { @@ -31,3 +33,12 @@ export interface CatalogTableRow { ownedByRelations: CompoundEntityRef[]; }; } + +/** + * Typed columns function to dynamically render columns based on entity list context. + * + * @public + */ +export type CatalogTableColumnsFunc = ( + entityListContext: EntityListContextProps, +) => TableColumn[];