Table: Adds new footer feature that can show Totals or other column calculations (#38653)

* table footer for showing calculations

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Scott Lepper 2021-09-22 09:29:07 -04:00 committed by GitHub
parent bf0dc3ef62
commit 593140cfa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 25 deletions

View File

@ -132,7 +132,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@ -263,7 +263,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@ -351,7 +351,7 @@
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@ -387,7 +387,7 @@
},
{
"collapsed": false,
"datasource": null,
"datasource": "gdev-testdata",
"gridPos": {
"h": 1,
"w": 24,
@ -460,7 +460,7 @@
"value": [
{
"title": "Details",
"url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}"
"url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}"
}
]
}
@ -479,7 +479,7 @@
"options": {
"showHeader": true
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"alias": "S1",
@ -571,7 +571,7 @@
}
]
},
"pluginVersion": "7.5.0-pre",
"pluginVersion": "",
"targets": [
{
"refId": "A",
@ -604,6 +604,55 @@
}
],
"type": "table"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 26
},
"id": 12,
"options": {
"footer": {
"show": true,
"fields": "",
"reducer": [
"sum"
]
},
"showHeader": true
},
"pluginVersion": "",
"title": "Footer",
"type": "table"
}
],
"schemaVersion": 27,
@ -635,5 +684,5 @@
"timezone": "",
"title": "Panel Tests - React Table",
"uid": "U_bZIMRMk",
"version": 1
"version": 6
}

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react';
import { Select, Table } from '@grafana/ui';
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
import { PanelOptions } from './models.gen';
@ -9,6 +8,7 @@ import { FilterItem, TableSortByFieldState } from '@grafana/ui/src/components/Ta
import { dispatch } from '../../../store/store';
import { applyFilterFromTable } from '../../../features/variables/adhoc/actions';
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
import { getFooterCells } from './footer';
interface Props extends PanelProps<PanelOptions> {}
@ -79,6 +79,7 @@ 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
@ -92,21 +93,21 @@ export class TablePanel extends Component<Props> {
onSortByChange={this.onSortByChange}
onColumnResize={this.onColumnResize}
onCellFilterAdded={this.onCellFilterAdded}
footerValues={footerValues}
/>
);
}
getCurrentFrameIndex() {
const { data, options } = this.props;
const count = data.series?.length;
return options.frameIndex > 0 && options.frameIndex < count ? options.frameIndex : 0;
getCurrentFrameIndex(frames: DataFrame[], options: PanelOptions) {
return options.frameIndex > 0 && options.frameIndex < frames.length ? options.frameIndex : 0;
}
render() {
const { data, height, width } = this.props;
const { data, height, width, options } = this.props;
const count = data.series?.length;
const hasFields = data.series[0]?.fields.length;
const frames = data.series;
const count = frames?.length;
const hasFields = frames[0]?.fields.length;
if (!count || !hasFields) {
return <div className={tableStyles.noData}>No data</div>;
@ -115,8 +116,8 @@ export class TablePanel extends Component<Props> {
if (count > 1) {
const inputHeight = config.theme.spacing.formInputHeight;
const padding = 8 * 2;
const currentIndex = this.getCurrentFrameIndex();
const names = data.series.map((frame, index) => {
const currentIndex = this.getCurrentFrameIndex(frames, options);
const names = frames.map((frame, index) => {
return {
label: getFrameDisplayName(frame),
value: index,

View File

@ -0,0 +1,40 @@
import {
DataFrame,
Field,
FieldType,
formattedValueToString,
getDisplayProcessor,
reduceField,
fieldReducers,
} from '@grafana/data';
import { FooterItem } from '@grafana/ui/src/components/Table/types';
import { TableFooterCalc } from './models.gen';
import { config } from 'app/core/config';
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

@ -16,12 +16,23 @@ export interface PanelOptions {
showHeader: boolean;
showTypeIcons?: boolean;
sortBy?: TableSortByFieldState[];
footer?: TableFooterCalc; // TODO: should be array (options builder is limited)
}
export interface TableFooterCalc {
show: boolean;
reducer: string[]; // actually 1 value
fields?: string[];
}
export const defaultPanelOptions: PanelOptions = {
frameIndex: 0,
showHeader: true,
showTypeIcons: false,
footer: {
show: false,
reducer: [],
},
};
export interface PanelFieldConfig {

View File

@ -1,4 +1,11 @@
import { PanelPlugin } from '@grafana/data';
import {
FieldOverrideContext,
FieldType,
getFieldDisplayName,
PanelPlugin,
ReducerID,
standardEditorsRegistry,
} from '@grafana/data';
import { TablePanel } from './TablePanel';
import { PanelOptions, PanelFieldConfig, defaultPanelOptions, defaultPanelFieldConfig } from './models.gen';
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
@ -75,10 +82,52 @@ export const plugin = new PanelPlugin<PanelOptions, PanelFieldConfig>(TablePanel
},
})
.setPanelOptions((builder) => {
builder.addBooleanSwitch({
path: 'showHeader',
name: 'Show header',
description: "To display table's header or not to display",
defaultValue: defaultPanelOptions.showHeader,
});
builder
.addBooleanSwitch({
path: 'showHeader',
name: 'Show header',
description: "To display table's header or not to display",
defaultValue: defaultPanelOptions.showHeader,
})
.addBooleanSwitch({
path: 'footer.show',
name: 'Show Footer',
description: "To display table's footer or not to display",
defaultValue: defaultPanelOptions.footer?.show,
})
.addCustomEditor({
id: 'footer.reducer',
path: 'footer.reducer',
name: 'Calculation',
description: 'Choose a reducer function / calculation',
editor: standardEditorsRegistry.get('stats-picker').editor as any,
defaultValue: [ReducerID.sum],
showIf: (cfg) => cfg.footer?.show,
})
.addMultiSelect({
path: 'footer.fields',
name: 'Fields',
description: 'Select the fields that should be calculated',
settings: {
allowCustomValue: false,
options: [],
placeholder: 'All Numeric Fields',
getOptions: async (context: FieldOverrideContext) => {
const options = [];
if (context && context.data && context.data.length > 0) {
const frame = context.data[0];
for (const field of frame.fields) {
if (field.type === FieldType.number) {
const name = getFieldDisplayName(field, frame, context.data);
const value = field.name;
options.push({ value, label: name } as any);
}
}
}
return options;
},
},
defaultValue: '',
showIf: (cfg) => cfg.footer?.show,
});
});