Merge pull request #15864 from ryantxu/all-data-as-table

Use TableData for all data in react
This commit is contained in:
Torkel Ödegaard 2019-03-20 09:15:06 +01:00 committed by GitHub
commit abd8948478
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 165 additions and 158 deletions

View File

@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}),
type: 'table',
columnMap: {},
};
}

View File

@ -51,27 +51,13 @@ export enum NullValueMode {
export type TimeSeriesVMs = TimeSeriesVM[];
export interface Column {
text: string;
title?: string;
type?: string;
sort?: boolean;
desc?: boolean;
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;
}
export interface TableData {
columns: Column[];
rows: any[];
type: string;
columnMap: any;
}
export type SingleStatValue = number | string | null;
/*
* So we can add meta info like tags & series name
*/
export interface SingleStatValueInfo {
value: SingleStatValue;
}

View File

@ -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<T = any> {
panelData: PanelData;
data?: TableData[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
replaceVariables: InterpolateFunction;
}
export interface PanelData {
timeSeries?: TimeSeries[];
tableData?: TableData;
}
export interface PanelEditorProps<T = any> {
options: T;
onOptionsChange: (options: T) => void;

View File

@ -2,7 +2,6 @@
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "Column 1",
@ -31,13 +30,11 @@ Object {
null,
],
],
"type": "table",
}
`;
exports[`processTableData basic processing should read header and two rows 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "a",
@ -61,6 +58,5 @@ Object {
6,
],
],
"type": "table",
}
`;

View File

@ -1,5 +1,5 @@
export * from './processTimeSeries';
export * from './singlestat';
export * from './processTableData';
export * from './valueFormats/valueFormats';
export * from './colors';
export * from './namedColorsPalette';

View File

@ -1,4 +1,4 @@
import { parseCSV } from './processTableData';
import { parseCSV, toTableData } from './processTableData';
describe('processTableData', () => {
describe('basic processing', () => {
@ -18,3 +18,41 @@ describe('processTableData', () => {
});
});
});
describe('toTableData', () => {
it('converts timeseries to table skipping nulls', () => {
const input1 = {
target: 'Field Name',
datapoints: [[100, 1], [200, 2]],
};
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', () => {
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([]);
});
});

View File

@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import Papa, { ParseError, ParseMeta } from 'papaparse';
// Types
import { TableData, Column } from '../types';
import { TableData, Column, TimeSeries } from '../types';
// Subset of all parse options
export interface TableParseOptions {
@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
return {
columns,
rows: fixedRows,
type: table.type,
columnMap: table.columnMap,
};
}
@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return {
columns: [],
rows: [],
type: 'table',
columnMap: {},
};
}
@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return matchRowSizes({
columns: makeColumns(header),
rows: results.data,
type: 'table',
columnMap: {},
});
}
function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
return {
columns: [
{
text: timeSeries.target || 'Value',
unit: timeSeries.unit,
},
{
text: 'Time',
type: 'time',
unit: 'dateTimeAsIso',
},
],
rows: timeSeries.datapoints,
};
}
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
export const toTableData = (results?: any[]): TableData[] => {
if (!results) {
return [];
}
return results
.filter(d => !!d)
.map(data => {
if (data.hasOwnProperty('columns')) {
return data as TableData;
}
if (data.hasOwnProperty('datapoints')) {
return convertTimeSeriesToTableData(data);
}
// TODO, try to convert JSON to table?
console.warn('Can not convert', data);
throw new Error('Unsupported data format');
});
};
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
if (isNumber(sortIndex)) {
const copy = {

View File

@ -4,17 +4,36 @@ 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 (or null to guess)
yColumn?: number; // Value (or null to guess)
nullValueMode: NullValueMode;
}
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
const vmSeries = timeSeries.map((item, index) => {
// NOTE: this should move to processTableData.ts
// 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)) {
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.target;
const label = item.columns[yColumn].text;
const result = [];
// stat defaults
@ -42,9 +61,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 +114,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;
}

View File

@ -1,33 +0,0 @@
import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
import { processTimeSeries } from './processTimeSeries';
export interface SingleStatProcessingOptions {
panelData: PanelData;
stat: string;
}
//
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
//
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
const { panelData, stat } = options;
if (panelData.timeSeries) {
const timeSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
nullValueMode: NullValueMode.Null,
});
return timeSeries.map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return {
value: value,
};
});
} else if (panelData.tableData) {
throw { message: 'Panel data not supported' };
}
return [];
}

View File

@ -1,17 +1,18 @@
import _ from 'lodash';
import { Column, TableData } from '@grafana/ui';
interface Column {
text: string;
/**
* Extends the standard Column class with variables that get
* mutated in the angular table panel.
*/
interface MutableColumn 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: MutableColumn[];
rows: any[];
type: string;
columnMap: any;

View File

@ -11,16 +11,15 @@ import {
DataQueryResponse,
DataQueryError,
LoadingState,
PanelData,
TableData,
TimeRange,
TimeSeries,
ScopedVars,
toTableData,
} from '@grafana/ui';
interface RenderProps {
loading: LoadingState;
panelData: PanelData;
data: TableData[];
}
export interface Props {
@ -44,7 +43,7 @@ export interface State {
isFirstLoad: boolean;
loading: LoadingState;
response: DataQueryResponse;
panelData: PanelData;
data?: TableData[];
}
export class DataPanel extends Component<Props, State> {
@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
response: {
data: [],
},
panelData: {},
isFirstLoad: true,
};
}
@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
this.setState({
loading: LoadingState.Done,
response: resp,
panelData: this.getPanelData(resp),
data: toTableData(resp.data),
isFirstLoad: false,
});
} catch (err) {
@ -172,23 +170,9 @@ export class DataPanel extends Component<Props, State> {
}
};
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)) {
@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })}
{this.props.children({ loading, data })}
</>
);
}

View File

@ -11,7 +11,7 @@ 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';
import config from 'app/core/config';
@ -19,7 +19,7 @@ import config from 'app/core/config';
// 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, toTableData } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv';
@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
}
get getDataForPanel() {
return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : 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<Props, State> {
<div className="panel-content">
<PanelComponent
loading={loading}
panelData={panelData}
data={data}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * config.theme.panelPadding.horizontal}
@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
onDataResponse={this.onDataResponse}
onError={this.onDataError}
>
{({ loading, panelData }) => {
return this.renderPanelPlugin(loading, panelData, width, height);
{({ loading, data }) => {
return this.renderPanelPlugin(loading, data, width, height);
}}
</DataPanel>
) : (

View File

@ -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 { TimeRange } from '@grafana/ui';
// Utils
import { isString as _isString } from 'lodash';
@ -170,19 +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): 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;
}
throw new Error('snapshotData is invalid:' + snapshotData.toString());
};

View File

@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

View File

@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

View File

@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
export class GraphPanel extends PureComponent<Props> {
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) {
vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
data,
nullValueMode: NullValueMode.Ignore,
});
}

View File

@ -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);

View File

@ -4,27 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
// Types
import { SingleStatOptions, SingleStatBaseOptions } from './types';
import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
import { config } from 'app/core/config';
import { getDisplayProcessor } from '@grafana/ui';
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
const { panelData, replaceVariables, options } = props;
const { data, replaceVariables, options } = props;
const { valueOptions, valueMappings } = options;
const { unit, decimals, stat } = valueOptions;
const processor = getDisplayProcessor({
unit: valueOptions.unit,
decimals: valueOptions.decimals,
unit,
decimals,
mappings: valueMappings,
thresholds: options.thresholds,
prefix: replaceVariables(valueOptions.prefix),
suffix: replaceVariables(valueOptions.suffix),
theme: config.theme,
});
return processSingleStatPanelData({
panelData: panelData,
stat: valueOptions.stat,
}).map(stat => processor(stat.value));
return processTimeSeries({
data,
nullValueMode: NullValueMode.Null,
}).map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return processor(value);
});
};
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
@ -49,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

View File

@ -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') {

View File

@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
}
render() {
const { panelData, options } = this.props;
const { data, options } = this.props;
if (!panelData || !panelData.tableData) {
if (data.length < 1) {
return <div>No Table Data...</div>;
}
return (
<ThemeContext.Consumer>
{theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
{theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
</ThemeContext.Consumer>
);
}