Merge pull request #21093 from stokedout/pstoker/catalog-index-columns-func

Accept columns prop as an array or a function
This commit is contained in:
Patrik Oldsberg
2023-11-20 18:01:36 +01:00
committed by GitHub
8 changed files with 166 additions and 50 deletions
+5
View File
@@ -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`.
+8 -2
View File
@@ -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<CatalogTableRow>[];
// @public
export interface CatalogTableProps {
// (undocumented)
actions?: TableProps<CatalogTableRow>['actions'];
// (undocumented)
columns?: TableColumn<CatalogTableRow>[];
columns?: TableColumn<CatalogTableRow>[] | CatalogTableColumnsFunc;
// (undocumented)
emptyContent?: ReactNode;
// (undocumented)
@@ -218,7 +224,7 @@ export interface DefaultCatalogPageProps {
// (undocumented)
actions?: TableProps<CatalogTableRow>['actions'];
// (undocumented)
columns?: TableColumn<CatalogTableRow>[];
columns?: TableColumn<CatalogTableRow>[] | CatalogTableColumnsFunc;
// (undocumented)
emptyContent?: ReactNode;
// (undocumented)
@@ -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(<DefaultCatalogPage columns={columns} />);
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(<DefaultCatalogPage />);
await waitFor(() => expect(catalogApi.queryEntities).toHaveBeenCalled());
@@ -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<CatalogTableRow>[];
columns?: TableColumn<CatalogTableRow>[] | CatalogTableColumnsFunc;
actions?: TableProps<CatalogTableRow>['actions'];
initialKind?: string;
tableOptions?: TableProps<CatalogTableRow>['options'];
@@ -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(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
entities: [entity],
filters: {
kind: {
value: 'api',
getCatalogFilters: () => ({ kind: 'api' }),
toQueryValue: () => 'api',
},
},
}}
>
<CatalogTable columns={columns} />
</MockEntityListContextProvider>
</ApiProvider>,
{
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();
});
});
@@ -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<CatalogTableRow>[];
columns?: TableColumn<CatalogTableRow>[] | CatalogTableColumnsFunc;
actions?: TableProps<CatalogTableRow>['actions'];
tableOptions?: TableProps<CatalogTableRow>['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<CatalogTableRow>[] {
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<CatalogTableRow>[] = useMemo(() => {
return [
columnFactories.createTitleColumn({ hidden: true }),
columnFactories.createNameColumn({ defaultKind: filters.kind?.value }),
...createEntitySpecificColumns(),
columnFactories.createMetadataDescriptionColumn(),
columnFactories.createTagsColumn(),
];
function createEntitySpecificColumns(): TableColumn<CatalogTableRow>[] {
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 (
<Table<CatalogTableRow>
isLoading={loading}
columns={columns || defaultColumns}
columns={tableColumns}
options={{
paging: showPagination,
pageSize: 20,
@@ -16,4 +16,4 @@
export { CatalogTable } from './CatalogTable';
export type { CatalogTableProps } from './CatalogTable';
export type { CatalogTableRow } from './types';
export type { CatalogTableRow, CatalogTableColumnsFunc } from './types';
@@ -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<CatalogTableRow>[];