TableNG: Move header cell component (#99844)

* fix(table-ng): move header cell into separate file

* Fix sub table

---------

Co-authored-by: drew08t <drew08@gmail.com>
This commit is contained in:
Ihor Yeromin 2025-01-31 20:32:19 +02:00 committed by GitHub
parent f7e849bb0b
commit 4fcdb90ffc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 182 additions and 133 deletions

View File

@ -651,6 +651,13 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Table/TableCellInspector.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"]
],
"packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@ -669,14 +676,10 @@ exports[`better eslint`] = {
],
"packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"packages/grafana-ui/src/components/Table/TableNG/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

View File

@ -0,0 +1,164 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import React, { useLayoutEffect, useRef, useEffect } from 'react';
import { Column, SortDirection } from 'react-data-grid';
import { Field, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes';
import { Icon } from '../../../Icon/Icon';
import { TableColumnResizeActionCallback, TableRow } from '../../types';
import { FilterType } from '../TableNG';
import { Filter } from './../Filter/Filter';
interface HeaderCellProps {
column: Column<any>;
// rows: Record<string, string>[];
rows: TableRow[];
field: Field;
onSort: (columnKey: string, direction: SortDirection, isMultiSort: boolean) => void;
direction: SortDirection | undefined;
justifyContent?: Property.JustifyContent;
filter: any;
setFilter: (value: any) => void;
filterable: boolean;
onColumnResize?: TableColumnResizeActionCallback;
headerCellRefs: React.MutableRefObject<Record<string, HTMLDivElement | null>>;
crossFilterOrder: React.MutableRefObject<string[]>;
crossFilterRows: React.MutableRefObject<{ [key: string]: TableRow[] }>;
}
const HeaderCell: React.FC<HeaderCellProps> = ({
column,
rows,
field,
onSort,
direction,
justifyContent,
filter,
setFilter,
filterable,
onColumnResize,
headerCellRefs,
crossFilterOrder,
crossFilterRows,
}) => {
const styles = useStyles2(getStyles);
const headerRef = useRef<HTMLDivElement>(null);
let isColumnFilterable = filterable;
if (field.config.custom?.filterable !== filterable) {
isColumnFilterable = field.config.custom?.filterable || false;
}
// we have to remove/reset the filter if the column is not filterable
if (!isColumnFilterable && filter[field.name]) {
setFilter((filter: FilterType) => {
const newFilter = { ...filter };
delete newFilter[field.name];
return newFilter;
});
}
const handleSort = (event: React.MouseEvent<HTMLButtonElement>) => {
const isMultiSort = event.shiftKey;
onSort(column.key as string, direction === 'ASC' ? 'DESC' : 'ASC', isMultiSort);
};
// collecting header cell refs to handle manual column resize
useLayoutEffect(() => {
if (headerRef.current) {
headerCellRefs.current[column.key] = headerRef.current;
}
}, [headerRef, column.key]); // eslint-disable-line react-hooks/exhaustive-deps
// TODO: this is a workaround to handle manual column resize;
useEffect(() => {
const headerCellParent = headerRef.current?.parentElement;
if (headerCellParent) {
// `lastElement` is an HTML element added by react-data-grid for resizing columns.
// We add a click event listener to `lastElement` to handle the end of the resize operation.
const lastElement = headerCellParent.lastElementChild;
if (lastElement) {
const handleMouseUp = () => {
let newWidth = headerCellParent.clientWidth;
const columnMinWidth = column.minWidth;
if (columnMinWidth && newWidth < columnMinWidth) {
newWidth = columnMinWidth;
}
onColumnResize?.(column.key as string, newWidth);
};
lastElement.addEventListener('click', handleMouseUp);
return () => {
lastElement.removeEventListener('click', handleMouseUp);
};
}
}
// to handle "Not all code paths return a value." error
return;
}, [column]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div
ref={headerRef}
style={{ display: 'flex', justifyContent }}
// TODO find a better solution to this issue, see: https://github.com/adazzle/react-data-grid/issues/3535
// Unblock spacebar event
onKeyDown={(event) => {
if (event.key === ' ') {
event.stopPropagation();
}
}}
>
<button className={styles.headerCellLabel} onClick={handleSort}>
<div>{column.name}</div>
{direction &&
(direction === 'ASC' ? (
<Icon name="arrow-up" size="lg" className={styles.sortIcon} />
) : (
<Icon name="arrow-down" size="lg" className={styles.sortIcon} />
))}
</button>
{isColumnFilterable && (
<Filter
name={column.key}
rows={rows}
filter={filter}
setFilter={setFilter}
field={field}
crossFilterOrder={crossFilterOrder.current}
crossFilterRows={crossFilterRows.current}
/>
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
headerCellLabel: css({
border: 'none',
padding: 0,
background: 'inherit',
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: theme.typography.fontWeightMedium,
display: 'flex',
alignItems: 'center',
marginRight: theme.spacing(0.5),
'&:hover': {
textDecoration: 'underline',
color: theme.colors.text.link,
},
}),
sortIcon: css({
marginLeft: theme.spacing(0.5),
}),
});
export { HeaderCell };

View File

@ -17,15 +17,14 @@ import { TableCellHeight } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../themes';
import { ContextMenu } from '../../ContextMenu/ContextMenu';
import { Icon } from '../../Icon/Icon';
import { MenuItem } from '../../Menu/MenuItem';
import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector';
import { TableNGProps } from '../types';
import { getTextAlign } from '../utils';
import { HeaderCell } from './Cells/HeaderCell';
import { RowExpander } from './Cells/RowExpander';
import { TableCellNG } from './Cells/TableCellNG';
import { Filter } from './Filter/Filter';
import { getRowHeight, shouldTextOverflow, getFooterItemNG } from './utils';
const DEFAULT_CELL_PADDING = 6;
@ -40,15 +39,6 @@ interface TableColumn extends Column<TableRow> {
field: Field;
}
interface HeaderCellProps {
column: Column<any>;
field: Field;
onSort: (columnKey: string, direction: SortDirection, isMultiSort: boolean) => void;
direction: SortDirection | undefined;
justifyContent?: Property.JustifyContent;
filter: any;
}
export type FilterType = {
[key: string]: {
filteredSet: Set<string>;
@ -164,100 +154,6 @@ export function TableNG(props: TableNGProps) {
const defaultRowHeight = getDefaultRowHeight();
const defaultLineHeight = theme.typography.body.lineHeight * theme.typography.fontSize;
// TODO: move this component to a separate file
const HeaderCell: React.FC<HeaderCellProps> = ({ column, field, onSort, direction, justifyContent, filter }) => {
const headerRef = useRef<HTMLDivElement>(null);
let isColumnFilterable = filterable;
if (field.config.custom?.filterable !== filterable) {
isColumnFilterable = field.config.custom?.filterable || false;
}
// we have to remove/reset the filter if the column is not filterable
if (!isColumnFilterable && filter[field.name]) {
setFilter((filter: FilterType) => {
const newFilter = { ...filter };
delete newFilter[field.name];
return newFilter;
});
}
const handleSort = (event: React.MouseEvent<HTMLButtonElement>) => {
const isMultiSort = event.shiftKey;
onSort(column.key as string, direction === 'ASC' ? 'DESC' : 'ASC', isMultiSort);
};
// collecting header cell refs to handle manual column resize
useLayoutEffect(() => {
if (headerRef.current) {
headerCellRefs.current[column.key] = headerRef.current;
}
}, [headerRef, column.key]);
// TODO: this is a workaround to handle manual column resize;
useEffect(() => {
const headerCellParent = headerRef.current?.parentElement;
if (headerCellParent) {
// `lastElement` is an HTML element added by react-data-grid for resizing columns.
// We add a click event listener to `lastElement` to handle the end of the resize operation.
const lastElement = headerCellParent.lastElementChild;
if (lastElement) {
const handleMouseUp = () => {
let newWidth = headerCellParent.clientWidth;
const columnMinWidth = column.minWidth;
if (columnMinWidth && newWidth < columnMinWidth) {
newWidth = columnMinWidth;
}
onColumnResize?.(column.key as string, newWidth);
};
lastElement.addEventListener('click', handleMouseUp);
return () => {
lastElement.removeEventListener('click', handleMouseUp);
};
}
}
// to handle "Not all code paths return a value." error
return;
}, [column]);
return (
<div
ref={headerRef}
style={{ display: 'flex', justifyContent }}
// TODO find a better solution to this issue, see: https://github.com/adazzle/react-data-grid/issues/3535
// Unblock spacebar event
onKeyDown={(event) => {
if (event.key === ' ') {
event.stopPropagation();
}
}}
>
<button className={styles.headerCellLabel} onClick={handleSort}>
<div>{column.name}</div>
{direction &&
(direction === 'ASC' ? (
<Icon name="arrow-up" size="lg" className={styles.sortIcon} />
) : (
<Icon name="arrow-down" size="lg" className={styles.sortIcon} />
))}
</button>
{isColumnFilterable && (
<Filter
name={column.key}
rows={rows}
filter={filter}
setFilter={setFilter}
field={field}
crossFilterOrder={crossFilterOrder.current}
crossFilterRows={crossFilterRows.current}
/>
)}
</div>
);
};
const handleSort = (columnKey: string, direction: SortDirection, isMultiSort: boolean) => {
let currentSortColumn: SortColumn | undefined;
@ -444,11 +340,18 @@ export function TableNG(props: TableNGProps) {
renderHeaderCell: ({ column, sortDirection }) => (
<HeaderCell
column={column}
rows={rows}
field={field}
onSort={handleSort}
direction={sortDirection}
justifyContent={justifyColumnContent}
filter={filter}
setFilter={setFilter}
filterable={filterable}
onColumnResize={onColumnResize}
headerCellRefs={headerCellRefs}
crossFilterOrder={crossFilterOrder}
crossFilterRows={crossFilterRows}
/>
),
// TODO these anys are making me sad
@ -720,27 +623,6 @@ const getStyles = (theme: GrafanaTheme2, textWrap: boolean) => ({
menuItem: css({
maxWidth: '200px',
}),
headerCellLabel: css({
border: 'none',
padding: 0,
background: 'inherit',
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: theme.typography.fontWeightMedium,
display: 'flex',
alignItems: 'center',
marginRight: theme.spacing(0.5),
'&:hover': {
textDecoration: 'underline',
color: theme.colors.text.link,
},
}),
sortIcon: css({
marginLeft: theme.spacing(0.5),
}),
cell: css({
'--rdg-border-color': theme.colors.border.medium,
borderLeft: 'none',