Table: Refactoring state handling for expanded rows (#60791)

* Table: Refactoring state handling for expanded rows

* Simplify row expander

* remove console.log

* review fixes

* Simplify hook name

* fixed test
This commit is contained in:
Torkel Ödegaard 2022-12-28 19:37:17 +01:00 committed by GitHub
parent 14c2209b33
commit 497ce81867
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 244 additions and 230 deletions

View File

@ -1419,18 +1419,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/Table/Table.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
[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/TableCell.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@ -1443,6 +1436,18 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Table/TableCellInspectModal.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/Table/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "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/reducer.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/components/Table/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]

View File

@ -3,10 +3,8 @@ import { ColumnInstance, HeaderGroup } from 'react-table';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes';
import { EmptyCell, FooterCell } from './FooterCell';
import { getTableStyles, TableStyles } from './styles';
import { TableStyles } from './styles';
import { FooterItem } from './types';
export interface FooterRowProps {
@ -14,12 +12,12 @@ export interface FooterRowProps {
footerGroups: HeaderGroup[];
footerValues: FooterItem[];
isPaginationVisible: boolean;
tableStyles: TableStyles;
}
export const FooterRow = (props: FooterRowProps) => {
const { totalColumnsWidth, footerGroups, isPaginationVisible } = props;
const { totalColumnsWidth, footerGroups, isPaginationVisible, tableStyles } = props;
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
const tableStyles = useStyles2(getTableStyles);
return (
<div

View File

@ -3,22 +3,21 @@ import { HeaderGroup, Column } from 'react-table';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes';
import { getFieldTypeIcon } from '../../types';
import { Icon } from '../Icon/Icon';
import { Filter } from './Filter';
import { getTableStyles, TableStyles } from './styles';
import { TableStyles } from './styles';
export interface HeaderRowProps {
headerGroups: HeaderGroup[];
showTypeIcons?: boolean;
tableStyles: TableStyles;
}
export const HeaderRow = (props: HeaderRowProps) => {
const { headerGroups, showTypeIcons } = props;
const { headerGroups, showTypeIcons, tableStyles } = props;
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
const tableStyles = useStyles2(getTableStyles);
return (
<div role="rowgroup" className={tableStyles.headerRow}>

View File

@ -1,41 +1,23 @@
import React, { FC } from 'react';
import { Row } from 'react-table';
import React from 'react';
import { useStyles2 } from '../../themes';
import { Icon } from '../Icon/Icon';
import { getTableStyles } from './styles';
import { TableStyles } from './styles';
import { GrafanaTableRow } from './types';
export interface Props {
row: Row;
expandedIndexes: Set<number>;
setExpandedIndexes: (indexes: Set<number>) => void;
row: GrafanaTableRow;
tableStyles: TableStyles;
}
export const RowExpander: FC<Props> = ({ row, expandedIndexes, setExpandedIndexes }) => {
const tableStyles = useStyles2(getTableStyles);
const isExpanded = expandedIndexes.has(row.index);
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
export function RowExpander({ row, tableStyles }: Props) {
return (
<div
className={tableStyles.expanderCell}
onClick={() => {
const newExpandedIndexes = new Set(expandedIndexes);
if (isExpanded) {
newExpandedIndexes.delete(row.index);
} else {
newExpandedIndexes.add(row.index);
}
setExpandedIndexes(newExpandedIndexes);
}}
>
<div className={tableStyles.expanderCell} {...row.getToggleRowExpandedProps()}>
<Icon
aria-label={isExpanded ? 'Close trace' : 'Open trace'}
name={isExpanded ? 'angle-down' : 'angle-right'}
aria-label={row.isExpanded ? 'Collapse row' : 'Expand row'}
name={row.isExpanded ? 'angle-down' : 'angle-right'}
size="xl"
/>
</div>
);
};
}

View File

@ -4,7 +4,8 @@ import React from 'react';
import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame } from '@grafana/data';
import { Props, Table } from './Table';
import { Table } from './Table';
import { Props } from './types';
function getDefaultDataFrame(): DataFrame {
const dataFrame = toDataFrame({
@ -555,7 +556,7 @@ describe('Table', () => {
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
]);
within(rows[1]).getByLabelText('Open trace').click();
within(rows[1]).getByLabelText('Expand row').click();
const rowsAfterClick = within(getTable()).getAllByRole('row');
expect(within(rowsAfterClick[1]).getByRole('table')).toBeInTheDocument();
expect(within(rowsAfterClick[1]).getByText(/number0/)).toBeInTheDocument();

View File

@ -1,18 +1,17 @@
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Cell,
TableState,
useAbsoluteLayout,
useExpanded,
useFilters,
usePagination,
useResizeColumns,
useSortBy,
useTable,
} from 'react-table';
import usePrevious from 'react-use/lib/usePrevious';
import { VariableSizeList } from 'react-window';
import { DataFrame, getFieldDisplayName, Field, ReducerID } from '@grafana/data';
import { Field, ReducerID } from '@grafana/data';
import { useStyles2, useTheme2 } from '../../themes';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
@ -21,16 +20,10 @@ import { Pagination } from '../Pagination/Pagination';
import { FooterRow } from './FooterRow';
import { HeaderRow } from './HeaderRow';
import { TableCell } from './TableCell';
import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks';
import { getInitialState, useTableStateReducer } from './reducer';
import { getTableStyles } from './styles';
import {
TableColumnResizeActionCallback,
TableFilterActionCallback,
FooterItem,
TableSortByActionCallback,
TableSortByFieldState,
TableFooterCalc,
GrafanaTableColumn,
} from './types';
import { FooterItem, GrafanaTableState, Props } from './types';
import {
getColumns,
sortCaseInsensitive,
@ -42,91 +35,6 @@ import {
const COLUMN_MIN_WIDTH = 150;
export interface Props {
ariaLabel?: string;
data: DataFrame;
width: number;
height: number;
/** Minimal column width specified in pixels */
columnMinWidth?: number;
noHeader?: boolean;
showTypeIcons?: boolean;
resizable?: boolean;
initialSortBy?: TableSortByFieldState[];
onColumnResize?: TableColumnResizeActionCallback;
onSortByChange?: TableSortByActionCallback;
onCellFilterAdded?: TableFilterActionCallback;
footerOptions?: TableFooterCalc;
footerValues?: FooterItem[];
enablePagination?: boolean;
/** @alpha */
subData?: DataFrame[];
}
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
return useCallback(
(newState: TableState, action: { type: string }) => {
switch (action.type) {
case 'columnDoneResizing':
if (onColumnResize) {
const info = (newState.columnResizing.headerIdWidths as any)[0];
const columnIdString = info[0];
const fieldIndex = parseInt(columnIdString, 10);
const width = Math.round(newState.columnResizing.columnWidths[columnIdString] as number);
const field = data.fields[fieldIndex];
if (!field) {
return newState;
}
const fieldDisplayName = getFieldDisplayName(field, data);
onColumnResize(fieldDisplayName, width);
}
case 'toggleSortBy':
if (onSortByChange) {
const sortByFields: TableSortByFieldState[] = [];
for (const sortItem of newState.sortBy) {
const field = data.fields[parseInt(sortItem.id, 10)];
if (!field) {
continue;
}
sortByFields.push({
displayName: getFieldDisplayName(field, data),
desc: sortItem.desc,
});
}
onSortByChange(sortByFields);
}
break;
}
return newState;
},
[data, onColumnResize, onSortByChange]
);
}
function getInitialState(initialSortBy: Props['initialSortBy'], columns: GrafanaTableColumn[]): Partial<TableState> {
const state: Partial<TableState> = {};
if (initialSortBy) {
state.sortBy = [];
for (const sortBy of initialSortBy) {
for (const col of columns) {
if (col.Header === sortBy.displayName) {
state.sortBy.push({ id: col.id!, desc: sortBy.desc });
}
}
}
}
return state;
}
export const Table = memo((props: Props) => {
const {
ariaLabel,
@ -152,8 +60,6 @@ export const Table = memo((props: Props) => {
const theme = useTheme2();
const headerHeight = noHeader ? 0 : tableStyles.rowHeight;
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues);
const [expandedIndexes, setExpandedIndexes] = useState<Set<number>>(new Set());
const prevExpandedIndexes = usePrevious(expandedIndexes);
const footerHeight = useMemo(() => {
const EXTENDED_ROW_HEIGHT = headerHeight;
@ -197,18 +103,8 @@ export const Table = memo((props: Props) => {
// React-table column definitions
const memoizedColumns = useMemo(
() =>
getColumns(
data,
width,
columnMinWidth,
expandedIndexes,
setExpandedIndexes,
!!subData?.length,
footerItems,
isCountRowsSet
),
[data, width, columnMinWidth, footerItems, subData, expandedIndexes, isCountRowsSet]
() => getColumns(data, width, columnMinWidth, !!subData?.length, footerItems, isCountRowsSet),
[data, width, columnMinWidth, footerItems, subData, isCountRowsSet]
);
// Internal react table state reducer
@ -242,7 +138,9 @@ export const Table = memo((props: Props) => {
gotoPage,
setPageSize,
pageOptions,
} = useTable(options, useFilters, useSortBy, usePagination, useAbsoluteLayout, useResizeColumns);
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
const extendedState = state as GrafanaTableState;
/*
Footer value calculation is being moved in the Table component and the footerValues prop will be deprecated.
@ -299,45 +197,12 @@ export const Table = memo((props: Props) => {
setPageSize(pageSize);
}, [pageSize, setPageSize]);
useEffect(() => {
// react-table caches the height of cells so we need to reset them when expanding/collapsing rows
// We need to take the minimum of the current expanded indexes and the previous expandedIndexes array to account
// for collapsed rows, since they disappear from expandedIndexes but still keep their expanded height
listRef.current?.resetAfterIndex(
Math.min(...Array.from(expandedIndexes), ...(prevExpandedIndexes ? Array.from(prevExpandedIndexes) : []))
);
}, [expandedIndexes, prevExpandedIndexes]);
useEffect(() => {
// To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
// we need to bring the element from the VariableSizeList scope to the outer Table container scope,
// because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
// it always visible since the entire width is in view.
// Select the scrollbar element from the VariableSizeList scope
const listVerticalScrollbarHTML = (variableSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
'.track-vertical'
);
// Select Table custom scrollbars
const tableScrollbarView = (tableDivRef.current as HTMLDivElement)?.firstChild;
//If they exists, move the scrollbar element to the Table container scope
if (tableScrollbarView && listVerticalScrollbarHTML) {
listVerticalScrollbarHTML?.remove();
(tableScrollbarView as HTMLDivElement).querySelector(':scope > .track-vertical')?.remove();
(tableScrollbarView as HTMLDivElement).append(listVerticalScrollbarHTML as Node);
}
});
useEffect(() => {
setExpandedIndexes(new Set());
}, [data, subData]);
useResetVariableListSizeCache(extendedState, listRef, data);
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
const renderSubTable = React.useCallback(
(rowIndex: number) => {
if (expandedIndexes.has(rowIndex)) {
if (state.expanded[rowIndex]) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === rowIndex);
if (rowSubData) {
const noHeader = !!rowSubData.meta?.custom?.noHeader;
@ -362,7 +227,7 @@ export const Table = memo((props: Props) => {
}
return null;
},
[expandedIndexes, subData, tableStyles.rowHeight, theme.colors, width]
[state.expanded, subData, tableStyles.rowHeight, theme.colors, width]
);
const RenderRow = React.useCallback(
@ -430,7 +295,7 @@ export const Table = memo((props: Props) => {
}
const getItemSize = (index: number): number => {
if (expandedIndexes.has(index)) {
if (state.expanded[index]) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === index);
if (rowSubData) {
const noHeader = !!rowSubData.meta?.custom?.noHeader;
@ -452,7 +317,9 @@ export const Table = memo((props: Props) => {
<div {...getTableProps()} className={tableStyles.table} aria-label={ariaLabel} role="table" ref={tableDivRef}>
<CustomScrollbar hideVerticalTrack={true}>
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
{!noHeader && <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
{!noHeader && (
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
)}
{itemCount > 0 ? (
<div ref={variableSizeListScrollbarRef}>
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
@ -479,6 +346,7 @@ export const Table = memo((props: Props) => {
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles}
/>
)}
</div>

View File

@ -0,0 +1,53 @@
import React, { useEffect } from 'react';
import { VariableSizeList } from 'react-window';
import { DataFrame } from '@grafana/data';
import { GrafanaTableState } from './types';
/**
To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
we need to bring the element from the VariableSizeList scope to the outer Table container scope,
because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
it always visible since the entire width is in view.
Select the scrollbar element from the VariableSizeList scope
*/
export function useFixScrollbarContainer(
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
tableDivRef: React.RefObject<HTMLDivElement>
) {
useEffect(() => {
const listVerticalScrollbarHTML = (variableSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
'.track-vertical'
);
// Select Table custom scrollbars
const tableScrollbarView = (tableDivRef.current as HTMLDivElement)?.firstChild;
//If they exists, move the scrollbar element to the Table container scope
if (tableScrollbarView && listVerticalScrollbarHTML) {
listVerticalScrollbarHTML?.remove();
(tableScrollbarView as HTMLDivElement).querySelector(':scope > .track-vertical')?.remove();
(tableScrollbarView as HTMLDivElement).append(listVerticalScrollbarHTML as Node);
}
});
}
/**
react-table caches the height of cells so we need to reset them when expanding/collapsing rows
We need to take the minimum of the current expanded indexes and the previous expandedIndexes array to account
for collapsed rows, since they disappear from expandedIndexes but still keep their expanded height
*/
export function useResetVariableListSizeCache(
extendedState: GrafanaTableState,
listRef: React.RefObject<VariableSizeList>,
data: DataFrame
) {
useEffect(() => {
if (extendedState.lastExpandedIndex !== undefined) {
listRef.current?.resetAfterIndex(Math.max(extendedState.lastExpandedIndex - 1, 0));
return;
}
}, [extendedState.lastExpandedIndex, extendedState.toggleRowExpandedCounter, listRef, data]);
}

View File

@ -0,0 +1,87 @@
import { useCallback } from 'react';
import { getFieldDisplayName } from '@grafana/data';
import { TableSortByFieldState, GrafanaTableColumn, GrafanaTableState, Props } from './types';
export interface ActionType {
type: string;
id: string | undefined;
}
export function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
return useCallback(
(newState: GrafanaTableState, action: ActionType) => {
switch (action.type) {
case 'columnDoneResizing':
if (onColumnResize) {
const info = (newState.columnResizing.headerIdWidths as any)[0];
const columnIdString = info[0];
const fieldIndex = parseInt(columnIdString, 10);
const width = Math.round(newState.columnResizing.columnWidths[columnIdString] as number);
const field = data.fields[fieldIndex];
if (!field) {
return newState;
}
const fieldDisplayName = getFieldDisplayName(field, data);
onColumnResize(fieldDisplayName, width);
}
case 'toggleSortBy':
if (onSortByChange) {
const sortByFields: TableSortByFieldState[] = [];
for (const sortItem of newState.sortBy) {
const field = data.fields[parseInt(sortItem.id, 10)];
if (!field) {
continue;
}
sortByFields.push({
displayName: getFieldDisplayName(field, data),
desc: sortItem.desc,
});
}
onSortByChange(sortByFields);
}
case 'toggleRowExpanded': {
if (action.id) {
return {
...newState,
lastExpandedIndex: parseInt(action.id, 10),
toggleRowExpandedCounter: newState.toggleRowExpandedCounter + 1,
};
}
}
}
return newState;
},
[data, onColumnResize, onSortByChange]
);
}
export function getInitialState(
initialSortBy: Props['initialSortBy'],
columns: GrafanaTableColumn[]
): Partial<GrafanaTableState> {
const state: Partial<GrafanaTableState> = {
toggleRowExpandedCounter: 0,
};
if (initialSortBy) {
state.sortBy = [];
for (const sortBy of initialSortBy) {
for (const col of columns) {
if (col.Header === sortBy.displayName) {
state.sortBy.push({ id: col.id!, desc: sortBy.desc });
}
}
}
}
return state;
}

View File

@ -1,8 +1,8 @@
import { Property } from 'csstype';
import { FC } from 'react';
import { CellProps, Column, Row } from 'react-table';
import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table';
import { Field, KeyValue, SelectableValue } from '@grafana/data';
import { DataFrame, Field, KeyValue, SelectableValue } from '@grafana/data';
import { TableStyles } from './styles';
@ -52,3 +52,31 @@ export interface TableFooterCalc {
enablePagination?: boolean;
countRows?: boolean;
}
export interface GrafanaTableState extends TableState {
lastExpandedIndex?: number;
toggleRowExpandedCounter: number;
}
export interface GrafanaTableRow extends Row, UseExpandedRowProps<{}> {}
export interface Props {
ariaLabel?: string;
data: DataFrame;
width: number;
height: number;
/** Minimal column width specified in pixels */
columnMinWidth?: number;
noHeader?: boolean;
showTypeIcons?: boolean;
resizable?: boolean;
initialSortBy?: TableSortByFieldState[];
onColumnResize?: TableColumnResizeActionCallback;
onSortByChange?: TableSortByActionCallback;
onCellFilterAdded?: TableFilterActionCallback;
footerOptions?: TableFooterCalc;
footerValues?: FooterItem[];
enablePagination?: boolean;
/** @alpha */
subData?: DataFrame[];
}

View File

@ -46,21 +46,21 @@ function getData() {
describe('Table utils', () => {
describe('getColumns', () => {
it('Should build columns from DataFrame', () => {
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
const columns = getColumns(getData(), 1000, 120, false);
expect(columns[0].Header).toBe('Time');
expect(columns[1].Header).toBe('Value');
});
it('Should distribute width and use field config width', () => {
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
const columns = getColumns(getData(), 1000, 120, false);
expect(columns[0].width).toBe(450);
expect(columns[1].width).toBe(100);
});
it('Should distribute width and use field config width with expander enabled', () => {
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, true);
const columns = getColumns(getData(), 1000, 120, true);
expect(columns[0].width).toBe(50); // expander column
expect(columns[1].width).toBe(425);
@ -68,7 +68,7 @@ describe('Table utils', () => {
});
it('Should set field on columns', () => {
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
const columns = getColumns(getData(), 1000, 120, false);
expect(columns[0].field.name).toBe('Time');
expect(columns[1].field.name).toBe('Value');

View File

@ -1,7 +1,6 @@
import { Property } from 'csstype';
import { clone } from 'lodash';
import memoizeOne from 'memoize-one';
import React from 'react';
import { Row } from 'react-table';
import {
@ -65,35 +64,29 @@ export function getColumns(
data: DataFrame,
availableWidth: number,
columnMinWidth: number,
expandedIndexes: Set<number>,
setExpandedIndexes: (indexes: Set<number>) => void,
expander: boolean,
footerValues?: FooterItem[],
isCountRowsSet?: boolean
): GrafanaTableColumn[] {
const columns: GrafanaTableColumn[] = expander
? [
{
// Make an expander cell
Header: () => null, // No header
id: 'expander', // It needs an ID
Cell: ({ row }) => {
return <RowExpander row={row} expandedIndexes={expandedIndexes} setExpandedIndexes={setExpandedIndexes} />;
},
width: EXPANDER_WIDTH,
minWidth: EXPANDER_WIDTH,
filter: (rows: Row[], id: string, filterValues?: SelectableValue[]) => {
return [];
},
justifyContent: 'left',
field: data.fields[0],
sortType: 'basic',
},
]
: [];
const columns: GrafanaTableColumn[] = [];
let fieldCountWithoutWidth = 0;
if (expander) {
columns.push({
// Make an expander cell
Header: () => null, // No header
id: 'expander', // It needs an ID
Cell: RowExpander,
width: EXPANDER_WIDTH,
minWidth: EXPANDER_WIDTH,
filter: (rows: Row[], id: string, filterValues?: SelectableValue[]) => {
return [];
},
justifyContent: 'left',
field: data.fields[0],
sortType: 'basic',
});
availableWidth -= EXPANDER_WIDTH;
}