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;
};