mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
bf0dc3ef62
commit
593140cfa4
@ -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
|
||||
}
|
@ -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,
|
||||
|
40
public/app/plugins/panel/table/footer.ts
Normal file
40
public/app/plugins/panel/table/footer.ts
Normal 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));
|
||||
}
|
@ -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 {
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user