diff --git a/.betterer.results b/.betterer.results index 3bf954ddb78..adfead33213 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1639,7 +1639,9 @@ exports[`better eslint`] = { [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.", "11"], + [0, 0, 0, "Do not use any type assertions.", "12"], + [0, 0, 0, "Do not use any type assertions.", "13"] ], "packages/grafana-ui/src/components/Table/TableCell.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], @@ -1692,7 +1694,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Unexpected any. Specify a different type.", "12"] + [0, 0, 0, "Unexpected any. Specify a different type.", "12"], + [0, 0, 0, "Unexpected any. Specify a different type.", "13"], + [0, 0, 0, "Unexpected any. Specify a different type.", "14"], + [0, 0, 0, "Unexpected any. Specify a different type.", "15"] ], "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index d0faba48455..572d50b9021 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo, useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Cell, Column, @@ -12,9 +12,9 @@ import { } from 'react-table'; import { FixedSizeList } from 'react-window'; -import { DataFrame, getFieldDisplayName } from '@grafana/data'; +import { DataFrame, getFieldDisplayName, Field } from '@grafana/data'; -import { useStyles2 } from '../../themes'; +import { useStyles2, useTheme2 } from '../../themes'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; import { Pagination } from '../Pagination/Pagination'; @@ -28,8 +28,9 @@ import { FooterItem, TableSortByActionCallback, TableSortByFieldState, + TableFooterCalc, } from './types'; -import { getColumns, sortCaseInsensitive, sortNumber } from './utils'; +import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFooterCalculationValues } from './utils'; const COLUMN_MIN_WIDTH = 150; @@ -47,6 +48,7 @@ export interface Props { onColumnResize?: TableColumnResizeActionCallback; onSortByChange?: TableSortByActionCallback; onCellFilterAdded?: TableFilterActionCallback; + footerOptions?: TableFooterCalc; footerValues?: FooterItem[]; enablePagination?: boolean; } @@ -126,8 +128,9 @@ export const Table: FC = memo((props: Props) => { noHeader, resizable = true, initialSortBy, - footerValues, + footerOptions, showTypeIcons, + footerValues, enablePagination, } = props; @@ -135,17 +138,19 @@ export const Table: FC = memo((props: Props) => { const tableDivRef = useRef(null); const fixedSizeListScrollbarRef = useRef(null); const tableStyles = useStyles2(getTableStyles); + const theme = useTheme2(); const headerHeight = noHeader ? 0 : tableStyles.cellHeight; + const [footerItems, setFooterItems] = useState(footerValues); const footerHeight = useMemo(() => { const EXTENDED_ROW_HEIGHT = 33; let length = 0; - if (!footerValues) { + if (!footerItems) { return 0; } - for (const fv of footerValues) { + for (const fv of footerItems) { if (Array.isArray(fv) && fv.length > length) { length = fv.length; } @@ -156,7 +161,7 @@ export const Table: FC = memo((props: Props) => { } return EXTENDED_ROW_HEIGHT; - }, [footerValues]); + }, [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 @@ -172,8 +177,8 @@ export const Table: FC = memo((props: Props) => { // React-table column definitions const memoizedColumns = useMemo( - () => getColumns(data, width, columnMinWidth, footerValues), - [data, width, columnMinWidth, footerValues] + () => getColumns(data, width, columnMinWidth, footerItems), + [data, width, columnMinWidth, footerItems] ); // Internal react table state reducer @@ -209,6 +214,38 @@ export const Table: FC = memo((props: Props) => { pageOptions, } = useTable(options, useFilters, useSortBy, usePagination, useAbsoluteLayout, useResizeColumns); + /* + 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 + footerOptions prop will switch the Table component to use the new footer calculation. Using both props will + result in the footerValues prop being ignored. + */ + useEffect(() => { + if (!footerOptions) { + setFooterItems(footerValues); + } + }, [footerValues, footerOptions]); + + useEffect(() => { + if (!footerOptions) { + return; + } + + if (footerOptions.show) { + setFooterItems( + getFooterItems( + headerGroups[0].headers as unknown as Array<{ field: Field }>, + createFooterCalculationValues(rows), + footerOptions, + theme + ) + ); + } else { + setFooterItems(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [footerOptions, theme, state.filters]); + let listHeight = height - (headerHeight + footerHeight); if (enablePagination) { @@ -340,11 +377,11 @@ export const Table: FC = memo((props: Props) => { No data )} - {footerValues && ( + {footerItems && ( diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index 9014619cdf1..0f613eda42e 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -44,3 +44,10 @@ export type GrafanaTableColumn = Column & { justifyContent: Property.JustifyContent; minWidth: number; }; + +export interface TableFooterCalc { + show: boolean; + reducer: string[]; // actually 1 value + fields?: string[]; + enablePagination?: boolean; +} diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 5b6e13c740f..cf5c22139c8 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -1,4 +1,5 @@ import { Property } from 'csstype'; +import { clone } from 'lodash'; import memoizeOne from 'memoize-one'; import { Row } from 'react-table'; @@ -9,6 +10,11 @@ import { formattedValueToString, getFieldDisplayName, SelectableValue, + fieldReducers, + getDisplayProcessor, + reduceField, + GrafanaTheme2, + ArrayVector, } from '@grafana/data'; import { BarGaugeCell } from './BarGaugeCell'; @@ -17,7 +23,14 @@ import { getFooterValue } from './FooterRow'; import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONViewCell } from './JSONViewCell'; -import { CellComponent, TableCellDisplayMode, TableFieldOptions, FooterItem, GrafanaTableColumn } from './types'; +import { + CellComponent, + TableCellDisplayMode, + TableFieldOptions, + FooterItem, + GrafanaTableColumn, + TableFooterCalc, +} from './types'; export function getTextAlign(field?: Field): Property.JustifyContent { if (!field) { @@ -256,3 +269,56 @@ function toNumber(value: any): number { return Number(value); } + +export function getFooterItems( + filterFields: Array<{ field: Field }>, + values: any[number], + options: TableFooterCalc, + theme2: GrafanaTheme2 +): FooterItem[] { + 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) { + const reducer = fieldReducers.get(options.reducer[0]); + return reducer.name; + } + return undefined; + } + let newField = clone(data.field); + newField.values = new ArrayVector(values[i]); + newField.state = undefined; + + data.field = newField; + if (options.fields && options.fields.length > 0) { + const f = options.fields.find((f) => f === data.field.name); + if (f) { + return getFormattedValue(data.field, options.reducer, theme2); + } + return undefined; + } + return getFormattedValue(data.field, options.reducer || [], theme2); + }); +} + +function getFormattedValue(field: Field, reducer: string[], theme: GrafanaTheme2) { + const fmt = field.display ?? getDisplayProcessor({ field, theme }); + const calc = reducer[0]; + const v = reduceField({ field, reducers: reducer })[calc]; + return formattedValueToString(fmt(v)); +} + +export function createFooterCalculationValues(rows: Row[]): any[number] { + const values: any[number] = []; + + for (const key in rows) { + for (const [valKey, val] of Object.entries(rows[key].values)) { + if (values[valKey] === undefined) { + values[valKey] = []; + } + values[valKey].push(val); + } + } + + return values; +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5e5f3db4781..44858fec38d 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -77,7 +77,7 @@ export { PageToolbar } from './PageLayout/PageToolbar'; export { SetInterval } from './SetInterval/SetInterval'; export { Table } from './Table/Table'; -export { TableCellDisplayMode, type TableSortByFieldState } from './Table/types'; +export { TableCellDisplayMode, type TableSortByFieldState, type TableFooterCalc } from './Table/types'; export { TableInputCSV } from './TableInputCSV/TableInputCSV'; export { TabsBar } from './Tabs/TabsBar'; export { Tab } from './Tabs/Tab'; diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index a0d01ad41f1..c7f105259c8 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -19,7 +19,6 @@ import { getDashboardSrv } from '../../../features/dashboard/services/DashboardS import { applyFilterFromTable } from '../../../features/variables/adhoc/actions'; import { dispatch } from '../../../store/store'; -import { getFooterCells } from './footer'; import { PanelOptions } from './models.gen'; interface Props extends PanelProps {} @@ -97,7 +96,6 @@ export class TablePanel extends Component { renderTable(frame: DataFrame, width: number, height: number) { const { options } = this.props; - const footerValues = options.footer?.show ? getFooterCells(frame, options.footer) : undefined; return ( { onSortByChange={this.onSortByChange} onColumnResize={this.onColumnResize} onCellFilterAdded={this.onCellFilterAdded} - footerValues={footerValues} + footerOptions={options.footer} enablePagination={options.footer?.enablePagination} /> ); diff --git a/public/app/plugins/panel/table/footer.ts b/public/app/plugins/panel/table/footer.ts deleted file mode 100644 index 0ac8fa5c80d..00000000000 --- a/public/app/plugins/panel/table/footer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - DataFrame, - Field, - FieldType, - formattedValueToString, - getDisplayProcessor, - reduceField, - fieldReducers, -} from '@grafana/data'; -import { FooterItem } from '@grafana/ui/src/components/Table/types'; -import { config } from 'app/core/config'; - -import { TableFooterCalc } from './models.gen'; - -export function getFooterCells(frame: DataFrame, options?: TableFooterCalc): FooterItem[] { - return frame.fields.map((field, i) => { - if (field.type !== FieldType.number) { - // show the reducer in the first column - if (i === 0 && options && options.reducer.length > 0) { - const reducer = fieldReducers.get(options.reducer[0]); - return reducer.name; - } - return undefined; - } - if (options?.fields && options.fields.length > 0) { - const f = options.fields.find((f) => f === field.name); - if (f) { - return getFormattedValue(field, options.reducer); - } - return undefined; - } - return getFormattedValue(field, options?.reducer || []); - }); -} - -function getFormattedValue(field: Field, reducer: string[]) { - const fmt = field.display ?? getDisplayProcessor({ field, theme: config.theme2 }); - const calc = reducer[0]; - const v = reduceField({ field, reducers: reducer })[calc]; - return formattedValueToString(fmt(v)); -} diff --git a/public/app/plugins/panel/table/models.gen.ts b/public/app/plugins/panel/table/models.gen.ts index 18c02f6cecd..cc01543b6d9 100644 --- a/public/app/plugins/panel/table/models.gen.ts +++ b/public/app/plugins/panel/table/models.gen.ts @@ -3,7 +3,7 @@ // It is currenty hand written but will serve as the target for cuetsy //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import { TableCellDisplayMode, TableSortByFieldState } from '@grafana/ui'; +import { TableCellDisplayMode, TableSortByFieldState, TableFooterCalc } from '@grafana/ui'; import { TableFieldOptions } from '@grafana/schema'; // Only the latest schema version is translated to TypeScript, on the premise @@ -20,13 +20,6 @@ export interface PanelOptions { footer?: TableFooterCalc; // TODO: should be array (options builder is limited) } -export interface TableFooterCalc { - show: boolean; - reducer: string[]; // actually 1 value - fields?: string[]; - enablePagination?: boolean; -} - export const defaultPanelOptions: PanelOptions = { frameIndex: 0, showHeader: true,