cleanup and guess all columns
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { parseCSV, toTableData } from './processTableData';
|
import { parseCSV, toTableData, guessColumnTypes, guessColumnTypeFromValue } from './processTableData';
|
||||||
|
import { ColumnType } from '../types/data';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
describe('processTableData', () => {
|
describe('processTableData', () => {
|
||||||
describe('basic processing', () => {
|
describe('basic processing', () => {
|
||||||
@@ -20,23 +22,23 @@ describe('processTableData', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('toTableData', () => {
|
describe('toTableData', () => {
|
||||||
it('converts timeseries to table skipping nulls', () => {
|
it('converts timeseries to table ', () => {
|
||||||
const input1 = {
|
const input1 = {
|
||||||
target: 'Field Name',
|
target: 'Field Name',
|
||||||
datapoints: [[100, 1], [200, 2]],
|
datapoints: [[100, 1], [200, 2]],
|
||||||
};
|
};
|
||||||
|
let table = toTableData(input1);
|
||||||
|
expect(table.columns[0].text).toBe(input1.target);
|
||||||
|
expect(table.rows).toBe(input1.datapoints);
|
||||||
|
|
||||||
|
// Should fill a default name if target is empty
|
||||||
const input2 = {
|
const input2 = {
|
||||||
// without target
|
// without target
|
||||||
target: '',
|
target: '',
|
||||||
datapoints: [[100, 1], [200, 2]],
|
datapoints: [[100, 1], [200, 2]],
|
||||||
};
|
};
|
||||||
const data = toTableData([null, input1, input2, null, null]);
|
table = toTableData(input2);
|
||||||
expect(data.length).toBe(2);
|
expect(table.columns[0].text).toEqual('Value');
|
||||||
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', () => {
|
it('keeps tableData unchanged', () => {
|
||||||
@@ -44,15 +46,42 @@ describe('toTableData', () => {
|
|||||||
columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
||||||
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
|
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
|
||||||
};
|
};
|
||||||
const data = toTableData([null, input, null, null]);
|
const table = toTableData(input);
|
||||||
expect(data.length).toBe(1);
|
expect(table).toBe(input);
|
||||||
expect(data[0]).toBe(input);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports null values OK', () => {
|
it('Guess Colum Types from value', () => {
|
||||||
expect(toTableData([null, null, null, null])).toEqual([]);
|
expect(guessColumnTypeFromValue(1)).toBe(ColumnType.number);
|
||||||
expect(toTableData(undefined)).toEqual([]);
|
expect(guessColumnTypeFromValue(1.234)).toBe(ColumnType.number);
|
||||||
expect(toTableData((null as unknown) as any[])).toEqual([]);
|
expect(guessColumnTypeFromValue(3.125e7)).toBe(ColumnType.number);
|
||||||
expect(toTableData([])).toEqual([]);
|
expect(guessColumnTypeFromValue('1')).toBe(ColumnType.string);
|
||||||
|
expect(guessColumnTypeFromValue('1.234')).toBe(ColumnType.string);
|
||||||
|
expect(guessColumnTypeFromValue('3.125e7')).toBe(ColumnType.string);
|
||||||
|
expect(guessColumnTypeFromValue(true)).toBe(ColumnType.boolean);
|
||||||
|
expect(guessColumnTypeFromValue(false)).toBe(ColumnType.boolean);
|
||||||
|
expect(guessColumnTypeFromValue(new Date())).toBe(ColumnType.time);
|
||||||
|
expect(guessColumnTypeFromValue(moment())).toBe(ColumnType.time);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Guess Colum Types from strings', () => {
|
||||||
|
expect(guessColumnTypeFromValue('1', true)).toBe(ColumnType.number);
|
||||||
|
expect(guessColumnTypeFromValue('1.234', true)).toBe(ColumnType.number);
|
||||||
|
expect(guessColumnTypeFromValue('3.125e7', true)).toBe(ColumnType.number);
|
||||||
|
expect(guessColumnTypeFromValue('True', true)).toBe(ColumnType.boolean);
|
||||||
|
expect(guessColumnTypeFromValue('FALSE', true)).toBe(ColumnType.boolean);
|
||||||
|
expect(guessColumnTypeFromValue('true', true)).toBe(ColumnType.boolean);
|
||||||
|
expect(guessColumnTypeFromValue('xxxx', true)).toBe(ColumnType.string);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Guess Colum Types from table', () => {
|
||||||
|
const table = {
|
||||||
|
columns: [{ text: 'A (number)' }, { text: 'B (strings)' }, { text: 'C (nulls)' }, { text: 'Time' }],
|
||||||
|
rows: [[123, null, null, '2000'], [null, '123', null, 'XXX']],
|
||||||
|
};
|
||||||
|
const norm = guessColumnTypes(table);
|
||||||
|
expect(norm.columns[0].type).toBe(ColumnType.number);
|
||||||
|
expect(norm.columns[1].type).toBe(ColumnType.string);
|
||||||
|
expect(norm.columns[2].type).toBeUndefined();
|
||||||
|
expect(norm.columns[3].type).toBe(ColumnType.time); // based on name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
import isString from 'lodash/isString';
|
import isString from 'lodash/isString';
|
||||||
|
import isBoolean from 'lodash/isBoolean';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||||
|
|
||||||
@@ -159,77 +161,112 @@ export const getFirstTimeColumn = (table: TableData): number => {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PapaParse Dynamic Typing regex:
|
||||||
|
// https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
|
||||||
|
const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a table Returns a copy of the table with the best guess for each column type
|
* Given a value this will guess the best column type
|
||||||
|
*
|
||||||
|
* TODO: better Date/Time support! Look for standard date strings?
|
||||||
*/
|
*/
|
||||||
export const guessColumnTypes = (table: TableData): TableData => {
|
export function guessColumnTypeFromValue(v: any, parseString?: boolean): ColumnType {
|
||||||
let changed = false;
|
if (isNumber(v)) {
|
||||||
const columns = table.columns.map((column, index) => {
|
return ColumnType.number;
|
||||||
if (!column.type) {
|
}
|
||||||
// 1. Use the column name to guess
|
|
||||||
if (column.text) {
|
if (isString(v)) {
|
||||||
const name = column.text.toLowerCase();
|
if (parseString) {
|
||||||
if (name === 'date' || name === 'time') {
|
const c0 = v[0].toLowerCase();
|
||||||
changed = true;
|
if (c0 === 't' || c0 === 'f') {
|
||||||
return {
|
if (v === 'true' || v === 'TRUE' || v === 'True' || v === 'false' || v === 'FALSE' || v === 'False') {
|
||||||
...column,
|
return ColumnType.boolean;
|
||||||
type: ColumnType.time,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check the first non-null value
|
if (NUMBER.test(v)) {
|
||||||
for (let i = 0; i < table.rows.length; i++) {
|
return ColumnType.number;
|
||||||
const v = table.rows[i][index];
|
|
||||||
if (v !== null) {
|
|
||||||
let type: ColumnType | undefined;
|
|
||||||
if (isNumber(v)) {
|
|
||||||
type = ColumnType.number;
|
|
||||||
} else if (isString(v)) {
|
|
||||||
type = ColumnType.string;
|
|
||||||
}
|
|
||||||
if (type) {
|
|
||||||
changed = true;
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return column;
|
return ColumnType.string;
|
||||||
});
|
|
||||||
if (changed) {
|
|
||||||
return {
|
|
||||||
...table,
|
|
||||||
columns,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBoolean(v)) {
|
||||||
|
return ColumnType.boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof Date || v instanceof moment) {
|
||||||
|
return ColumnType.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ColumnType.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks at the data to guess the column type. This ignores any existing setting
|
||||||
|
*/
|
||||||
|
function guessColumnTypeFromTable(table: TableData, index: number, parseString?: boolean): ColumnType | undefined {
|
||||||
|
const column = table.columns[index];
|
||||||
|
|
||||||
|
// 1. Use the column name to guess
|
||||||
|
if (column.text) {
|
||||||
|
const name = column.text.toLowerCase();
|
||||||
|
if (name === 'date' || name === 'time') {
|
||||||
|
return ColumnType.time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check the first non-null value
|
||||||
|
for (let i = 0; i < table.rows.length; i++) {
|
||||||
|
const v = table.rows[i][index];
|
||||||
|
if (v !== null) {
|
||||||
|
return guessColumnTypeFromValue(v, parseString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not find anything
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a table Returns a copy of the table with the best guess for each column type
|
||||||
|
* If the table already has column types defined, they will be used
|
||||||
|
*/
|
||||||
|
export const guessColumnTypes = (table: TableData): TableData => {
|
||||||
|
for (let i = 0; i < table.columns.length; i++) {
|
||||||
|
if (!table.columns[i].type) {
|
||||||
|
// Somethign is missing a type return a modified copy
|
||||||
|
return {
|
||||||
|
...table,
|
||||||
|
columns: table.columns.map((column, index) => {
|
||||||
|
if (column.type) {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
// Replace it with a calculated version
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
type: guessColumnTypeFromTable(table, index),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No changes necessary
|
||||||
return table;
|
return table;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
|
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
|
||||||
|
|
||||||
export const toTableData = (results?: any[]): TableData[] => {
|
export const toTableData = (data: any): TableData => {
|
||||||
if (!results) {
|
if (data.hasOwnProperty('columns')) {
|
||||||
return [];
|
return data as TableData;
|
||||||
}
|
}
|
||||||
|
if (data.hasOwnProperty('datapoints')) {
|
||||||
return results
|
return convertTimeSeriesToTableData(data);
|
||||||
.filter(d => !!d)
|
}
|
||||||
.map(data => {
|
// TODO, try to convert JSON/Array to table?
|
||||||
if (data.hasOwnProperty('columns')) {
|
console.warn('Can not convert', data);
|
||||||
return data as TableData;
|
throw new Error('Unsupported data format');
|
||||||
}
|
|
||||||
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 {
|
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Library
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DataPanel, getProcessedTableData } from './DataPanel';
|
||||||
|
|
||||||
|
describe('DataPanel', () => {
|
||||||
|
let dataPanel: DataPanel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dataPanel = new DataPanel({
|
||||||
|
queries: [],
|
||||||
|
panelId: 1,
|
||||||
|
widthPixels: 100,
|
||||||
|
refreshCounter: 1,
|
||||||
|
datasource: 'xxx',
|
||||||
|
children: r => {
|
||||||
|
return <div>hello</div>;
|
||||||
|
},
|
||||||
|
onError: (message, error) => {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts with unloaded state', () => {
|
||||||
|
expect(dataPanel.state.isFirstLoad).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = getProcessedTableData([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');
|
||||||
|
|
||||||
|
// Every colun should have a name and a type
|
||||||
|
for (const table of data) {
|
||||||
|
for (const column of table.columns) {
|
||||||
|
expect(column.text).toBeDefined();
|
||||||
|
expect(column.type).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports null values from query OK', () => {
|
||||||
|
expect(getProcessedTableData([null, null, null, null])).toEqual([]);
|
||||||
|
expect(getProcessedTableData(undefined)).toEqual([]);
|
||||||
|
expect(getProcessedTableData((null as unknown) as any[])).toEqual([]);
|
||||||
|
expect(getProcessedTableData([])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
toTableData,
|
toTableData,
|
||||||
|
guessColumnTypes,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
interface RenderProps {
|
interface RenderProps {
|
||||||
@@ -46,6 +47,25 @@ export interface State {
|
|||||||
data?: TableData[];
|
data?: TableData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All panels will be passed tables that have our best guess at colum type set
|
||||||
|
*
|
||||||
|
* This is also used by PanelChrome for snapshot support
|
||||||
|
*/
|
||||||
|
export function getProcessedTableData(results?: any[]): TableData[] {
|
||||||
|
if (!results) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables: TableData[] = [];
|
||||||
|
for (const r of results) {
|
||||||
|
if (r) {
|
||||||
|
tables.push(guessColumnTypes(toTableData(r)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
export class DataPanel extends Component<Props, State> {
|
export class DataPanel extends Component<Props, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
@@ -147,7 +167,7 @@ export class DataPanel extends Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
loading: LoadingState.Done,
|
loading: LoadingState.Done,
|
||||||
response: resp,
|
response: resp,
|
||||||
data: toTableData(resp.data),
|
data: getProcessedTableData(resp.data),
|
||||||
isFirstLoad: false,
|
isFirstLoad: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ import config from 'app/core/config';
|
|||||||
// Types
|
// Types
|
||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { PanelPlugin } from 'app/types';
|
import { PanelPlugin } from 'app/types';
|
||||||
import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
|
import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError } from '@grafana/ui';
|
||||||
import { ScopedVars } from '@grafana/ui';
|
import { ScopedVars } from '@grafana/ui';
|
||||||
|
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
|
import { getProcessedTableData } from './DataPanel';
|
||||||
|
|
||||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -139,7 +141,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get getDataForPanel() {
|
get getDataForPanel() {
|
||||||
return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
|
return this.hasPanelSnapshot ? getProcessedTableData(this.props.panel.snapshotData) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
|
renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
colors,
|
colors,
|
||||||
TimeSeriesVMs,
|
TimeSeriesVMs,
|
||||||
ColumnType,
|
ColumnType,
|
||||||
guessColumnTypes,
|
|
||||||
getFirstTimeColumn,
|
getFirstTimeColumn,
|
||||||
processTimeSeries,
|
processTimeSeries,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
@@ -23,8 +22,7 @@ export class GraphPanel extends PureComponent<Props> {
|
|||||||
const { showLines, showBars, showPoints } = this.props.options;
|
const { showLines, showBars, showPoints } = this.props.options;
|
||||||
|
|
||||||
const vmSeries: TimeSeriesVMs = [];
|
const vmSeries: TimeSeriesVMs = [];
|
||||||
for (let t = 0; t < data.length; t++) {
|
for (const table of data) {
|
||||||
const table = guessColumnTypes(data[t]);
|
|
||||||
const timeColumn = getFirstTimeColumn(table);
|
const timeColumn = getFirstTimeColumn(table);
|
||||||
if (timeColumn >= 0) {
|
if (timeColumn >= 0) {
|
||||||
for (let i = 0; i < table.columns.length; i++) {
|
for (let i = 0; i < table.columns.length; i++) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { PureComponent, CSSProperties } from 'react';
|
|||||||
// Types
|
// Types
|
||||||
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
import { SingleStatOptions, SingleStatBaseOptions } from './types';
|
||||||
|
|
||||||
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode, guessColumnTypes, ColumnType } from '@grafana/ui';
|
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode, ColumnType } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { getDisplayProcessor } from '@grafana/ui';
|
import { getDisplayProcessor } from '@grafana/ui';
|
||||||
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
|
||||||
@@ -25,8 +25,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
|
|||||||
});
|
});
|
||||||
|
|
||||||
const values: DisplayValue[] = [];
|
const values: DisplayValue[] = [];
|
||||||
for (let t = 0; t < data.length; t++) {
|
for (const table of data) {
|
||||||
const table = guessColumnTypes(data[t]);
|
|
||||||
for (let i = 0; i < table.columns.length; i++) {
|
for (let i = 0; i < table.columns.length; i++) {
|
||||||
const column = table.columns[i];
|
const column = table.columns[i];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user