From abf015ace227a113fcda67542f2aa77c7f348e66 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 12:13:38 -0800 Subject: [PATCH 01/11] use TableData for timeseries in react --- packages/grafana-ui/src/types/data.ts | 10 ++-- packages/grafana-ui/src/types/panel.ts | 9 +--- .../grafana-ui/src/utils/processTimeSeries.ts | 20 ++++---- public/app/core/table_model.ts | 12 ++--- .../features/dashboard/dashgrid/DataPanel.tsx | 33 +++++-------- .../dashboard/dashgrid/PanelChrome.tsx | 10 ++-- public/app/features/dashboard/utils/panel.ts | 46 +++++++++++++------ public/app/plugins/panel/gauge/GaugePanel.tsx | 22 ++++++--- .../app/plugins/panel/graph2/GraphPanel.tsx | 11 +++-- 9 files changed, 94 insertions(+), 79 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 1e4ccba3948..1ea89bcd28e 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -53,12 +53,9 @@ export interface TimeSeriesVMs { length: number; } -interface Column { - text: string; - title?: string; - type?: string; - sort?: boolean; - desc?: boolean; +export interface Column { + text: string; // name + type?: 'time' | 'number' | 'string' | 'object'; filterable?: boolean; unit?: string; } @@ -67,5 +64,4 @@ export interface TableData { columns: Column[]; rows: any[]; type: string; - columnMap: any; } diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 260ff78df76..4d9dc820b96 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,12 +1,12 @@ import { ComponentClass } from 'react'; -import { TimeSeries, LoadingState, TableData } from './data'; +import { LoadingState, TableData } from './data'; import { TimeRange } from './time'; import { ScopedVars } from './datasource'; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export interface PanelProps { - panelData: PanelData; + data?: TableData[]; timeRange: TimeRange; loading: LoadingState; options: T; @@ -16,11 +16,6 @@ export interface PanelProps { replaceVariables: InterpolateFunction; } -export interface PanelData { - timeSeries?: TimeSeries[]; - tableData?: TableData; -} - export interface PanelEditorProps { options: T; onOptionsChange: (options: T) => void; diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index f5e9f96efba..d8827e567e3 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,17 +4,19 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types'; +import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types'; interface Options { - timeSeries: TimeSeries[]; + data: TableData[]; + xColumn: number; // Time + yColumn: number; // Value nullValueMode: NullValueMode; } -export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { - const vmSeries = timeSeries.map((item, index) => { +export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { + const vmSeries = data.map((item, index) => { const colorIndex = index % colors.length; - const label = item.target; + const label = item.columns[yColumn].text; const result = []; // stat defaults @@ -42,9 +44,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS let previousValue = 0; let previousDeltaUp = true; - for (let i = 0; i < item.datapoints.length; i++) { - currentValue = item.datapoints[i][0]; - currentTime = item.datapoints[i][1]; + for (let i = 0; i < item.rows.length; i++) { + currentValue = item.rows[i][yColumn]; + currentTime = item.rows[i][xColumn]; if (typeof currentTime !== 'number') { continue; @@ -95,7 +97,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS if (previousValue > currentValue) { // counter reset previousDeltaUp = false; - if (i === item.datapoints.length - 1) { + if (i === item.rows.length - 1) { // reset on last delta += currentValue; } diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index fa7170bed13..988c3b1992e 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,17 +1,15 @@ import _ from 'lodash'; +import { Column, TableData } from '@grafana/ui'; -interface Column { - text: string; +// This class mutates and uses the extra column fields +interface ColumnEX extends Column { title?: string; - type?: string; sort?: boolean; desc?: boolean; - filterable?: boolean; - unit?: string; } -export default class TableModel { - columns: Column[]; +export default class TableModel implements TableData { + columns: ColumnEX[]; rows: any[]; type: string; columnMap: any; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 09864d85960..c12462e2b49 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -11,16 +11,16 @@ import { DataQueryResponse, DataQueryError, LoadingState, - PanelData, TableData, TimeRange, - TimeSeries, ScopedVars, } from '@grafana/ui'; +import { toTableData } from '../utils/panel'; + interface RenderProps { loading: LoadingState; - panelData: PanelData; + data: TableData[]; } export interface Props { @@ -44,7 +44,7 @@ export interface State { isFirstLoad: boolean; loading: LoadingState; response: DataQueryResponse; - panelData: PanelData; + data?: TableData[]; } export class DataPanel extends Component { @@ -64,7 +64,6 @@ export class DataPanel extends Component { response: { data: [], }, - panelData: {}, isFirstLoad: true, }; } @@ -146,10 +145,12 @@ export class DataPanel extends Component { onDataResponse(resp); } + const data = toTableData(resp.data); + console.log('Converted:', data); this.setState({ loading: LoadingState.Done, response: resp, - panelData: this.getPanelData(resp), + data, isFirstLoad: false, }); } catch (err) { @@ -172,23 +173,9 @@ export class DataPanel extends Component { } }; - getPanelData(response: DataQueryResponse) { - if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') { - return { - tableData: response.data[0] as TableData, - timeSeries: null, - }; - } - - return { - timeSeries: response.data as TimeSeries[], - tableData: null, - }; - } - render() { const { queries } = this.props; - const { loading, isFirstLoad, panelData } = this.state; + const { loading, isFirstLoad, data } = this.state; // do not render component until we have first data if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { @@ -203,10 +190,12 @@ export class DataPanel extends Component { ); } + console.log('RENDER', data); + return ( <> {loading === LoadingState.Loading && this.renderLoadingState()} - {this.props.children({ loading, panelData })} + {this.props.children({ loading, data })} ); } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 0a9d1d44ceb..0dd82b7dad9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -18,7 +18,7 @@ import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui'; import variables from 'sass/_variables.generated.scss'; @@ -142,7 +142,7 @@ export class PanelChrome extends PureComponent { return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null; } - renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { + renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; const PanelComponent = plugin.exports.reactPanel.panel; @@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent {
{ onDataResponse={this.onDataResponse} onError={this.onDataError} > - {({ loading, panelData }) => { - return this.renderPanelPlugin(loading, panelData, width, height); + {({ loading, data }) => { + return this.renderPanelPlugin(loading, data, width, height); }} ) : ( diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index d14432cb2eb..3058e2b9db6 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,8 +4,7 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { PanelData, TimeRange, TimeSeries } from '@grafana/ui'; -import { TableData } from '@grafana/ui/src'; +import { TableData, TimeRange, TimeSeries } from '@grafana/ui'; // Utils import { isString as _isString } from 'lodash'; @@ -173,16 +172,37 @@ export function getResolution(panel: PanelModel): number { const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints'); const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const snapshotDataToPanelData = (panel: PanelModel): PanelData => { - const snapshotData = panel.snapshotData; - if (isTimeSeries(snapshotData[0])) { - return { - timeSeries: snapshotData, - } as PanelData; - } else if (isTableData(snapshotData[0])) { - return { - tableData: snapshotData[0], - } as PanelData; +export const snapshotDataToPanelData = (panel: PanelModel): TableData[] => { + return toTableData(panel.snapshotData); +}; + +export const toTableData = (results: any[]): TableData[] => { + if (!results) { + return []; } - throw new Error('snapshotData is invalid:' + snapshotData.toString()); + return results.map(data => { + if (isTableData(data)) { + return data as TableData; + } + if (isTimeSeries(data)) { + const ts = data as TimeSeries; + return { + type: 'timeseries', + columns: [ + { + text: ts.target, + unit: ts.unit, + type: 'number', // Is this really true? + }, + { + text: 'time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData; + } + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + }); }; diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b75d4a1c7f3..7913aa33eb0 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -22,29 +22,39 @@ export class GaugePanel extends Component { this.state = { value: this.findValue(props), }; + console.log('CONSTRUCTOR!', this.props.data); } componentDidUpdate(prevProps: Props) { - if (this.props.panelData !== prevProps.panelData) { + console.log('UPDATE', this.props.data); + + if (this.props.data !== prevProps.data) { this.setState({ value: this.findValue(this.props) }); } } findValue(props: Props): number | null { - const { panelData, options } = props; + const { data, options } = props; const { valueOptions } = options; - if (panelData.timeSeries) { + console.log('FIND VALUE', data); + + if (data) { + // For now, assume timeseries defaults + const xColumn = 1; // time + const yColumn = 0; // value const vmSeries = processTimeSeries({ - timeSeries: panelData.timeSeries, + data, + xColumn, + yColumn, nullValueMode: NullValueMode.Null, }); + console.log('GOT', vmSeries); + if (vmSeries[0]) { return vmSeries[0].stats[valueOptions.stat]; } - } else if (panelData.tableData) { - return panelData.tableData.rows[0].find(prop => prop > 0); } return null; } diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index f1fc2b43d51..c859b5f9cf3 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -16,13 +16,18 @@ interface Props extends PanelProps {} export class GraphPanel extends PureComponent { render() { - const { panelData, timeRange, width, height } = this.props; + const { data, timeRange, width, height } = this.props; const { showLines, showBars, showPoints } = this.props.options; let vmSeries: TimeSeriesVMs; - if (panelData.timeSeries) { + if (data) { + // For now, assume timeseries defaults + const xColumn = 1; // time + const yColumn = 0; // value vmSeries = processTimeSeries({ - timeSeries: panelData.timeSeries, + data, + xColumn, + yColumn, nullValueMode: NullValueMode.Ignore, }); } From 8bf57359ab745a0ed67bc876c26e753c24f492e0 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 12:47:21 -0800 Subject: [PATCH 02/11] don't require x & y columns for timeSeries --- .../grafana-ui/src/utils/processTimeSeries.ts | 19 +++++++++++++++++-- .../features/dashboard/dashgrid/DataPanel.tsx | 6 +----- public/app/plugins/panel/gauge/GaugePanel.tsx | 12 ------------ .../app/plugins/panel/graph2/GraphPanel.tsx | 5 ----- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index d8827e567e3..4b64ae3dd99 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -8,13 +8,28 @@ import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../typ interface Options { data: TableData[]; - xColumn: number; // Time - yColumn: number; // Value + xColumn?: number; // Time + yColumn?: number; // Value nullValueMode: NullValueMode; } export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = data.map((item, index) => { + if (!isNumber(xColumn)) { + xColumn = 1; // Default timeseries colum. TODO, find first time field! + } + if (!isNumber(yColumn)) { + yColumn = 0; // TODO, find first non-time field + } + + // TODO? either % or throw error? + if (xColumn >= item.columns.length) { + throw new Error('invalid colum: ' + xColumn); + } + if (yColumn >= item.columns.length) { + throw new Error('invalid colum: ' + yColumn); + } + const colorIndex = index % colors.length; const label = item.columns[yColumn].text; const result = []; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index c12462e2b49..1dd62c58e5c 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -145,12 +145,10 @@ export class DataPanel extends Component { onDataResponse(resp); } - const data = toTableData(resp.data); - console.log('Converted:', data); this.setState({ loading: LoadingState.Done, response: resp, - data, + data: toTableData(resp.data), isFirstLoad: false, }); } catch (err) { @@ -190,8 +188,6 @@ export class DataPanel extends Component { ); } - console.log('RENDER', data); - return ( <> {loading === LoadingState.Loading && this.renderLoadingState()} diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 7913aa33eb0..c4df1bb48ca 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -22,12 +22,9 @@ export class GaugePanel extends Component { this.state = { value: this.findValue(props), }; - console.log('CONSTRUCTOR!', this.props.data); } componentDidUpdate(prevProps: Props) { - console.log('UPDATE', this.props.data); - if (this.props.data !== prevProps.data) { this.setState({ value: this.findValue(this.props) }); } @@ -37,21 +34,12 @@ export class GaugePanel extends Component { const { data, options } = props; const { valueOptions } = options; - console.log('FIND VALUE', data); - if (data) { - // For now, assume timeseries defaults - const xColumn = 1; // time - const yColumn = 0; // value const vmSeries = processTimeSeries({ data, - xColumn, - yColumn, nullValueMode: NullValueMode.Null, }); - console.log('GOT', vmSeries); - if (vmSeries[0]) { return vmSeries[0].stats[valueOptions.stat]; } diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index c859b5f9cf3..f04e73e56fb 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -21,13 +21,8 @@ export class GraphPanel extends PureComponent { let vmSeries: TimeSeriesVMs; if (data) { - // For now, assume timeseries defaults - const xColumn = 1; // time - const yColumn = 0; // value vmSeries = processTimeSeries({ data, - xColumn, - yColumn, nullValueMode: NullValueMode.Ignore, }); } From 439b04420454f6cbf4a2bb769189002c919bf713 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 7 Mar 2019 13:09:53 -0800 Subject: [PATCH 03/11] move toTableData to grafana/ui --- .../grafana-ui/src/utils/processTimeSeries.ts | 41 +++++++++++++++++-- .../features/dashboard/dashgrid/DataPanel.tsx | 3 +- .../dashboard/dashgrid/PanelChrome.tsx | 6 +-- public/app/features/dashboard/utils/panel.ts | 39 +----------------- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 4b64ae3dd99..3b2d1bd05aa 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -4,12 +4,12 @@ import isNumber from 'lodash/isNumber'; import { colors } from './colors'; // Types -import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types'; +import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData, TimeSeries } from '../types'; interface Options { data: TableData[]; - xColumn?: number; // Time - yColumn?: number; // Value + xColumn?: number; // Time (or null to guess) + yColumn?: number; // Value (or null to guess) nullValueMode: NullValueMode; } @@ -190,3 +190,38 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } + +export const toTableData = (results: any[]): TableData[] => { + const tables: TableData[] = []; + if (results) { + for (let i = 0; i < results.length; i++) { + const data = results[i]; + if (data) { + if (data.hasOwnProperty('columns')) { + tables.push(data as TableData); + } else if (data.hasOwnProperty('datapoints')) { + const ts = data as TimeSeries; + tables.push({ + type: 'timeseries', + columns: [ + { + text: ts.target, + unit: ts.unit, + type: 'number', // Is this really true? + }, + { + text: 'time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData); + } else { + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + } + } + } + } + return tables; +}; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 1dd62c58e5c..872e2823553 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -14,10 +14,9 @@ import { TableData, TimeRange, ScopedVars, + toTableData, } from '@grafana/ui'; -import { toTableData } from '../utils/panel'; - interface RenderProps { loading: LoadingState; data: TableData[]; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 0dd82b7dad9..af58202dfa9 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -11,14 +11,14 @@ import { DataPanel } from './DataPanel'; import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; // Utils -import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; +import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui'; import variables from 'sass/_variables.generated.scss'; @@ -139,7 +139,7 @@ export class PanelChrome extends PureComponent { } get getDataForPanel() { - return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null; + return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null; } renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element { diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 3058e2b9db6..57f4b81a0e0 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { TableData, TimeRange, TimeSeries } from '@grafana/ui'; +import { TimeRange } from '@grafana/ui'; // Utils import { isString as _isString } from 'lodash'; @@ -169,40 +169,3 @@ export function getResolution(panel: PanelModel): number { return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24)); } - -const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints'); -const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const snapshotDataToPanelData = (panel: PanelModel): TableData[] => { - return toTableData(panel.snapshotData); -}; - -export const toTableData = (results: any[]): TableData[] => { - if (!results) { - return []; - } - return results.map(data => { - if (isTableData(data)) { - return data as TableData; - } - if (isTimeSeries(data)) { - const ts = data as TimeSeries; - return { - type: 'timeseries', - columns: [ - { - text: ts.target, - unit: ts.unit, - type: 'number', // Is this really true? - }, - { - text: 'time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData; - } - console.warn('Can not convert', data); - throw new Error('Unsupported data format'); - }); -}; From 188dc862465e3e4251e6b59efb1eed504ff778a5 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 00:41:35 -0800 Subject: [PATCH 04/11] remove type field and add helper functions to check if data isTableData --- packages/grafana-ui/src/types/data.ts | 1 - packages/grafana-ui/src/utils/processTimeSeries.ts | 6 +++--- public/app/plugins/panel/singlestat/module.ts | 4 ++-- public/app/plugins/panel/table/module.ts | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index 1ea89bcd28e..21b863049fc 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -63,5 +63,4 @@ export interface Column { export interface TableData { columns: Column[]; rows: any[]; - type: string; } diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 3b2d1bd05aa..e7582c9f13c 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -191,6 +191,8 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } +export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); + export const toTableData = (results: any[]): TableData[] => { const tables: TableData[] = []; if (results) { @@ -202,15 +204,13 @@ export const toTableData = (results: any[]): TableData[] => { } else if (data.hasOwnProperty('datapoints')) { const ts = data as TimeSeries; tables.push({ - type: 'timeseries', columns: [ { text: ts.target, unit: ts.unit, - type: 'number', // Is this really true? }, { - text: 'time', + text: 'Time', type: 'time', }, ], diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index bcf09297cf7..5b75de44949 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; +import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui'; class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { scopedVars: _.extend({}, this.panel.scopedVars), }; - if (dataList.length > 0 && dataList[0].type === 'table') { + if (dataList.length > 0 && isTableData(dataList[0])) { this.dataType = 'table'; const tableData = dataList.map(this.tableHandler.bind(this)); this.setTableValues(tableData, data); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 268f5aa7ac4..b7b3c0312a3 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers'; import { tablePanelEditor } from './editor'; import { columnOptionsTab } from './column_options'; import { TableRenderer } from './renderer'; +import { isTableData } from '@grafana/ui'; class TablePanelCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { // automatically correct transform mode based on data if (this.dataRaw && this.dataRaw.length) { - if (this.dataRaw[0].type === 'table') { + if (isTableData(this.dataRaw[0])) { this.panel.transform = 'table'; } else { if (this.dataRaw[0].type === 'docs') { From 229dff757c310438bb3706c96d0116974ca6515c Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 02:14:35 -0800 Subject: [PATCH 05/11] less nesting and add test --- .../src/utils/processTimeSeries.test.ts | 31 ++++++++++ .../grafana-ui/src/utils/processTimeSeries.ts | 60 +++++++++---------- 2 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 packages/grafana-ui/src/utils/processTimeSeries.test.ts diff --git a/packages/grafana-ui/src/utils/processTimeSeries.test.ts b/packages/grafana-ui/src/utils/processTimeSeries.test.ts new file mode 100644 index 00000000000..fe291898c55 --- /dev/null +++ b/packages/grafana-ui/src/utils/processTimeSeries.test.ts @@ -0,0 +1,31 @@ +import { toTableData } from './processTimeSeries'; + +describe('toTableData', () => { + it('converts timeseries to table skipping nulls', () => { + const input = { + target: 'Field Name', + datapoints: [[100, 1], [200, 2]], + }; + const data = toTableData([null, input, null, null]); + expect(data.length).toBe(1); + expect(data[0].columns[0].text).toBe(input.target); + expect(data[0].rows).toBe(input.datapoints); + }); + + it('keeps tableData unchanged', () => { + const input = { + columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }], + rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]], + }; + const data = toTableData([null, input, null, null]); + expect(data.length).toBe(1); + expect(data[0]).toBe(input); + }); + + it('supports null values OK', () => { + expect(toTableData([null, null, null, null])).toEqual([]); + expect(toTableData(undefined)).toEqual([]); + expect(toTableData((null as unknown) as any[])).toEqual([]); + expect(toTableData([])).toEqual([]); + }); +}); diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index e7582c9f13c..112bd67f481 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -193,35 +193,35 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); -export const toTableData = (results: any[]): TableData[] => { - const tables: TableData[] = []; - if (results) { - for (let i = 0; i < results.length; i++) { - const data = results[i]; - if (data) { - if (data.hasOwnProperty('columns')) { - tables.push(data as TableData); - } else if (data.hasOwnProperty('datapoints')) { - const ts = data as TimeSeries; - tables.push({ - columns: [ - { - text: ts.target, - unit: ts.unit, - }, - { - text: 'Time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData); - } else { - console.warn('Can not convert', data); - throw new Error('Unsupported data format'); - } - } - } +export const toTableData = (results?: any[]): TableData[] => { + if (!results) { + return []; } - return tables; + + return results + .filter(d => !!d) + .map(data => { + if (data.hasOwnProperty('columns')) { + return data as TableData; + } + if (data.hasOwnProperty('datapoints')) { + const ts = data as TimeSeries; + return { + columns: [ + { + text: ts.target || 'Timeseries', + unit: ts.unit, + }, + { + text: 'Time', + type: 'time', + }, + ], + rows: ts.datapoints, + } as TableData; + } + // TODO, try to convert JSON to table? + console.warn('Can not convert', data); + throw new Error('Unsupported data format'); + }); }; From 7ce7da1251d89eaef687a8c617d0b742a3e75b71 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 8 Mar 2019 08:27:48 -0800 Subject: [PATCH 06/11] merge master --- .../src/utils/processTimeSeries.test.ts | 18 +++++++++++++----- .../grafana-ui/src/utils/processTimeSeries.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.test.ts b/packages/grafana-ui/src/utils/processTimeSeries.test.ts index fe291898c55..bf88a43e7fb 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.test.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.test.ts @@ -2,14 +2,22 @@ import { toTableData } from './processTimeSeries'; describe('toTableData', () => { it('converts timeseries to table skipping nulls', () => { - const input = { + const input1 = { target: 'Field Name', datapoints: [[100, 1], [200, 2]], }; - const data = toTableData([null, input, null, null]); - expect(data.length).toBe(1); - expect(data[0].columns[0].text).toBe(input.target); - expect(data[0].rows).toBe(input.datapoints); + const input2 = { + // without target + target: '', + datapoints: [[100, 1], [200, 2]], + }; + const data = toTableData([null, input1, input2, null, null]); + expect(data.length).toBe(2); + expect(data[0].columns[0].text).toBe(input1.target); + expect(data[0].rows).toBe(input1.datapoints); + + // Default name + expect(data[1].columns[0].text).toEqual('Value'); }); it('keeps tableData unchanged', () => { diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 112bd67f481..85457a718ab 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -209,7 +209,7 @@ export const toTableData = (results?: any[]): TableData[] => { return { columns: [ { - text: ts.target || 'Timeseries', + text: ts.target || 'Value', unit: ts.unit, }, { From 84fa9be29f33b89fa7ddd7d72a4c76f73b67b999 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 10 Mar 2019 15:51:19 -0700 Subject: [PATCH 07/11] add comment --- packages/grafana-ui/src/utils/processTimeSeries.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index 85457a718ab..a56fe004b05 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -13,6 +13,8 @@ interface Options { nullValueMode: NullValueMode; } +// NOTE -- this should be refactored into a TableData utility file. +// I left it as is so the merge changes are more clear. export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs { const vmSeries = data.map((item, index) => { if (!isNumber(xColumn)) { From 455a33bd8eaaf9e26192861040309e5e986f58a9 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 09:24:25 -0700 Subject: [PATCH 08/11] cleanup after review --- .../grafana-ui/src/utils/processTimeSeries.ts | 31 ++++++++++--------- public/app/core/table_model.ts | 9 ++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTimeSeries.ts b/packages/grafana-ui/src/utils/processTimeSeries.ts index a56fe004b05..8202d2f6187 100644 --- a/packages/grafana-ui/src/utils/processTimeSeries.ts +++ b/packages/grafana-ui/src/utils/processTimeSeries.ts @@ -193,6 +193,22 @@ export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Opt return vmSeries; } +function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData { + return { + columns: [ + { + text: timeSeries.target || 'Value', + unit: timeSeries.unit, + }, + { + text: 'Time', + type: 'time', + }, + ], + rows: timeSeries.datapoints, + }; +} + export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); export const toTableData = (results?: any[]): TableData[] => { @@ -207,20 +223,7 @@ export const toTableData = (results?: any[]): TableData[] => { return data as TableData; } if (data.hasOwnProperty('datapoints')) { - const ts = data as TimeSeries; - return { - columns: [ - { - text: ts.target || 'Value', - unit: ts.unit, - }, - { - text: 'Time', - type: 'time', - }, - ], - rows: ts.datapoints, - } as TableData; + return convertTimeSeriesToTableData(data); } // TODO, try to convert JSON to table? console.warn('Can not convert', data); diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 988c3b1992e..3e8389e4be8 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,15 +1,18 @@ import _ from 'lodash'; import { Column, TableData } from '@grafana/ui'; -// This class mutates and uses the extra column fields -interface ColumnEX extends Column { +/** + * Extends the standard Column class with variables that get + * mutated in the angular table panel. + */ +interface AngularTableColumn extends Column { title?: string; sort?: boolean; desc?: boolean; } export default class TableModel implements TableData { - columns: ColumnEX[]; + columns: AngularTableColumn[]; rows: any[]; type: string; columnMap: any; From 00942ec882d55c8bc34a1fb559e2d1239c4b089a Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 11 Mar 2019 09:29:30 -0700 Subject: [PATCH 09/11] MutableColumn --- public/app/core/table_model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 3e8389e4be8..291689941f7 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -5,14 +5,14 @@ import { Column, TableData } from '@grafana/ui'; * Extends the standard Column class with variables that get * mutated in the angular table panel. */ -interface AngularTableColumn extends Column { +interface MutableColumn extends Column { title?: string; sort?: boolean; desc?: boolean; } export default class TableModel implements TableData { - columns: AngularTableColumn[]; + columns: MutableColumn[]; rows: any[]; type: string; columnMap: any; From 011b2cdf094f4ae21897caf2cd5d5906e4b34d8d Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 13 Mar 2019 09:10:21 -0700 Subject: [PATCH 10/11] merge master --- packages/grafana-ui/src/utils/processTableData.ts | 2 +- packages/grafana-ui/src/utils/singlestat.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/utils/processTableData.ts b/packages/grafana-ui/src/utils/processTableData.ts index d8c6a047a59..9f81635da20 100644 --- a/packages/grafana-ui/src/utils/processTableData.ts +++ b/packages/grafana-ui/src/utils/processTableData.ts @@ -1,6 +1,6 @@ // Libraries -import Papa, { ParseError, ParseMeta } from 'papaparse'; import isNumber from 'lodash/isNumber'; +import Papa, { ParseError, ParseMeta } from 'papaparse'; // Types import { TableData, Column, TimeSeries } from '../types'; diff --git a/packages/grafana-ui/src/utils/singlestat.ts b/packages/grafana-ui/src/utils/singlestat.ts index c37c5550e22..e4f1e6ec294 100644 --- a/packages/grafana-ui/src/utils/singlestat.ts +++ b/packages/grafana-ui/src/utils/singlestat.ts @@ -15,7 +15,7 @@ export function processSingleStatPanelData(options: SingleStatProcessingOptions) const timeSeries = processTimeSeries({ data, xColumn: 0, - yColumn: 0, + yColumn: 1, nullValueMode: NullValueMode.Null, }); From 4600aac7d5240d09a9e0ea108eb47535de9ad25c Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 13 Mar 2019 22:31:54 -0700 Subject: [PATCH 11/11] set the unit on time data --- packages/grafana-ui/src/types/data.ts | 6 +++--- packages/grafana-ui/src/utils/processTableData.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index d8d67809ed8..992e11690d5 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -51,9 +51,9 @@ export enum NullValueMode { export type TimeSeriesVMs = TimeSeriesVM[]; export interface Column { - text: string; // name - type?: 'time' | 'number' | 'string' | 'object'; - filterable?: boolean; + text: string; // The column name + type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove? + filterable?: boolean; // currently only set by elasticsearch, and used in the table panel unit?: string; } diff --git a/packages/grafana-ui/src/utils/processTableData.ts b/packages/grafana-ui/src/utils/processTableData.ts index 9f81635da20..d4a91efd622 100644 --- a/packages/grafana-ui/src/utils/processTableData.ts +++ b/packages/grafana-ui/src/utils/processTableData.ts @@ -139,6 +139,7 @@ function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData { { text: 'Time', type: 'time', + unit: 'dateTimeAsIso', }, ], rows: timeSeries.datapoints,