Table: Add row number column option (#62256)

* baldm0mma/addRowNumbers/ add showRowNums to panel cue

* baldm0mma/addRowNumbers/ add panel option for sowing row numbers

* baldm0mma/addRowNumbers/ update typing for showRowNums

* baldm0mma/addRowNumbers/ add buildFieldsForOptionalRowNums

* baldm0mma/addRowNumbers/ update addOptionalNumbersRowToTable

* baldm0mma/addRowNumbers/ adjust display method to return numeric and text values

* baldm0mma/ chaneg prop name to match

* baldm0mma/addRowNumbers/ update boolean swicth path

* baldm0mma/addRowNumbers/ move function

* baldm0mma/addRowNumbers/ add getToggleHiddenProps

* baldm0mma/addRowNumbers/ remove addNumbersRowToTable second arg

* baldm0mma/addRowNumbers/ add updateInitialState

* baldm0mma/addRowNumbers/ update getInitialState reducer with initialShowRowNumbers arg

* baldm0mma/addRowNumbers/ add useEffect for RowNumberColumn toggling

* baldm0mma/addRowNums/ bootleg fix

* baldm0mma/addRowNumbers/ export OPTIONAL_ROW_NUMBER_COLUMN_WIDTH

* baldm0mma/addRowNumbers/ add annos for readability

* baldm0mma/addRowNumbers/ remove superfluous annos

* baldm0mma/addRowNumbers/ add a few logs

* baldm0mma/addRowNumbers/ update annos

* baldm0mma/addRowNumbers/ update which footer row displays reducer type

* baldm0mma/addRowNumbers/ abstract away defaultRowNumberColumnFieldData

* baldm0mma/addRowNumbers/ update annos in utils.tsx

* baldm0mma/addRowNumbers/ update annos for defaultRowNumberColumnFieldData

* baldm0mma/addRowNumbers/ mark unused args with underscore

* baldm0mma/addRowNumbers/ add annos to addRowNumbersFieldToData

* baldm0mma/addRowNumbers/ update utils file type

* baldm0mma/addRowNumbers/ remove console.logs

* baldm0mma/addRowNumbers/ update file type

* baldm0mma/addRowNumbers/ update annos in utils

* baldm0mma/addRowNumbers/ remove superfluous footerGroups object

* baldm0mma/addRowNumbers/ add annos for tests

* baldm0mma/addRowNumbers/ add annos for self

* baldm0mma/addRowNumbers/ add tests to table.test.tsx

* baldm0mma/addRowNumbers/ update tests in utils.test

* baldm0mma/addRowNumbers/ update annos and tests

* baldm0mma/addRowNumbers/ remove console.logs

* baldm0mma/addRowNumbers/ update utils file ext

* baldm0mma/addRowNumbers/ update anno in table.tsx

* baldm0mma/addRowNumbers/ update annos in table.tsx

* baldm0mma/addRowNumbers/ rem error annos

* baldm0mma/addRowNumbers/ revert footerCell

* baldm0mma/addRowNumbers/ revert tests

* baldm0mma/addRowNumbers/ skip tests

* baldm0mma/addRowNumbers/ revert table isCountRowSet

* baldm0mma/addRowNumbers/ remove cloneDeep

* baldm0mma/addRowNumbers/ update filterFields

* baldm0mma/addRowNumbers/ skip tests

* Refactor count rows

* baldm0mma/addRowNumbers/ rem test skips

* baldm0mma/addRowNumbers/ update with annos

* baldm0mma/addRowNumbers/ skip timeing out test

* baldm0mma/addRowNumbers/ static row numbering and test updates

* baldm0mma/addRowNumbers/ remove dupe

---------

Co-authored-by: Victor Marin <victor.marin@grafana.com>
This commit is contained in:
Jev Forsberg 2023-02-03 07:00:29 -07:00 committed by GitHub
parent 41dc88bd25
commit 2e98f5063b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 237 additions and 46 deletions

View File

@ -1487,7 +1487,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
],
"packages/grafana-ui/src/components/Table/utils.tsx:5381": [
"packages/grafana-ui/src/components/Table/utils.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, "Unexpected any. Specify a different type.", "2"],

View File

@ -25,6 +25,7 @@ export const FooterCell = (props: FooterProps) => {
if (props.value && !Array.isArray(props.value)) {
return <span>{props.value}</span>;
}
if (props.value && Array.isArray(props.value) && props.value.length > 0) {
return (
<ul className={cell}>
@ -32,7 +33,7 @@ export const FooterCell = (props: FooterProps) => {
const key = Object.keys(v)[0];
return (
<li className={list} key={i}>
<span>{key}:</span>
<span>{key}</span>
<span>{v[key]}</span>
</li>
);
@ -40,6 +41,7 @@ export const FooterCell = (props: FooterProps) => {
</ul>
);
}
return EmptyCell;
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import { ColumnInstance, HeaderGroup } from 'react-table';
import { fieldReducers, ReducerID } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EmptyCell, FooterCell } from './FooterCell';
@ -63,12 +64,13 @@ export function getFooterValue(index: number, footerValues?: FooterItem[], isCou
}
if (isCountRowsSet) {
const count = footerValues[index];
if (typeof count !== 'string') {
if (footerValues[index] === undefined) {
return EmptyCell;
}
return FooterCell({ value: [{ Count: count }] });
const key = fieldReducers.get(ReducerID.count).name;
return FooterCell({ value: [{ [key]: String(footerValues[index]) }] });
}
return FooterCell({ value: footerValues[index] });

View File

@ -152,6 +152,38 @@ describe('Table', () => {
});
});
describe('when `showRowNums` is toggled', () => {
const showRowNumsTestContext = {
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number',
type: FieldType.number,
values: [1, 1, 1, 2, 2, 3, 4, 5],
config: {
custom: {
filterable: true,
},
},
},
],
}),
};
it('should render the (fields.length) rows when `showRowNums` is untoggled', () => {
getTestContext({ ...showRowNumsTestContext, showRowNums: false });
expect(screen.getAllByRole('columnheader')).toHaveLength(1);
});
it('should render (fields.length + 1) rows row when `showRowNums` is toggled', () => {
getTestContext({ ...showRowNumsTestContext, showRowNums: true });
expect(screen.getAllByRole('columnheader')).toHaveLength(2);
});
});
describe('when mounted with footer', () => {
it('then footer should be displayed', () => {
const footerValues = ['a', 'b', 'c'];
@ -445,7 +477,7 @@ describe('Table', () => {
});
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
'Count:'
'Count'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
});
@ -471,7 +503,7 @@ describe('Table', () => {
});
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
'Count:'
'Count'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');

View File

@ -1,4 +1,5 @@
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { clone } from 'lodash';
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState, UIEventHandler } from 'react';
import {
Cell,
useAbsoluteLayout,
@ -11,7 +12,7 @@ import {
} from 'react-table';
import { VariableSizeList } from 'react-window';
import { Field, ReducerID } from '@grafana/data';
import { DataFrame, Field, ReducerID } from '@grafana/data';
import { useStyles2, useTheme2 } from '../../themes';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
@ -31,6 +32,7 @@ import {
getFooterItems,
createFooterCalculationValues,
EXPANDER_WIDTH,
buildFieldsForOptionalRowNums,
} from './utils';
const COLUMN_MIN_WIDTH = 150;
@ -48,6 +50,7 @@ export const Table = memo((props: Props) => {
columnMinWidth = COLUMN_MIN_WIDTH,
noHeader,
resizable = true,
showRowNums,
initialSortBy,
footerOptions,
showTypeIcons,
@ -84,8 +87,8 @@ export const Table = memo((props: Props) => {
return EXTENDED_ROW_HEIGHT;
}, [footerItems]);
// React table data array. This data acts just like a dummy array to let react-table know how many rows exist
// The cells use the field to look up values
// React table data array. This data acts just like a dummy array to let react-table know how many rows exist.
// The cells use the field to look up values, therefore this is simply a length/size placeholder.
const memoizedData = useMemo(() => {
if (!data.fields.length) {
return [];
@ -96,6 +99,7 @@ export const Table = memo((props: Props) => {
return Array(data.length).fill(0);
}, [data]);
// This checks whether `Show table footer` is toggled on, the `Calculation` is set to `Count`, and finally, whether `Count rows` is toggled on.
const isCountRowsSet = Boolean(
footerOptions?.countRows &&
footerOptions.reducer &&
@ -105,7 +109,8 @@ export const Table = memo((props: Props) => {
// React-table column definitions
const memoizedColumns = useMemo(
() => getColumns(data, width, columnMinWidth, !!subData?.length, footerItems, isCountRowsSet),
() =>
getColumns(addRowNumbersFieldToData(data), width, columnMinWidth, !!subData?.length, footerItems, isCountRowsSet),
[data, width, columnMinWidth, footerItems, subData, isCountRowsSet]
);
@ -118,14 +123,14 @@ export const Table = memo((props: Props) => {
data: memoizedData,
disableResizing: !resizable,
stateReducer: stateReducer,
initialState: getInitialState(initialSortBy, memoizedColumns),
initialState: getInitialState(initialSortBy, showRowNums, memoizedColumns),
autoResetFilters: false,
sortTypes: {
number: sortNumber, // the builtin number type on react-table does not handle NaN values
'alphanumeric-insensitive': sortCaseInsensitive, // should be replace with the builtin string when react-table is upgraded, see https://github.com/tannerlinsley/react-table/pull/3235
},
}),
[initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer]
[initialSortBy, showRowNums, memoizedColumns, memoizedData, resizable, stateReducer]
);
const {
@ -134,16 +139,21 @@ export const Table = memo((props: Props) => {
rows,
prepareRow,
totalColumnsWidth,
footerGroups,
page,
state,
gotoPage,
setPageSize,
pageOptions,
setHiddenColumns,
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
const extendedState = state as GrafanaTableState;
// Hide Row Number column on toggle
useEffect(() => {
!!showRowNums ? setHiddenColumns([]) : setHiddenColumns(['0']);
}, [showRowNums, setHiddenColumns]);
/*
Footer value calculation is being moved in the Table component and the footerValues prop will be deprecated.
The footerValues prop is still used in the Table component for backwards compatibility. Adding the
@ -166,20 +176,31 @@ export const Table = memo((props: Props) => {
return;
}
if (isCountRowsSet) {
const footerItemsCountRows: FooterItem[] = [];
/*
Update the 1st index of the empty array with the row length, which will then default the 0th index to `undefined`,
which will therefore account for our Row Numbers column, and render the count in the following column.
This will work with tables with only a single column as well, since our Row Numbers column is being prepended reguardless.
*/
footerItemsCountRows[1] = headerGroups[0]?.headers[0]?.filteredRows.length.toString() ?? data.length.toString();
setFooterItems(footerItemsCountRows);
return;
}
const footerItems = getFooterItems(
headerGroups[0].headers as unknown as Array<{ field: Field }>,
/*
The `headerGroups` object is NOT based on the `data.fields`, but instead on the currently rendered headers in the Table,
which may or may not include the Row Numbers column.
*/
headerGroups[0].headers as unknown as Array<{ id: string; field: Field }>,
// The `rows` object, on the other hand, is based on the `data.fields` data, and therefore ALWAYS include the Row Numbers column data.
createFooterCalculationValues(rows),
footerOptions,
theme
);
if (isCountRowsSet) {
const footerItemsCountRows: FooterItem[] = new Array(footerItems.length).fill(undefined);
footerItemsCountRows[0] = data.length.toString();
setFooterItems(footerItemsCountRows);
} else {
setFooterItems(footerItems);
}
setFooterItems(footerItems);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [footerOptions, theme, state.filters, data]);
@ -214,7 +235,7 @@ export const Table = memo((props: Props) => {
useResetVariableListSizeCache(extendedState, listRef, data);
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
const renderSubTable = React.useCallback(
const renderSubTable = useCallback(
(rowIndex: number) => {
if (state.expanded[rowIndex]) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === rowIndex);
@ -245,28 +266,40 @@ export const Table = memo((props: Props) => {
[state.expanded, subData, tableStyles.rowHeight, theme.colors, width]
);
const RenderRow = React.useCallback(
const RenderRow = useCallback(
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
let row = rows[rowIndex];
if (enablePagination) {
row = page[rowIndex];
}
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className={tableStyles.row}>
{/*add the subtable to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/}
{renderSubTable(rowIndex)}
{row.cells.map((cell: Cell, index: number) => (
<TableCell
key={index}
tableStyles={tableStyles}
cell={cell}
onCellFilterAdded={onCellFilterAdded}
columnIndex={index}
columnCount={row.cells.length}
/>
))}
{row.cells.map((cell: Cell, index: number) => {
/*
Here we test if the `row.cell` is of id === "0"; only if the user has toggled ON `Show row numbers` in the panelOptions panel will this cell exist.
This cell had already been built, but with undefined values. This is so we can now update our empty/undefined `cell.value` to the current `rowIndex + 1`.
This will assure that on sort, our row numbers don't also sort; but instewad stay in their respective rows.
*/
if (cell.column.id === '0') {
cell.value = rowIndex + 1;
}
return (
<TableCell
key={index}
tableStyles={tableStyles}
cell={cell}
onCellFilterAdded={onCellFilterAdded}
columnIndex={index}
columnCount={row.cells.length}
/>
);
})}
</div>
);
},
@ -309,6 +342,19 @@ export const Table = memo((props: Props) => {
);
}
// This adds the `Field` data needed to display a column with Row Numbers.
function addRowNumbersFieldToData(data: DataFrame): DataFrame {
/*
The `length` prop in a DataFrame tells us the amount of rows of data that will appear in our table;
with that we can build the correct buffered incrementing values for our Row Number column data.
*/
const rowField: Field = buildFieldsForOptionalRowNums(data.length);
// Clone data to avoid unwanted mutation.
const clonedData = clone(data);
clonedData.fields = [rowField, ...data.fields];
return clonedData;
}
const getItemSize = (index: number): number => {
if (state.expanded[index]) {
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === index);
@ -320,7 +366,7 @@ export const Table = memo((props: Props) => {
return tableStyles.rowHeight;
};
const handleScroll: React.UIEventHandler = (event) => {
const handleScroll: UIEventHandler = (event) => {
const { scrollTop } = event.target as HTMLDivElement;
if (listRef.current !== null) {
@ -359,7 +405,11 @@ export const Table = memo((props: Props) => {
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
/*
The `headerGroups` and `footerGroups` objects destructured from the `useTable` hook are perfectly equivalent, in deep value, but not reference.
So we can use `headerGroups` here for building the footer, and no longer have a need for `footerGroups`.
*/
footerGroups={headerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles}
/>

View File

@ -13,7 +13,7 @@ export interface Props {
userProps?: object;
}
export const TableCell: FC<Props> = ({ cell, tableStyles, onCellFilterAdded, columnIndex, columnCount, userProps }) => {
export const TableCell: FC<Props> = ({ cell, tableStyles, onCellFilterAdded, userProps }) => {
const cellProps = cell.getCellProps();
const field = (cell.column as unknown as GrafanaTableColumn).field;

View File

@ -65,10 +65,12 @@ export function useTableStateReducer({ onColumnResize, onSortByChange, data }: P
export function getInitialState(
initialSortBy: Props['initialSortBy'],
initialShowRowNumbers: Props['showRowNums'],
columns: GrafanaTableColumn[]
): Partial<GrafanaTableState> {
const state: Partial<GrafanaTableState> = {
toggleRowExpandedCounter: 0,
hiddenColumns: initialShowRowNumbers ? [] : ['0'],
};
if (initialSortBy) {

View File

@ -76,6 +76,7 @@ export interface Props {
noHeader?: boolean;
showTypeIcons?: boolean;
resizable?: boolean;
showRowNums?: boolean;
initialSortBy?: TableSortByFieldState[];
onColumnResize?: TableColumnResizeActionCallback;
onSortByChange?: TableSortByActionCallback;

View File

@ -12,6 +12,8 @@ import {
sortNumber,
sortOptions,
valuesToOptions,
buildBufferedEmptyValues,
buildFieldsForOptionalRowNums,
} from './utils';
function getData() {
@ -365,6 +367,30 @@ describe('Table utils', () => {
});
});
describe('buildBufferedEmptyValues', () => {
it('should build a buffered VectorArray of empty values the length of the number passed to it as an argument', () => {
const arrayVectorLength = 10;
const bufferedArray = buildBufferedEmptyValues(arrayVectorLength);
expect(bufferedArray).toBeInstanceOf(ArrayVector);
// Convert back into a standard array type.
const nonBufferedArray = Array.from(bufferedArray);
expect(nonBufferedArray[0]).toEqual(undefined);
expect(nonBufferedArray[nonBufferedArray.length - 1]).toEqual(undefined);
});
});
describe('buildFieldsForOptionalRowNums', () => {
it('should prepend a Field to a `DataFrame.field` so row numbers can be calculated and rendered', () => {
const builtField = buildFieldsForOptionalRowNums(10);
expect(builtField['name']).toEqual(' ');
expect(builtField['type']).toEqual(FieldType.string);
expect(typeof builtField['display']).toBe('function');
expect(typeof builtField['config']).toBe('object');
});
});
describe('getFilteredOptions', () => {
describe('when called without filterValues', () => {
it('then it should return an empty array', () => {

View File

@ -40,6 +40,7 @@ import {
} from './types';
export const EXPANDER_WIDTH = 50;
export const OPTIONAL_ROW_NUMBER_COLUMN_WIDTH = 50;
export function getTextAlign(field?: Field): Property.JustifyContent {
if (!field) {
@ -85,7 +86,7 @@ export function getColumns(
Cell: RowExpander,
width: EXPANDER_WIDTH,
minWidth: EXPANDER_WIDTH,
filter: (rows: Row[], id: string, filterValues?: SelectableValue[]) => {
filter: (_rows: Row[], _id: string, _filterValues?: SelectableValue[]) => {
return [];
},
justifyContent: 'left',
@ -126,7 +127,7 @@ export function getColumns(
id: fieldIndex.toString(),
field: field,
Header: getFieldDisplayName(field, data),
accessor: (row: any, i: number) => {
accessor: (_row: any, i: number) => {
return field.values.get(i);
},
sortType: selectSortType(field.type),
@ -162,6 +163,25 @@ export function getColumns(
return columns;
}
/*
Build `Field` data for row numbers and prepend to the field array;
this way, on other column's sort, the row numbers will persist in their proper place.
*/
export function buildFieldsForOptionalRowNums(totalRows: number): Field {
return {
...defaultRowNumberColumnFieldData,
values: buildBufferedEmptyValues(totalRows),
};
}
/*
This gives us an empty buffered ArrayVector of the desired length to match the table data.
It is simply a data placeholder for the Row Number column data.
*/
export function buildBufferedEmptyValues(totalRows: number): ArrayVector<string> {
return new ArrayVector(new Array(totalRows));
}
export function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent {
switch (displayMode) {
case TableCellDisplayMode.ColorText:
@ -299,20 +319,36 @@ function toNumber(value: any): number {
}
export function getFooterItems(
filterFields: Array<{ field: Field }>,
filterFields: Array<{ id: string; field: Field }>,
values: any[number],
options: TableFooterCalc,
theme2: GrafanaTheme2
): FooterItem[] {
/*
Here, `filterFields` is passed to as the `headerGroups[0].headers` array that was destrcutured from the `useTable` hook.
Unfortunately, since the `headerGroups` object is data based ONLY on the rendered "non-hidden" column headers,
it will NOT include the Row Number column if it has been toggled off. This will shift the rendering of the footer left 1 column,
creating an off-by-one issue. This is why we test for a `field.id` of "0". If the condition is truthy, the togglable Row Number column is being rendered,
and we can proceed normally. If not, we must add the field data in its place so that the footer data renders in the expected column.
*/
if (!filterFields.some((field) => field.id === '0')) {
const length = values.length;
// Build the additional field that will correct the off-by-one footer issue.
const fieldToAdd = { id: '0', field: buildFieldsForOptionalRowNums(length) };
filterFields = [fieldToAdd, ...filterFields];
}
return filterFields.map((data, i) => {
if (data.field.type !== FieldType.number) {
// show the reducer in the first column
if (i === 0 && options.reducer && options.reducer.length > 0) {
// Show the reducer type ("Total", "Range", "Count", "Delta", etc) in the first non "Row Number" column, only if it cannot be numerically reduced.
if (i === 1 && options.reducer && options.reducer.length > 0) {
const reducer = fieldReducers.get(options.reducer[0]);
return reducer.name;
}
// Otherwise return `undefined`, which will render an <EmptyCell />.
return undefined;
}
let newField = clone(data.field);
newField.values = new ArrayVector(values[i]);
newField.state = undefined;
@ -336,6 +372,7 @@ function getFormattedValue(field: Field, reducer: string[], theme: GrafanaTheme2
return formattedValueToString(fmt(v));
}
// This strips the raw vales from the `rows` object.
export function createFooterCalculationValues(rows: Row[]): any[number] {
const values: any[number] = [];
@ -412,3 +449,31 @@ export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDispl
};
}
}
/*
For building the column data for the togglable Row Number field.
`values` property is omitted, as it will be added at a later time.
*/
export const defaultRowNumberColumnFieldData: Omit<Field, 'values'> = {
/*
Single whitespace as value for `name` property so as to render an empty/invisible column header;
without the single whitespace, falsey headers (empty strings) are given a default name of "Value".
*/
name: ' ',
display: function (value: string) {
return {
numeric: Number(value),
text: value,
};
},
type: FieldType.string,
config: {
color: { mode: 'thresholds' },
custom: {
align: 'auto',
cellOptions: { type: 'auto' },
inspect: false,
width: OPTIONAL_ROW_NUMBER_COLUMN_WIDTH,
},
},
};

View File

@ -5,6 +5,7 @@ import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableV
import { PanelDataErrorView } from '@grafana/runtime';
import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui';
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
import { OPTIONAL_ROW_NUMBER_COLUMN_WIDTH } from '@grafana/ui/src/components/Table/utils';
import { PanelOptions } from './models.gen';
@ -18,7 +19,7 @@ export function TablePanel(props: Props) {
const frames = data.series;
const mainFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex === undefined);
const subFrames = frames.filter((f) => f.meta?.custom?.parentRowIndex !== undefined);
const count = mainFrames?.length;
const count = mainFrames.length;
const hasFields = mainFrames[0]?.fields.length;
const currentIndex = getCurrentFrameIndex(mainFrames, options);
const main = mainFrames[currentIndex];
@ -41,14 +42,16 @@ export function TablePanel(props: Props) {
const tableElement = (
<Table
height={tableHeight}
width={width}
// This calculation is to accommodate the optionally rendered Row Numbers Column
width={options.showRowNums ? width : width + OPTIONAL_ROW_NUMBER_COLUMN_WIDTH}
data={main}
noHeader={!options.showHeader}
showTypeIcons={options.showTypeIcons}
resizable={true}
showRowNums={options.showRowNums}
initialSortBy={options.sortBy}
onSortByChange={(sortBy) => onSortByChange(sortBy, props)}
onColumnResize={(displayName, width) => onColumnResize(displayName, width, props)}
onColumnResize={(displayName, resizedWidth) => onColumnResize(displayName, resizedWidth, props)}
onCellFilterAdded={panelContext.onAddAdHocFilter}
footerOptions={options.footer}
enablePagination={options.footer?.enablePagination}

View File

@ -15,6 +15,7 @@ export const modelVersion = Object.freeze([1, 0]);
export interface PanelOptions {
frameIndex: number;
showHeader: boolean;
showRowNums?: boolean;
showTypeIcons?: boolean;
sortBy?: TableSortByFieldState[];
footer?: TableFooterCalc; // TODO: should be array (options builder is limited)
@ -23,6 +24,7 @@ export interface PanelOptions {
export const defaultPanelOptions: PanelOptions = {
frameIndex: 0,
showHeader: true,
showRowNums: false,
showTypeIcons: false,
footer: {
show: false,

View File

@ -108,6 +108,11 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
name: 'Show table header',
defaultValue: defaultPanelOptions.showHeader,
})
.addBooleanSwitch({
path: 'showRowNums',
name: 'Show row numbers',
defaultValue: defaultPanelOptions.showRowNums,
})
.addBooleanSwitch({
path: 'footer.show',
category: [footerCategory],

View File

@ -28,6 +28,7 @@ composableKinds: PanelCfg: {
frameIndex: number | *0
showHeader: bool | *true
showTypeIcons: bool | *false
showRowNums?: bool | *false
sortBy?: [...common.TableSortByFieldState]
} @cuetsy(kind="interface")
PanelFieldConfig: common.TableFieldOptions & {} @cuetsy(kind="interface")