mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
41dc88bd25
commit
2e98f5063b
@ -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"],
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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] });
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -76,6 +76,7 @@ export interface Props {
|
||||
noHeader?: boolean;
|
||||
showTypeIcons?: boolean;
|
||||
resizable?: boolean;
|
||||
showRowNums?: boolean;
|
||||
initialSortBy?: TableSortByFieldState[];
|
||||
onColumnResize?: TableColumnResizeActionCallback;
|
||||
onSortByChange?: TableSortByActionCallback;
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user