Table: Custom headerComponent field config option (#83254)

* Table: Custom headerComponent field config option

* Table custom header component (#83830)

* Add tests, fix bug

---------

Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>
Co-authored-by: Galen <galen.kistler@grafana.com>
pull/83884/head
Torkel Ödegaard 1 year ago committed by GitHub
parent 1bb38e8f95
commit 9264e2a3bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 51
      packages/grafana-ui/src/components/Table/HeaderRow.tsx
  2. 23
      packages/grafana-ui/src/components/Table/Table.test.tsx
  3. 10
      packages/grafana-ui/src/components/Table/types.ts

@ -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 && (
<>
<button {...column.getSortByToggleProps()} className={tableStyles.headerCellLabel}>
{showTypeIcons && (
<Icon name={getFieldTypeIcon(field)} title={field?.type} size="sm" className={tableStyles.typeIcon} />
)}
<div>{headerContent}</div>
{column.isSorted &&
(column.isSortedDesc ? (
<Icon size="lg" name="arrow-down" className={tableStyles.sortIcon} />
) : (
<Icon name="arrow-up" size="lg" className={tableStyles.sortIcon} />
))}
</button>
{column.canFilter && <Filter column={column} tableStyles={tableStyles} field={field} />}
</>
);
if (sortHeaderContent && tableFieldOptions?.headerComponent) {
sortHeaderContent = <tableFieldOptions.headerComponent field={field} defaultContent={sortHeaderContent} />;
} else if (tableFieldOptions?.headerComponent) {
headerContent = <tableFieldOptions.headerComponent field={field} defaultContent={headerContent} />;
}
return (
<div className={tableStyles.headerCell} {...headerProps} role="columnheader">
{column.canSort && (
<>
<button {...column.getSortByToggleProps()} className={tableStyles.headerCellLabel}>
{showTypeIcons && (
<Icon name={getFieldTypeIcon(field)} title={field?.type} size="sm" className={tableStyles.typeIcon} />
)}
<div>{column.render('Header')}</div>
{column.isSorted &&
(column.isSortedDesc ? (
<Icon size="lg" name="arrow-down" className={tableStyles.sortIcon} />
) : (
<Icon name="arrow-up" size="lg" className={tableStyles.sortIcon} />
))}
</button>
{column.canFilter && <Filter column={column} tableStyles={tableStyles} field={field} />}
</>
)}
{!column.canSort && column.render('Header')}
{column.canSort && sortHeaderContent}
{!column.canSort && headerContent}
{!column.canSort && column.canFilter && <Filter column={column} tableStyles={tableStyles} field={field} />}
{column.canResize && <div {...column.getResizerProps()} className={tableStyles.resizeHandle} />}
</div>

@ -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) => (
<span>
{props.defaultContent}
<Icon aria-label={'header-icon'} name={'ellipsis-v'} />
</span>
),
},
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({

@ -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<schema.TableFieldOptions, 'cellOptions'> & {
cellOptions: TableCellOptions;
headerComponent?: React.ComponentType<CustomHeaderRendererProps>;
};

Loading…
Cancel
Save