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": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -263,7 +263,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pluginVersion": "7.5.0-pre",
|
"pluginVersion": "",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -351,7 +351,7 @@
|
|||||||
"showHeader": true,
|
"showHeader": true,
|
||||||
"sortBy": []
|
"sortBy": []
|
||||||
},
|
},
|
||||||
"pluginVersion": "7.5.0-pre",
|
"pluginVersion": "",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -387,7 +387,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"datasource": null,
|
"datasource": "gdev-testdata",
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 1,
|
"h": 1,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
@ -460,7 +460,7 @@
|
|||||||
"value": [
|
"value": [
|
||||||
{
|
{
|
||||||
"title": "Details",
|
"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": {
|
"options": {
|
||||||
"showHeader": true
|
"showHeader": true
|
||||||
},
|
},
|
||||||
"pluginVersion": "7.5.0-pre",
|
"pluginVersion": "",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"alias": "S1",
|
"alias": "S1",
|
||||||
@ -571,7 +571,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pluginVersion": "7.5.0-pre",
|
"pluginVersion": "",
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -604,6 +604,55 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "table"
|
"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,
|
"schemaVersion": 27,
|
||||||
@ -635,5 +684,5 @@
|
|||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Panel Tests - React Table",
|
"title": "Panel Tests - React Table",
|
||||||
"uid": "U_bZIMRMk",
|
"uid": "U_bZIMRMk",
|
||||||
"version": 1
|
"version": 6
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Select, Table } from '@grafana/ui';
|
import { Select, Table } from '@grafana/ui';
|
||||||
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
|
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
@ -9,6 +8,7 @@ import { FilterItem, TableSortByFieldState } from '@grafana/ui/src/components/Ta
|
|||||||
import { dispatch } from '../../../store/store';
|
import { dispatch } from '../../../store/store';
|
||||||
import { applyFilterFromTable } from '../../../features/variables/adhoc/actions';
|
import { applyFilterFromTable } from '../../../features/variables/adhoc/actions';
|
||||||
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||||
|
import { getFooterCells } from './footer';
|
||||||
|
|
||||||
interface Props extends PanelProps<PanelOptions> {}
|
interface Props extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
@ -79,6 +79,7 @@ export class TablePanel extends Component<Props> {
|
|||||||
|
|
||||||
renderTable(frame: DataFrame, width: number, height: number) {
|
renderTable(frame: DataFrame, width: number, height: number) {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
|
const footerValues = options.footer?.show ? getFooterCells(frame, options.footer) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
@ -92,21 +93,21 @@ export class TablePanel extends Component<Props> {
|
|||||||
onSortByChange={this.onSortByChange}
|
onSortByChange={this.onSortByChange}
|
||||||
onColumnResize={this.onColumnResize}
|
onColumnResize={this.onColumnResize}
|
||||||
onCellFilterAdded={this.onCellFilterAdded}
|
onCellFilterAdded={this.onCellFilterAdded}
|
||||||
|
footerValues={footerValues}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentFrameIndex() {
|
getCurrentFrameIndex(frames: DataFrame[], options: PanelOptions) {
|
||||||
const { data, options } = this.props;
|
return options.frameIndex > 0 && options.frameIndex < frames.length ? options.frameIndex : 0;
|
||||||
const count = data.series?.length;
|
|
||||||
return options.frameIndex > 0 && options.frameIndex < count ? options.frameIndex : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, height, width } = this.props;
|
const { data, height, width, options } = this.props;
|
||||||
|
|
||||||
const count = data.series?.length;
|
const frames = data.series;
|
||||||
const hasFields = data.series[0]?.fields.length;
|
const count = frames?.length;
|
||||||
|
const hasFields = frames[0]?.fields.length;
|
||||||
|
|
||||||
if (!count || !hasFields) {
|
if (!count || !hasFields) {
|
||||||
return <div className={tableStyles.noData}>No data</div>;
|
return <div className={tableStyles.noData}>No data</div>;
|
||||||
@ -115,8 +116,8 @@ export class TablePanel extends Component<Props> {
|
|||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
const inputHeight = config.theme.spacing.formInputHeight;
|
const inputHeight = config.theme.spacing.formInputHeight;
|
||||||
const padding = 8 * 2;
|
const padding = 8 * 2;
|
||||||
const currentIndex = this.getCurrentFrameIndex();
|
const currentIndex = this.getCurrentFrameIndex(frames, options);
|
||||||
const names = data.series.map((frame, index) => {
|
const names = frames.map((frame, index) => {
|
||||||
return {
|
return {
|
||||||
label: getFrameDisplayName(frame),
|
label: getFrameDisplayName(frame),
|
||||||
value: index,
|
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;
|
showHeader: boolean;
|
||||||
showTypeIcons?: boolean;
|
showTypeIcons?: boolean;
|
||||||
sortBy?: TableSortByFieldState[];
|
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 = {
|
export const defaultPanelOptions: PanelOptions = {
|
||||||
frameIndex: 0,
|
frameIndex: 0,
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
showTypeIcons: false,
|
showTypeIcons: false,
|
||||||
|
footer: {
|
||||||
|
show: false,
|
||||||
|
reducer: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PanelFieldConfig {
|
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 { TablePanel } from './TablePanel';
|
||||||
import { PanelOptions, PanelFieldConfig, defaultPanelOptions, defaultPanelFieldConfig } from './models.gen';
|
import { PanelOptions, PanelFieldConfig, defaultPanelOptions, defaultPanelFieldConfig } from './models.gen';
|
||||||
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
|
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
|
||||||
@ -75,10 +82,52 @@ export const plugin = new PanelPlugin<PanelOptions, PanelFieldConfig>(TablePanel
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder) => {
|
||||||
builder.addBooleanSwitch({
|
builder
|
||||||
path: 'showHeader',
|
.addBooleanSwitch({
|
||||||
name: 'Show header',
|
path: 'showHeader',
|
||||||
description: "To display table's header or not to display",
|
name: 'Show header',
|
||||||
defaultValue: defaultPanelOptions.showHeader,
|
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