TablePanel: Adding sort order persistance (#24705)

* TablePanel: Adding sort order persistance

* adds panel test dashboard for table panel
This commit is contained in:
Torkel Ödegaard 2020-05-19 09:23:25 +02:00 committed by GitHub
parent 6b29c11776
commit b709f6ad71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 681 additions and 35 deletions

View File

@ -0,0 +1,602 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 7,
"title": "Cell styles",
"type": "row"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "color-background"
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 20
},
{
"color": "orange",
"value": 60
},
{
"color": "red",
"value": 70
}
]
},
"unit": "degree"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Max"
},
"properties": [
{
"id": "custom.width",
"value": 84
}
]
},
{
"matcher": {
"id": "byName",
"options": "Last"
},
"properties": [
{
"id": "custom.width",
"value": 78
}
]
},
{
"matcher": {
"id": "byName",
"options": "Mean"
},
"properties": [
{
"id": "custom.width",
"value": 74
}
]
},
{
"matcher": {
"id": "byName",
"options": "Field"
},
"properties": [
{
"id": "custom.align",
"value": "left"
}
]
}
]
},
"gridPos": {
"h": 16,
"w": 7,
"x": 0,
"y": 1
},
"id": 4,
"options": {
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Last"
}
]
},
"pluginVersion": "7.1.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 15,
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Colored background",
"transformations": [
{
"id": "reduce",
"options": {
"reducers": ["max", "mean", "last"]
}
}
],
"type": "table"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"custom": {
"align": null
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "orange",
"value": null
},
{
"color": "red",
"value": 50
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.displayMode",
"value": "gradient-gauge"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Info"
},
"properties": [
{
"id": "custom.width",
"value": 92
}
]
},
{
"matcher": {
"id": "byName",
"options": "Min"
},
"properties": [
{
"id": "custom.width",
"value": 76
}
]
},
{
"matcher": {
"id": "byName",
"options": "Max"
},
"properties": [
{
"id": "custom.width",
"value": 89
}
]
},
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "custom.width",
"value": 165
}
]
}
]
},
"gridPos": {
"h": 16,
"w": 8,
"x": 7,
"y": 1
},
"id": 2,
"options": {
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "Min"
}
]
},
"pluginVersion": "7.1.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk_table",
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Bar gauge cells",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"indexByName": {
"Info": 1,
"Max": 3,
"Min": 2,
"Time": 0,
"Value": 4
},
"renameByName": {}
}
}
],
"type": "table"
},
{
"datasource": "gdev-testdata",
"description": "",
"fieldConfig": {
"defaults": {
"custom": {
"align": null
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 50
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.displayMode",
"value": "lcd-gauge"
},
{
"id": "custom.align",
"value": "center"
}
]
}
]
},
"gridPos": {
"h": 16,
"w": 9,
"x": 15,
"y": 1
},
"id": 5,
"options": {
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.1.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk_table",
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Retro LCD cell",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Info": false,
"Max": true,
"Min": true,
"Time": false
},
"indexByName": {
"Info": 1,
"Max": 3,
"Min": 2,
"Time": 0,
"Value": 4
},
"renameByName": {}
}
}
],
"type": "table"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 9,
"panels": [],
"title": "Data links",
"type": "row"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "color-text"
},
"decimals": 2,
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 20
},
{
"color": "orange",
"value": 50
},
{
"color": "red",
"value": 70
}
]
},
"unit": "percent"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "custom.align",
"value": null
}
]
},
{
"matcher": {
"id": "byName",
"options": "{name=\"S1\", server=\"A\"}"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Details",
"url": "http://detail?serverLabel=${__field.labels.server}&valueNumeric=${__value.numeric}"
}
]
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 18
},
"id": 3,
"maxDataPoints": "10",
"options": {
"showHeader": true
},
"pluginVersion": "7.1.0-pre",
"targets": [
{
"alias": "S1",
"labels": "server=A",
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
"stringInput": ""
},
{
"alias": "S2",
"labels": "server=B",
"refId": "B",
"scenarioId": "random_walk",
"seriesCount": 1,
"stringInput": ""
},
{
"alias": "S3",
"labels": "server=C",
"refId": "C",
"scenarioId": "random_walk",
"seriesCount": 1,
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "Data link with labels and numeric value",
"transformations": [
{
"id": "seriesToColumns",
"options": {}
}
],
"type": "table"
},
{
"datasource": "gdev-testdata",
"fieldConfig": {
"defaults": {
"custom": {
"align": "center",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "blue",
"value": 20
},
{
"color": "orange",
"value": 60
},
{
"color": "red",
"value": 70
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 5,
"x": 12,
"y": 18
},
"id": 10,
"options": {
"showHeader": false,
"sortBy": [
{
"desc": true,
"displayName": "Last"
}
]
},
"pluginVersion": "7.1.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk_table",
"seriesCount": 5,
"stringInput": ""
}
],
"timeFrom": null,
"timeShift": null,
"title": "No header",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Min": true,
"Time": true,
"Value": true
},
"indexByName": {
"Info": 2,
"Max": 4,
"Min": 3,
"Time": 0,
"Value": 1
},
"renameByName": {}
}
}
],
"type": "table"
}
],
"schemaVersion": 25,
"style": "dark",
"tags": ["gdev", "panel-tests"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
},
"timezone": "",
"title": "Panel Tests - React Table",
"uid": "U_bZIMRMk",
"version": 17
}

View File

@ -1,5 +1,5 @@
import React, { FC, memo, useCallback, useMemo } from 'react';
import { DataFrame, Field } from '@grafana/data';
import { DataFrame, Field, getFieldDisplayName } from '@grafana/data';
import {
Cell,
Column,
@ -14,7 +14,12 @@ import {
import { FixedSizeList } from 'react-window';
import { getColumns, getTextAlign } from './utils';
import { useTheme } from '../../themes';
import { TableColumnResizeActionCallback, TableFilterActionCallback, TableSortByActionCallback } from './types';
import {
TableColumnResizeActionCallback,
TableFilterActionCallback,
TableSortByActionCallback,
TableSortByFieldState,
} from './types';
import { getTableStyles, TableStyles } from './styles';
import { TableCell } from './TableCell';
import { Icon } from '../Icon/Icon';
@ -30,9 +35,10 @@ export interface Props {
columnMinWidth?: number;
noHeader?: boolean;
resizable?: boolean;
initialSortBy?: TableSortByFieldState[];
onCellClick?: TableFilterActionCallback;
onColumnResize?: TableColumnResizeActionCallback;
onSortBy?: TableSortByActionCallback;
onSortByChange?: TableSortByActionCallback;
}
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}> {}
@ -43,25 +49,66 @@ function useTableStateReducer(props: Props) {
switch (action.type) {
case 'columnDoneResizing':
if (props.onColumnResize) {
const { data } = props;
const info = (newState.columnResizing.headerIdWidths as any)[0];
const columnIdString = info[0];
const fieldIndex = parseInt(columnIdString, 10);
const width = Math.round(newState.columnResizing.columnWidths[columnIdString] as number);
props.onColumnResize(fieldIndex, width);
const field = data.fields[fieldIndex];
if (!field) {
return newState;
}
const fieldDisplayName = getFieldDisplayName(field, data);
props.onColumnResize(fieldDisplayName, width);
}
case 'toggleSortBy':
if (props.onSortBy) {
// todo call callback and persist
if (props.onSortByChange) {
const { data } = props;
const sortByFields: TableSortByFieldState[] = [];
for (const sortItem of newState.sortBy) {
const field = data.fields[parseInt(sortItem.id, 10)];
if (!field) {
continue;
}
sortByFields.push({
displayName: getFieldDisplayName(field, data),
desc: sortItem.desc,
});
}
props.onSortByChange(sortByFields);
}
break;
}
return newState;
},
[props.onColumnResize]
[props.onColumnResize, props.onSortByChange, props.data]
);
}
function getInitialState(props: Props, columns: Column[]): Partial<ReactTableInternalState> {
const state: Partial<ReactTableInternalState> = {};
if (props.initialSortBy) {
state.sortBy = [];
for (const sortBy of props.initialSortBy) {
for (const col of columns) {
if (col.Header === sortBy.displayName) {
state.sortBy.push({ id: col.id as string, desc: sortBy.desc });
}
}
}
}
return state;
}
export const Table: FC<Props> = memo((props: Props) => {
const { data, height, onCellClick, width, columnMinWidth = COLUMN_MIN_WIDTH, noHeader, resizable = true } = props;
const theme = useTheme();
@ -91,10 +138,7 @@ export const Table: FC<Props> = memo((props: Props) => {
data: memoizedData,
disableResizing: !resizable,
stateReducer: stateReducer,
// this is how you set initial sort by state
// initialState: {
// sortBy: [{ id: '2', desc: true }],
// },
initialState: getInitialState(props, memoizedColumns),
}),
[memoizedColumns, memoizedData, stateReducer, resizable]
);

View File

@ -26,11 +26,11 @@ export interface TableRow {
}
export type TableFilterActionCallback = (key: string, value: string) => void;
export type TableColumnResizeActionCallback = (fieldIndex: number, width: number) => void;
export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void;
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
export interface TableSortByFieldState {
fieldIndex: number;
displayName: string;
desc?: boolean;
}

View File

@ -45,7 +45,7 @@ export { ModalsProvider, ModalRoot, ModalsController } from './Modal/ModalsConte
export { SetInterval } from './SetInterval/SetInterval';
export { Table } from './Table/Table';
export { TableCellDisplayMode } from './Table/types';
export { TableCellDisplayMode, TableSortByFieldState } from './Table/types';
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
export { TabsBar } from './Tabs/TabsBar';
export { Tab } from './Tabs/Tab';

View File

@ -1,17 +1,11 @@
import React, { Component } from 'react';
import { Table, Select } from '@grafana/ui';
import {
FieldMatcherID,
PanelProps,
DataFrame,
SelectableValue,
getFrameDisplayName,
getFieldDisplayName,
} from '@grafana/data';
import { FieldMatcherID, PanelProps, DataFrame, SelectableValue, getFrameDisplayName } from '@grafana/data';
import { Options } from './types';
import { css } from 'emotion';
import { config } from 'app/core/config';
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
interface Props extends PanelProps<Options> {}
@ -20,21 +14,10 @@ export class TablePanel extends Component<Props> {
super(props);
}
onColumnResize = (fieldIndex: number, width: number) => {
const { fieldConfig, data } = this.props;
onColumnResize = (fieldDisplayName: string, width: number) => {
const { fieldConfig } = this.props;
const { overrides } = fieldConfig;
const frame = data.series[this.getCurrentFrameIndex()];
if (!frame) {
return;
}
const field = frame.fields[fieldIndex];
if (!field) {
return;
}
const fieldDisplayName = getFieldDisplayName(field, frame, data.series);
const matcherId = FieldMatcherID.byName;
const propId = 'custom.width';
@ -62,6 +45,13 @@ export class TablePanel extends Component<Props> {
});
};
onSortByChange = (sortBy: TableSortByFieldState[]) => {
this.props.onOptionsChange({
...this.props.options,
sortBy,
});
};
onChangeTableSelection = (val: SelectableValue<number>) => {
this.props.onOptionsChange({
...this.props.options,
@ -82,6 +72,8 @@ export class TablePanel extends Component<Props> {
data={frame}
noHeader={!options.showHeader}
resizable={true}
initialSortBy={options.sortBy}
onSortByChange={this.onSortByChange}
onColumnResize={this.onColumnResize}
/>
);

View File

@ -1,6 +1,14 @@
import { TableSortByFieldState } from '@grafana/ui';
export interface Options {
frameIndex: number;
showHeader: boolean;
sortBy?: TableSortByFieldState[];
}
export interface TableSortBy {
displayName: string;
desc: boolean;
}
export interface CustomFieldConfig {