mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user