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:
@@ -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`.
|
||||
@@ -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>[];
|
||||
|
||||
Reference in New Issue
Block a user