mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TablePanel: Footer now updates values on column filtering (#56354)
* Table footer now updates values on column filtering * Backwards compatibility
This commit is contained in:
parent
17433f2166
commit
48c27872af
@ -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"]
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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));
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user