diff --git a/packages/grafana-ui/src/components/Table/HeaderRow.tsx b/packages/grafana-ui/src/components/Table/HeaderRow.tsx index 62f61e494d1..2bd1ef04801 100644 --- a/packages/grafana-ui/src/components/Table/HeaderRow.tsx +++ b/packages/grafana-ui/src/components/Table/HeaderRow.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { HeaderGroup, Column } from 'react-table'; +import { Field } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getFieldTypeIcon } from '../../types'; @@ -8,6 +9,7 @@ import { Icon } from '../Icon/Icon'; import { Filter } from './Filter'; import { TableStyles } from './styles'; +import { TableFieldOptions } from './types'; export interface HeaderRowProps { headerGroups: HeaderGroup[]; @@ -43,7 +45,8 @@ export const HeaderRow = (props: HeaderRowProps) => { function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: boolean) { const headerProps = column.getHeaderProps(); - const field = column.field ?? null; + const field: Field = column.field ?? null; + const tableFieldOptions: TableFieldOptions | undefined = field?.config.custom; if (column.canResize) { headerProps.style.userSelect = column.isResizing ? 'none' : 'auto'; // disables selecting text while resizing @@ -51,27 +54,37 @@ function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: headerProps.style.position = 'absolute'; headerProps.style.justifyContent = column.justifyContent; + headerProps.style.left = column.totalLeft; + + let headerContent = column.render('Header'); + + let sortHeaderContent = column.canSort && ( + <> + + {column.canFilter && } + + ); + if (sortHeaderContent && tableFieldOptions?.headerComponent) { + sortHeaderContent = ; + } else if (tableFieldOptions?.headerComponent) { + headerContent = ; + } return (
- {column.canSort && ( - <> - - {column.canFilter && } - - )} - {!column.canSort && column.render('Header')} + {column.canSort && sortHeaderContent} + {!column.canSort && headerContent} {!column.canSort && column.canFilter && } {column.canResize &&
}
diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index 1f8c73c1420..3d3fc8f5e16 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -4,8 +4,10 @@ import React from 'react'; import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } from '@grafana/data'; +import { Icon } from '../Icon/Icon'; + import { Table } from './Table'; -import { Props } from './types'; +import { CustomHeaderRendererProps, Props } from './types'; // mock transition styles to ensure consistent behaviour in unit tests jest.mock('@floating-ui/react', () => ({ @@ -35,6 +37,12 @@ const dataFrameData = { config: { custom: { filterable: false, + headerComponent: (props: CustomHeaderRendererProps) => ( + + {props.defaultContent} + + + ), }, links: [ { @@ -238,6 +246,19 @@ describe('Table', () => { }); }); + describe('custom header', () => { + it('Should be rendered', async () => { + getTestContext(); + + await userEvent.click(within(getColumnHeader(/temperature/)).getByText(/temperature/i)); + await userEvent.click(within(getColumnHeader(/temperature/)).getByText(/temperature/i)); + + const rows = within(getTable()).getAllByRole('row'); + expect(rows).toHaveLength(5); + expect(within(rows[0]).getByLabelText('header-icon')).toBeInTheDocument(); + }); + }); + describe('on filtering', () => { it('the rows should be filtered', async () => { getTestContext({ diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index b3d1f9f3be7..563a0d9ee48 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -124,9 +124,19 @@ export interface TableCustomCellOptions { type: schema.TableCellDisplayMode.Custom; } +/** + * @alpha + * Props that will be passed to the TableCustomCellOptions.cellComponent when rendered. + */ +export interface CustomHeaderRendererProps { + field: Field; + defaultContent: React.ReactNode; +} + // As cue/schema cannot define function types (as main point of schema is to be serializable) we have to extend the // types here with the dynamic API. This means right now this is not usable as a table panel option for example. export type TableCellOptions = schema.TableCellOptions | TableCustomCellOptions; export type TableFieldOptions = Omit & { cellOptions: TableCellOptions; + headerComponent?: React.ComponentType; };