TablePanel: Footer now updates values on column filtering (#56354)

* Table footer now updates values on column filtering

* Backwards compatibility
This commit is contained in:
Victor Marin 2022-10-12 07:57:49 +03:00 committed by GitHub
parent 17433f2166
commit 48c27872af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 68 deletions

View File

@ -1564,7 +1564,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"],
@ -1615,7 +1617,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[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.", "11"],
[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"]
],
"packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -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<Props> = memo((props: Props) => {
noHeader,
resizable = true,
initialSortBy,
footerValues,
footerOptions,
showTypeIcons,
footerValues,
enablePagination,
} = props;
@ -135,17 +138,19 @@ export const Table: FC<Props> = memo((props: Props) => {
const tableDivRef = useRef<HTMLDivElement>(null);
const fixedSizeListScrollbarRef = useRef<HTMLDivElement>(null);
const tableStyles = useStyles2(getTableStyles);
const theme = useTheme2();
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(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<Props> = 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<Props> = 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<Props> = 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<Props> = memo((props: Props) => {
No data
</div>
)}
{footerValues && (
{footerItems && (
<FooterRow
height={footerHeight}
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerValues}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
/>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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<PanelOptions> {}
@ -97,7 +96,6 @@ export class TablePanel extends Component<Props> {
renderTable(frame: DataFrame, width: number, height: number) {
const { options } = this.props;
const footerValues = options.footer?.show ? getFooterCells(frame, options.footer) : undefined;
return (
<Table
@ -111,7 +109,7 @@ export class TablePanel extends Component<Props> {
onSortByChange={this.onSortByChange}
onColumnResize={this.onColumnResize}
onCellFilterAdded={this.onCellFilterAdded}
footerValues={footerValues}
footerOptions={options.footer}
enablePagination={options.footer?.enablePagination}
/>
);

View File

@ -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));
}

View File

@ -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,