diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json new file mode 100644 index 00000000000..09e6da02526 --- /dev/null +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -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 +} diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index bdca83c9165..ce234702e25 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -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 { + const state: Partial = {}; + + 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 = 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 = 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] ); diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index a98becfcf55..c599f941123 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -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; } diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 75a082baec9..8eafccedf19 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 5573b593c91..f31a65b698d 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -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 {} @@ -20,21 +14,10 @@ export class TablePanel extends Component { 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 { }); }; + onSortByChange = (sortBy: TableSortByFieldState[]) => { + this.props.onOptionsChange({ + ...this.props.options, + sortBy, + }); + }; + onChangeTableSelection = (val: SelectableValue) => { this.props.onOptionsChange({ ...this.props.options, @@ -82,6 +72,8 @@ export class TablePanel extends Component { data={frame} noHeader={!options.showHeader} resizable={true} + initialSortBy={options.sortBy} + onSortByChange={this.onSortByChange} onColumnResize={this.onColumnResize} /> ); diff --git a/public/app/plugins/panel/table/types.ts b/public/app/plugins/panel/table/types.ts index b8b46f653b9..9126762388f 100644 --- a/public/app/plugins/panel/table/types.ts +++ b/public/app/plugins/panel/table/types.ts @@ -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 {