diff --git a/.betterer.results b/.betterer.results index 0f974928fec..42cca8866fa 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-ui/src/components/Table/FooterCell.tsx b/packages/grafana-ui/src/components/Table/FooterCell.tsx index b4ada5f441a..df11100193b 100644 --- a/packages/grafana-ui/src/components/Table/FooterCell.tsx +++ b/packages/grafana-ui/src/components/Table/FooterCell.tsx @@ -25,6 +25,7 @@ export const FooterCell = (props: FooterProps) => { if (props.value && !Array.isArray(props.value)) { return {props.value}; } + if (props.value && Array.isArray(props.value) && props.value.length > 0) { return ( ); } + return EmptyCell; }; diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/FooterRow.tsx index 6acbd766dc5..8f598b680b1 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/FooterRow.tsx @@ -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] }); diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index 5a76193e9cd..1ecdcdb6bcd 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -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'); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index ef8b83530ff..364d15dd283 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -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 (
{/*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) => ( - - ))} + {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 ( + + ); + })}
); }, @@ -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) => { diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index b0c622288eb..00d69ef171e 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -13,7 +13,7 @@ export interface Props { userProps?: object; } -export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, columnIndex, columnCount, userProps }) => { +export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, userProps }) => { const cellProps = cell.getCellProps(); const field = (cell.column as unknown as GrafanaTableColumn).field; diff --git a/packages/grafana-ui/src/components/Table/reducer.ts b/packages/grafana-ui/src/components/Table/reducer.ts index f8da8dc659e..cf33fcb446f 100644 --- a/packages/grafana-ui/src/components/Table/reducer.ts +++ b/packages/grafana-ui/src/components/Table/reducer.ts @@ -65,10 +65,12 @@ export function useTableStateReducer({ onColumnResize, onSortByChange, data }: P export function getInitialState( initialSortBy: Props['initialSortBy'], + initialShowRowNumbers: Props['showRowNums'], columns: GrafanaTableColumn[] ): Partial { const state: Partial = { toggleRowExpandedCounter: 0, + hiddenColumns: initialShowRowNumbers ? [] : ['0'], }; if (initialSortBy) { diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 4200c52907d..ba7f9ad3d6d 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -76,6 +76,7 @@ export interface Props { noHeader?: boolean; showTypeIcons?: boolean; resizable?: boolean; + showRowNums?: boolean; initialSortBy?: TableSortByFieldState[]; onColumnResize?: TableColumnResizeActionCallback; onSortByChange?: TableSortByActionCallback; diff --git a/packages/grafana-ui/src/components/Table/utils.test.tsx b/packages/grafana-ui/src/components/Table/utils.test.tsx index c28bf5d9ac9..51f8a97d348 100644 --- a/packages/grafana-ui/src/components/Table/utils.test.tsx +++ b/packages/grafana-ui/src/components/Table/utils.test.tsx @@ -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', () => { diff --git a/packages/grafana-ui/src/components/Table/utils.tsx b/packages/grafana-ui/src/components/Table/utils.ts similarity index 78% rename from packages/grafana-ui/src/components/Table/utils.tsx rename to packages/grafana-ui/src/components/Table/utils.ts index 77b68af4b1b..5f794438b66 100644 --- a/packages/grafana-ui/src/components/Table/utils.tsx +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -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 { + 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 . 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 = { + /* + 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, + }, + }, +}; diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index d475eed7329..88ba8557b8f 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -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 = ( 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} diff --git a/public/app/plugins/panel/table/models.gen.ts b/public/app/plugins/panel/table/models.gen.ts index 0e11c30c9ac..65abf26b389 100644 --- a/public/app/plugins/panel/table/models.gen.ts +++ b/public/app/plugins/panel/table/models.gen.ts @@ -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, diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index ef284a0c148..28d3c2d71ac 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -108,6 +108,11 @@ export const plugin = new PanelPlugin(TablePane name: 'Show table header', defaultValue: defaultPanelOptions.showHeader, }) + .addBooleanSwitch({ + path: 'showRowNums', + name: 'Show row numbers', + defaultValue: defaultPanelOptions.showRowNums, + }) .addBooleanSwitch({ path: 'footer.show', category: [footerCategory], diff --git a/public/app/plugins/panel/table/panelcfg.cue b/public/app/plugins/panel/table/panelcfg.cue index 3b49cc6b87b..c9a0429d557 100644 --- a/public/app/plugins/panel/table/panelcfg.cue +++ b/public/app/plugins/panel/table/panelcfg.cue @@ -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")