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>
This commit is contained in:
co-authored by
Galen Kistler
Galen
parent
1bb38e8f95
commit
9264e2a3bd
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HeaderGroup, Column } from 'react-table';
|
import { HeaderGroup, Column } from 'react-table';
|
||||||
|
|
||||||
|
import { Field } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
import { getFieldTypeIcon } from '../../types';
|
import { getFieldTypeIcon } from '../../types';
|
||||||
@@ -8,6 +9,7 @@ import { Icon } from '../Icon/Icon';
|
|||||||
|
|
||||||
import { Filter } from './Filter';
|
import { Filter } from './Filter';
|
||||||
import { TableStyles } from './styles';
|
import { TableStyles } from './styles';
|
||||||
|
import { TableFieldOptions } from './types';
|
||||||
|
|
||||||
export interface HeaderRowProps {
|
export interface HeaderRowProps {
|
||||||
headerGroups: HeaderGroup[];
|
headerGroups: HeaderGroup[];
|
||||||
@@ -43,7 +45,8 @@ export const HeaderRow = (props: HeaderRowProps) => {
|
|||||||
|
|
||||||
function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: boolean) {
|
function renderHeaderCell(column: any, tableStyles: TableStyles, showTypeIcons?: boolean) {
|
||||||
const headerProps = column.getHeaderProps();
|
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) {
|
if (column.canResize) {
|
||||||
headerProps.style.userSelect = column.isResizing ? 'none' : 'auto'; // disables selecting text while resizing
|
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.position = 'absolute';
|
||||||
headerProps.style.justifyContent = column.justifyContent;
|
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 (
|
return (
|
||||||
<div className={tableStyles.headerCell} {...headerProps} role="columnheader">
|
<div className={tableStyles.headerCell} {...headerProps} role="columnheader">
|
||||||
{column.canSort && (
|
{column.canSort && sortHeaderContent}
|
||||||
<>
|
{!column.canSort && headerContent}
|
||||||
<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 && column.canFilter && <Filter column={column} tableStyles={tableStyles} field={field} />}
|
{!column.canSort && column.canFilter && <Filter column={column} tableStyles={tableStyles} field={field} />}
|
||||||
{column.canResize && <div {...column.getResizerProps()} className={tableStyles.resizeHandle} />}
|
{column.canResize && <div {...column.getResizerProps()} className={tableStyles.resizeHandle} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import React from 'react';
|
|||||||
|
|
||||||
import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } from '@grafana/data';
|
import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
|
||||||
import { Table } from './Table';
|
import { Table } from './Table';
|
||||||
import { Props } from './types';
|
import { CustomHeaderRendererProps, Props } from './types';
|
||||||
|
|
||||||
// mock transition styles to ensure consistent behaviour in unit tests
|
// mock transition styles to ensure consistent behaviour in unit tests
|
||||||
jest.mock('@floating-ui/react', () => ({
|
jest.mock('@floating-ui/react', () => ({
|
||||||
@@ -35,6 +37,12 @@ const dataFrameData = {
|
|||||||
config: {
|
config: {
|
||||||
custom: {
|
custom: {
|
||||||
filterable: false,
|
filterable: false,
|
||||||
|
headerComponent: (props: CustomHeaderRendererProps) => (
|
||||||
|
<span>
|
||||||
|
{props.defaultContent}
|
||||||
|
<Icon aria-label={'header-icon'} name={'ellipsis-v'} />
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
links: [
|
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', () => {
|
describe('on filtering', () => {
|
||||||
it('the rows should be filtered', async () => {
|
it('the rows should be filtered', async () => {
|
||||||
getTestContext({
|
getTestContext({
|
||||||
|
|||||||
@@ -124,9 +124,19 @@ export interface TableCustomCellOptions {
|
|||||||
type: schema.TableCellDisplayMode.Custom;
|
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
|
// 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.
|
// 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 TableCellOptions = schema.TableCellOptions | TableCustomCellOptions;
|
||||||
export type TableFieldOptions = Omit<schema.TableFieldOptions, 'cellOptions'> & {
|
export type TableFieldOptions = Omit<schema.TableFieldOptions, 'cellOptions'> & {
|
||||||
cellOptions: TableCellOptions;
|
cellOptions: TableCellOptions;
|
||||||
|
headerComponent?: React.ComponentType<CustomHeaderRendererProps>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user