mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
14c2209b33
commit
497ce81867
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
53
packages/grafana-ui/src/components/Table/hooks.ts
Normal file
53
packages/grafana-ui/src/components/Table/hooks.ts
Normal 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]);
|
||||
}
|
87
packages/grafana-ui/src/components/Table/reducer.ts
Normal file
87
packages/grafana-ui/src/components/Table/reducer.ts
Normal 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;
|
||||
}
|
@ -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[];
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user