mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataFrame: convert from row based to a columnar value format (#18391)
This commit is contained in:
@@ -1,6 +1,3 @@
|
|||||||
import { Threshold } from './threshold';
|
|
||||||
import { ValueMapping } from './valueMapping';
|
|
||||||
|
|
||||||
export enum LoadingState {
|
export enum LoadingState {
|
||||||
NotStarted = 'NotStarted',
|
NotStarted = 'NotStarted',
|
||||||
Loading = 'Loading',
|
Loading = 'Loading',
|
||||||
@@ -9,14 +6,6 @@ export enum LoadingState {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FieldType {
|
|
||||||
time = 'time', // or date
|
|
||||||
number = 'number',
|
|
||||||
string = 'string',
|
|
||||||
boolean = 'boolean',
|
|
||||||
other = 'other', // Object, Array, etc
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryResultMeta {
|
export interface QueryResultMeta {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
||||||
@@ -42,34 +31,10 @@ export interface QueryResultBase {
|
|||||||
meta?: QueryResultMeta;
|
meta?: QueryResultMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Field {
|
|
||||||
name: string; // The column name
|
|
||||||
title?: string; // The display value for this field. This supports template variables blank is auto
|
|
||||||
type?: FieldType;
|
|
||||||
filterable?: boolean;
|
|
||||||
unit?: string;
|
|
||||||
decimals?: number | null; // Significant digits (for display)
|
|
||||||
min?: number | null;
|
|
||||||
max?: number | null;
|
|
||||||
|
|
||||||
// Convert input values into a display value
|
|
||||||
mappings?: ValueMapping[];
|
|
||||||
|
|
||||||
// Must be sorted by 'value', first value is always -Infinity
|
|
||||||
thresholds?: Threshold[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Labels {
|
export interface Labels {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataFrame extends QueryResultBase {
|
|
||||||
name?: string;
|
|
||||||
fields: Field[];
|
|
||||||
rows: any[][];
|
|
||||||
labels?: Labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Column {
|
export interface Column {
|
||||||
text: string; // For a Column, the 'text' is the field name
|
text: string; // For a Column, the 'text' is the field name
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
|
|||||||
110
packages/grafana-data/src/types/dataFrame.ts
Normal file
110
packages/grafana-data/src/types/dataFrame.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Threshold } from './threshold';
|
||||||
|
import { ValueMapping } from './valueMapping';
|
||||||
|
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||||
|
import { FieldCalcs } from '../utils/index';
|
||||||
|
import { DisplayProcessor } from './displayValue';
|
||||||
|
|
||||||
|
export enum FieldType {
|
||||||
|
time = 'time', // or date
|
||||||
|
number = 'number',
|
||||||
|
string = 'string',
|
||||||
|
boolean = 'boolean',
|
||||||
|
other = 'other', // Object, Array, etc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every property is optional
|
||||||
|
*
|
||||||
|
* Plugins may extend this with additional properties. Somethign like series overrides
|
||||||
|
*/
|
||||||
|
export interface FieldConfig {
|
||||||
|
title?: string; // The display value for this field. This supports template variables blank is auto
|
||||||
|
filterable?: boolean;
|
||||||
|
|
||||||
|
// Numeric Options
|
||||||
|
unit?: string;
|
||||||
|
decimals?: number | null; // Significant digits (for display)
|
||||||
|
min?: number | null;
|
||||||
|
max?: number | null;
|
||||||
|
|
||||||
|
// Convert input values into a display string
|
||||||
|
mappings?: ValueMapping[];
|
||||||
|
|
||||||
|
// Must be sorted by 'value', first value is always -Infinity
|
||||||
|
thresholds?: Threshold[];
|
||||||
|
|
||||||
|
// Used when reducing field values
|
||||||
|
nullValueMode?: NullValueMode;
|
||||||
|
|
||||||
|
// Alternative to empty string
|
||||||
|
noValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vector<T = any> {
|
||||||
|
length: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the value by index (Like an array)
|
||||||
|
*/
|
||||||
|
get(index: number): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resutls as an array.
|
||||||
|
*/
|
||||||
|
toArray(): T[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the values as a simple array for json serialization
|
||||||
|
*/
|
||||||
|
toJSON(): any; // same results as toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Field<T = any> {
|
||||||
|
name: string; // The column name
|
||||||
|
type: FieldType;
|
||||||
|
config: FieldConfig;
|
||||||
|
values: Vector<T>; // `buffer` when JSON
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of reduced values
|
||||||
|
*/
|
||||||
|
calcs?: FieldCalcs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert text to the field value
|
||||||
|
*/
|
||||||
|
parse?: (value: any) => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value for display
|
||||||
|
*/
|
||||||
|
display?: DisplayProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataFrame extends QueryResultBase {
|
||||||
|
name?: string;
|
||||||
|
fields: Field[]; // All fields of equal length
|
||||||
|
labels?: Labels;
|
||||||
|
|
||||||
|
// The number of rows
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like a field, but properties are optional and values may be a simple array
|
||||||
|
*/
|
||||||
|
export interface FieldDTO<T = any> {
|
||||||
|
name: string; // The column name
|
||||||
|
type?: FieldType;
|
||||||
|
config?: FieldConfig;
|
||||||
|
values?: Vector<T> | T[]; // toJSON will always be T[], input could be either
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like a DataFrame, but fields may be a FieldDTO
|
||||||
|
*/
|
||||||
|
export interface DataFrameDTO extends QueryResultBase {
|
||||||
|
name?: string;
|
||||||
|
labels?: Labels;
|
||||||
|
fields: Array<FieldDTO | Field>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export type DisplayProcessor = (value: any) => DisplayValue;
|
||||||
|
|
||||||
export interface DisplayValue {
|
export interface DisplayValue {
|
||||||
text: string; // Show in the UI
|
text: string; // Show in the UI
|
||||||
numeric: number; // Use isNaN to check if it is a real number
|
numeric: number; // Use isNaN to check if it is a real number
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './data';
|
export * from './data';
|
||||||
|
export * from './dataFrame';
|
||||||
export * from './dataLink';
|
export * from './dataLink';
|
||||||
export * from './logs';
|
export * from './logs';
|
||||||
export * from './navModel';
|
export * from './navModel';
|
||||||
|
|||||||
@@ -4,42 +4,54 @@ exports[`read csv should get X and y 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"fields": Array [
|
"fields": Array [
|
||||||
Object {
|
Object {
|
||||||
"name": "Column 1",
|
"config": Object {},
|
||||||
"type": "number",
|
"name": "Field 1",
|
||||||
|
"type": "string",
|
||||||
|
"values": Array [
|
||||||
|
"",
|
||||||
|
"2",
|
||||||
|
"5",
|
||||||
|
"",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"name": "Column 2",
|
"config": Object {},
|
||||||
|
"name": "Field 2",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
NaN,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"name": "Column 3",
|
"config": Object {},
|
||||||
|
"name": "Field 3",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
null,
|
||||||
|
4,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"config": Object {},
|
||||||
"name": "Field 4",
|
"name": "Field 4",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
"values": Array [
|
||||||
],
|
|
||||||
"rows": Array [
|
|
||||||
Array [
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
Array [
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
Array [
|
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
7,
|
7,
|
||||||
],
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
"labels": undefined,
|
||||||
|
"meta": undefined,
|
||||||
|
"name": undefined,
|
||||||
|
"refId": undefined,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -47,30 +59,37 @@ exports[`read csv should read csv from local file system 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"fields": Array [
|
"fields": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"config": Object {},
|
||||||
"name": "a",
|
"name": "a",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
10,
|
||||||
|
40,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"config": Object {},
|
||||||
"name": "b",
|
"name": "b",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"config": Object {},
|
||||||
"name": "c",
|
"name": "c",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
"values": Array [
|
||||||
],
|
|
||||||
"rows": Array [
|
|
||||||
Array [
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
30,
|
30,
|
||||||
],
|
|
||||||
Array [
|
|
||||||
40,
|
|
||||||
50,
|
|
||||||
60,
|
60,
|
||||||
],
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
"labels": undefined,
|
||||||
|
"meta": undefined,
|
||||||
|
"name": undefined,
|
||||||
|
"refId": undefined,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -78,42 +97,48 @@ exports[`read csv should read csv with headers 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"fields": Array [
|
"fields": Array [
|
||||||
Object {
|
Object {
|
||||||
"name": "a",
|
"config": Object {
|
||||||
"type": "number",
|
|
||||||
"unit": "ms",
|
"unit": "ms",
|
||||||
},
|
},
|
||||||
|
"name": "a",
|
||||||
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
10,
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
40,
|
||||||
|
],
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"name": "b",
|
"config": Object {
|
||||||
"type": "string",
|
|
||||||
"unit": "lengthm",
|
"unit": "lengthm",
|
||||||
},
|
},
|
||||||
|
"name": "b",
|
||||||
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
500,
|
||||||
|
50,
|
||||||
|
],
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"name": "c",
|
"config": Object {
|
||||||
"type": "boolean",
|
|
||||||
"unit": "s",
|
"unit": "s",
|
||||||
},
|
},
|
||||||
],
|
"name": "c",
|
||||||
"rows": Array [
|
"type": "boolean",
|
||||||
Array [
|
"values": Array [
|
||||||
10,
|
true,
|
||||||
"20",
|
false,
|
||||||
true,
|
false,
|
||||||
],
|
|
||||||
Array [
|
|
||||||
40,
|
|
||||||
"50",
|
|
||||||
false,
|
|
||||||
],
|
|
||||||
Array [
|
|
||||||
40,
|
|
||||||
"500",
|
|
||||||
false,
|
|
||||||
],
|
|
||||||
Array [
|
|
||||||
40,
|
|
||||||
"50",
|
|
||||||
true,
|
true,
|
||||||
],
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
"labels": undefined,
|
||||||
|
"meta": undefined,
|
||||||
|
"name": undefined,
|
||||||
|
"refId": undefined,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { readCSV, toCSV, CSVHeaderStyle } from './csv';
|
import { readCSV, toCSV, CSVHeaderStyle } from './csv';
|
||||||
|
import { getDataFrameRow } from './processDataFrame';
|
||||||
|
|
||||||
// Test with local CSV files
|
// Test with local CSV files
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
|
import { toDataFrameDTO } from './processDataFrame';
|
||||||
|
|
||||||
describe('read csv', () => {
|
describe('read csv', () => {
|
||||||
it('should get X and y', () => {
|
it('should get X and y', () => {
|
||||||
@@ -11,14 +13,31 @@ describe('read csv', () => {
|
|||||||
|
|
||||||
const series = data[0];
|
const series = data[0];
|
||||||
expect(series.fields.length).toBe(4);
|
expect(series.fields.length).toBe(4);
|
||||||
expect(series.rows.length).toBe(3);
|
|
||||||
|
const rows = 4;
|
||||||
|
expect(series.length).toBe(rows);
|
||||||
|
|
||||||
// Make sure everythign it padded properly
|
// Make sure everythign it padded properly
|
||||||
for (const row of series.rows) {
|
for (const field of series.fields) {
|
||||||
expect(row.length).toBe(series.fields.length);
|
expect(field.values.length).toBe(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(series).toMatchSnapshot();
|
const dto = toDataFrameDTO(series);
|
||||||
|
expect(dto).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read single string OK', () => {
|
||||||
|
const text = 'a,b,c';
|
||||||
|
const data = readCSV(text);
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
|
||||||
|
const series = data[0];
|
||||||
|
expect(series.fields.length).toBe(3);
|
||||||
|
expect(series.length).toBe(0);
|
||||||
|
|
||||||
|
expect(series.fields[0].name).toEqual('a');
|
||||||
|
expect(series.fields[1].name).toEqual('b');
|
||||||
|
expect(series.fields[2].name).toEqual('c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should read csv from local file system', () => {
|
it('should read csv from local file system', () => {
|
||||||
@@ -28,7 +47,7 @@ describe('read csv', () => {
|
|||||||
const csv = fs.readFileSync(path, 'utf8');
|
const csv = fs.readFileSync(path, 'utf8');
|
||||||
const data = readCSV(csv);
|
const data = readCSV(csv);
|
||||||
expect(data.length).toBe(1);
|
expect(data.length).toBe(1);
|
||||||
expect(data[0]).toMatchSnapshot();
|
expect(toDataFrameDTO(data[0])).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should read csv with headers', () => {
|
it('should read csv with headers', () => {
|
||||||
@@ -38,7 +57,7 @@ describe('read csv', () => {
|
|||||||
const csv = fs.readFileSync(path, 'utf8');
|
const csv = fs.readFileSync(path, 'utf8');
|
||||||
const data = readCSV(csv);
|
const data = readCSV(csv);
|
||||||
expect(data.length).toBe(1);
|
expect(data.length).toBe(1);
|
||||||
expect(data[0]).toMatchSnapshot();
|
expect(toDataFrameDTO(data[0])).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +73,7 @@ describe('write csv', () => {
|
|||||||
const data = readCSV(csv);
|
const data = readCSV(csv);
|
||||||
const out = toCSV(data, { headerStyle: CSVHeaderStyle.full });
|
const out = toCSV(data, { headerStyle: CSVHeaderStyle.full });
|
||||||
expect(data.length).toBe(1);
|
expect(data.length).toBe(1);
|
||||||
expect(data[0].rows[0]).toEqual(firstRow);
|
expect(getDataFrameRow(data[0], 0)).toEqual(firstRow);
|
||||||
expect(data[0].fields.length).toBe(3);
|
expect(data[0].fields.length).toBe(3);
|
||||||
expect(norm(out)).toBe(norm(csv));
|
expect(norm(out)).toBe(norm(csv));
|
||||||
|
|
||||||
@@ -65,7 +84,7 @@ describe('write csv', () => {
|
|||||||
const f = readCSV(shorter);
|
const f = readCSV(shorter);
|
||||||
const fields = f[0].fields;
|
const fields = f[0].fields;
|
||||||
expect(fields.length).toBe(3);
|
expect(fields.length).toBe(3);
|
||||||
expect(f[0].rows[0]).toEqual(firstRow);
|
expect(getDataFrameRow(f[0], 0)).toEqual(firstRow);
|
||||||
expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names
|
expect(fields.map(f => f.name).join(',')).toEqual('a,b,c'); // the names
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import defaults from 'lodash/defaults';
|
|||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataFrame, Field, FieldType } from '../types';
|
import { DataFrame, Field, FieldType, FieldConfig } from '../types';
|
||||||
import { guessFieldTypeFromValue } from './processDataFrame';
|
import { guessFieldTypeFromValue } from './processDataFrame';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
|
||||||
export enum CSVHeaderStyle {
|
export enum CSVHeaderStyle {
|
||||||
full,
|
full,
|
||||||
@@ -28,9 +29,9 @@ export interface CSVParseCallbacks {
|
|||||||
* This can return a modified table to force any
|
* This can return a modified table to force any
|
||||||
* Column configurations
|
* Column configurations
|
||||||
*/
|
*/
|
||||||
onHeader: (table: DataFrame) => void;
|
onHeader: (fields: Field[]) => void;
|
||||||
|
|
||||||
// Called after each row is read and
|
// Called after each row is read
|
||||||
onRow: (row: any[]) => void;
|
onRow: (row: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +50,13 @@ enum ParseState {
|
|||||||
ReadingRows,
|
ReadingRows,
|
||||||
}
|
}
|
||||||
|
|
||||||
type FieldParser = (value: string) => any;
|
|
||||||
|
|
||||||
export class CSVReader {
|
export class CSVReader {
|
||||||
config: CSVConfig;
|
config: CSVConfig;
|
||||||
callback?: CSVParseCallbacks;
|
callback?: CSVParseCallbacks;
|
||||||
|
|
||||||
field: FieldParser[];
|
|
||||||
series: DataFrame;
|
|
||||||
state: ParseState;
|
state: ParseState;
|
||||||
data: DataFrame[];
|
data: DataFrameHelper[];
|
||||||
|
current: DataFrameHelper;
|
||||||
|
|
||||||
constructor(options?: CSVOptions) {
|
constructor(options?: CSVOptions) {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
@@ -67,12 +65,8 @@ export class CSVReader {
|
|||||||
this.config = options.config || {};
|
this.config = options.config || {};
|
||||||
this.callback = options.callback;
|
this.callback = options.callback;
|
||||||
|
|
||||||
this.field = [];
|
this.current = new DataFrameHelper({ fields: [] });
|
||||||
this.state = ParseState.Starting;
|
this.state = ParseState.Starting;
|
||||||
this.series = {
|
|
||||||
fields: [],
|
|
||||||
rows: [],
|
|
||||||
};
|
|
||||||
this.data = [];
|
this.data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,37 +86,42 @@ export class CSVReader {
|
|||||||
const idx = first.indexOf('#', 2);
|
const idx = first.indexOf('#', 2);
|
||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
const k = first.substr(1, idx - 1);
|
const k = first.substr(1, idx - 1);
|
||||||
|
const isName = 'name' === k;
|
||||||
|
|
||||||
// Simple object used to check if headers match
|
// Simple object used to check if headers match
|
||||||
const headerKeys: Field = {
|
const headerKeys: FieldConfig = {
|
||||||
name: '#',
|
|
||||||
type: FieldType.number,
|
|
||||||
unit: '#',
|
unit: '#',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if it is a known/supported column
|
// Check if it is a known/supported column
|
||||||
if (headerKeys.hasOwnProperty(k)) {
|
if (isName || headerKeys.hasOwnProperty(k)) {
|
||||||
// Starting a new table after reading rows
|
// Starting a new table after reading rows
|
||||||
if (this.state === ParseState.ReadingRows) {
|
if (this.state === ParseState.ReadingRows) {
|
||||||
this.series = {
|
this.current = new DataFrameHelper({ fields: [] });
|
||||||
fields: [],
|
this.data.push(this.current);
|
||||||
rows: [],
|
|
||||||
};
|
|
||||||
this.data.push(this.series);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
padColumnWidth(this.series.fields, line.length);
|
|
||||||
const fields: any[] = this.series.fields; // cast to any so we can lookup by key
|
|
||||||
const v = first.substr(idx + 1);
|
const v = first.substr(idx + 1);
|
||||||
fields[0][k] = v;
|
if (isName) {
|
||||||
for (let j = 1; j < fields.length; j++) {
|
this.current.addFieldFor(undefined, v);
|
||||||
fields[j][k] = line[j];
|
for (let j = 1; j < line.length; j++) {
|
||||||
|
this.current.addFieldFor(undefined, line[j]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const { fields } = this.current;
|
||||||
|
for (let j = 0; j < fields.length; j++) {
|
||||||
|
if (!fields[j].config) {
|
||||||
|
fields[j].config = {};
|
||||||
|
}
|
||||||
|
const disp = fields[j].config as any; // any lets name lookup
|
||||||
|
disp[k] = j === 0 ? v : line[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.state = ParseState.InHeader;
|
this.state = ParseState.InHeader;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (this.state === ParseState.Starting) {
|
} else if (this.state === ParseState.Starting) {
|
||||||
this.series.fields = makeFieldsFor(line);
|
|
||||||
this.state = ParseState.InHeader;
|
this.state = ParseState.InHeader;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,67 +132,48 @@ export class CSVReader {
|
|||||||
if (this.state === ParseState.Starting) {
|
if (this.state === ParseState.Starting) {
|
||||||
const type = guessFieldTypeFromValue(first);
|
const type = guessFieldTypeFromValue(first);
|
||||||
if (type === FieldType.string) {
|
if (type === FieldType.string) {
|
||||||
this.series.fields = makeFieldsFor(line);
|
for (const s of line) {
|
||||||
|
this.current.addFieldFor(undefined, s);
|
||||||
|
}
|
||||||
this.state = ParseState.InHeader;
|
this.state = ParseState.InHeader;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.series.fields = makeFieldsFor(new Array(line.length));
|
|
||||||
this.series.fields[0].type = type;
|
|
||||||
this.state = ParseState.InHeader; // fall through to read rows
|
this.state = ParseState.InHeader; // fall through to read rows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state === ParseState.InHeader) {
|
// Add the current results to the data
|
||||||
padColumnWidth(this.series.fields, line.length);
|
if (this.state !== ParseState.ReadingRows) {
|
||||||
|
// anything???
|
||||||
|
}
|
||||||
|
|
||||||
this.state = ParseState.ReadingRows;
|
this.state = ParseState.ReadingRows;
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state === ParseState.ReadingRows) {
|
|
||||||
// Make sure colum structure is valid
|
// Make sure colum structure is valid
|
||||||
if (line.length > this.series.fields.length) {
|
if (line.length > this.current.fields.length) {
|
||||||
padColumnWidth(this.series.fields, line.length);
|
const { fields } = this.current;
|
||||||
|
for (let f = fields.length; f < line.length; f++) {
|
||||||
|
this.current.addFieldFor(line[f]);
|
||||||
|
}
|
||||||
if (this.callback) {
|
if (this.callback) {
|
||||||
this.callback.onHeader(this.series);
|
this.callback.onHeader(this.current.fields);
|
||||||
} else {
|
|
||||||
// Expand all rows with nulls
|
|
||||||
for (let x = 0; x < this.series.rows.length; x++) {
|
|
||||||
const row = this.series.rows[x];
|
|
||||||
while (row.length < line.length) {
|
|
||||||
row.push(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const row: any[] = [];
|
|
||||||
for (let j = 0; j < line.length; j++) {
|
|
||||||
const v = line[j];
|
|
||||||
if (v) {
|
|
||||||
if (!this.field[j]) {
|
|
||||||
this.field[j] = makeFieldParser(v, this.series.fields[j]);
|
|
||||||
}
|
|
||||||
row.push(this.field[j](v));
|
|
||||||
} else {
|
|
||||||
row.push(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.current.appendRow(line);
|
||||||
if (this.callback) {
|
if (this.callback) {
|
||||||
// Send the header after we guess the type
|
// // Send the header after we guess the type
|
||||||
if (this.series.rows.length === 0) {
|
// if (this.series.rows.length === 0) {
|
||||||
this.callback.onHeader(this.series);
|
// this.callback.onHeader(this.series);
|
||||||
this.series.rows.push(row); // Only add the first row
|
// }
|
||||||
}
|
this.callback.onRow(line);
|
||||||
this.callback.onRow(row);
|
|
||||||
} else {
|
|
||||||
this.series.rows.push(row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
readCSV(text: string): DataFrame[] {
|
readCSV(text: string): DataFrameHelper[] {
|
||||||
this.data = [this.series];
|
this.current = new DataFrameHelper({ fields: [] });
|
||||||
|
this.data = [this.current];
|
||||||
|
|
||||||
const papacfg = {
|
const papacfg = {
|
||||||
...this.config,
|
...this.config,
|
||||||
@@ -204,61 +184,11 @@ export class CSVReader {
|
|||||||
} as ParseConfig;
|
} as ParseConfig;
|
||||||
|
|
||||||
Papa.parse(text, papacfg);
|
Papa.parse(text, papacfg);
|
||||||
|
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeFieldParser(value: string, field: Field): FieldParser {
|
|
||||||
if (!field.type) {
|
|
||||||
if (field.name === 'time' || field.name === 'Time') {
|
|
||||||
field.type = FieldType.time;
|
|
||||||
} else {
|
|
||||||
field.type = guessFieldTypeFromValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.type === FieldType.number) {
|
|
||||||
return (value: string) => {
|
|
||||||
return parseFloat(value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will convert anything that starts with "T" to true
|
|
||||||
if (field.type === FieldType.boolean) {
|
|
||||||
return (value: string) => {
|
|
||||||
return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just pass the string back
|
|
||||||
return (value: string) => value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a field object for each string in the list
|
|
||||||
*/
|
|
||||||
function makeFieldsFor(line: string[]): Field[] {
|
|
||||||
const fields: Field[] = [];
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
|
||||||
const v = line[i] ? line[i] : 'Column ' + (i + 1);
|
|
||||||
fields.push({ name: v });
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure the colum has valid entries up the the width
|
|
||||||
*/
|
|
||||||
function padColumnWidth(fields: Field[], width: number) {
|
|
||||||
if (fields.length < width) {
|
|
||||||
for (let i = fields.length; i < width; i++) {
|
|
||||||
fields.push({
|
|
||||||
name: 'Field ' + (i + 1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldWriter = (value: any) => string;
|
type FieldWriter = (value: any) => string;
|
||||||
|
|
||||||
function writeValue(value: any, config: CSVConfig): string {
|
function writeValue(value: any, config: CSVConfig): string {
|
||||||
@@ -295,15 +225,26 @@ function makeFieldWriter(field: Field, config: CSVConfig): FieldWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string {
|
function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string {
|
||||||
|
const isName = 'name' === key;
|
||||||
|
const isType = 'type' === key;
|
||||||
|
|
||||||
for (const f of fields) {
|
for (const f of fields) {
|
||||||
if (f.hasOwnProperty(key)) {
|
const display = f.config;
|
||||||
|
if (isName || isType || (display && display.hasOwnProperty(key))) {
|
||||||
let line = '#' + key + '#';
|
let line = '#' + key + '#';
|
||||||
for (let i = 0; i < fields.length; i++) {
|
for (let i = 0; i < fields.length; i++) {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
line = line + config.delimiter;
|
line = line + config.delimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const v = (fields[i] as any)[key];
|
let v: any = fields[i].name;
|
||||||
|
if (isType) {
|
||||||
|
v = fields[i].type;
|
||||||
|
} else if (isName) {
|
||||||
|
// already name
|
||||||
|
} else {
|
||||||
|
v = (fields[i].config as any)[key];
|
||||||
|
}
|
||||||
if (v) {
|
if (v) {
|
||||||
line = line + writeValue(v, config);
|
line = line + writeValue(v, config);
|
||||||
}
|
}
|
||||||
@@ -329,7 +270,7 @@ export function toCSV(data: DataFrame[], config?: CSVConfig): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const series of data) {
|
for (const series of data) {
|
||||||
const { rows, fields } = series;
|
const { fields } = series;
|
||||||
if (config.headerStyle === CSVHeaderStyle.full) {
|
if (config.headerStyle === CSVHeaderStyle.full) {
|
||||||
csv =
|
csv =
|
||||||
csv +
|
csv +
|
||||||
@@ -346,21 +287,23 @@ export function toCSV(data: DataFrame[], config?: CSVConfig): string {
|
|||||||
}
|
}
|
||||||
csv += config.newline;
|
csv += config.newline;
|
||||||
}
|
}
|
||||||
|
const length = fields[0].values.length;
|
||||||
|
if (length > 0) {
|
||||||
const writers = fields.map(field => makeFieldWriter(field, config!));
|
const writers = fields.map(field => makeFieldWriter(field, config!));
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const row = rows[i];
|
for (let j = 0; j < fields.length; j++) {
|
||||||
for (let j = 0; j < row.length; j++) {
|
|
||||||
if (j > 0) {
|
if (j > 0) {
|
||||||
csv = csv + config.delimiter;
|
csv = csv + config.delimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const v = row[j];
|
const v = fields[j].values.get(i);
|
||||||
if (v !== null) {
|
if (v !== null) {
|
||||||
csv = csv + writers[j](v);
|
csv = csv + writers[j](v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
csv = csv + config.newline;
|
csv = csv + config.newline;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
csv = csv + config.newline;
|
csv = csv + config.newline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
packages/grafana-data/src/utils/dataFrameHelper.test.ts
Normal file
89
packages/grafana-data/src/utils/dataFrameHelper.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { FieldType, DataFrameDTO, FieldDTO } from '../types/index';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
|
||||||
|
describe('dataFrameHelper', () => {
|
||||||
|
const frame: DataFrameDTO = {
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [4, 5, 6] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const ext = new DataFrameHelper(frame);
|
||||||
|
|
||||||
|
it('Should get a valid count for the fields', () => {
|
||||||
|
expect(ext.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should get the first field with a duplicate name', () => {
|
||||||
|
const field = ext.getFieldByName('value');
|
||||||
|
expect(field!.name).toEqual('value');
|
||||||
|
expect(field!.values.toJSON()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FieldCache', () => {
|
||||||
|
it('when creating a new FieldCache from fields should be able to query cache', () => {
|
||||||
|
const fields: FieldDTO[] = [
|
||||||
|
{ name: 'time', type: FieldType.time },
|
||||||
|
{ name: 'string', type: FieldType.string },
|
||||||
|
{ name: 'number', type: FieldType.number },
|
||||||
|
{ name: 'boolean', type: FieldType.boolean },
|
||||||
|
{ name: 'other', type: FieldType.other },
|
||||||
|
{ name: 'undefined' },
|
||||||
|
];
|
||||||
|
const fieldCache = new DataFrameHelper({ fields });
|
||||||
|
const allFields = fieldCache.getFields();
|
||||||
|
expect(allFields).toHaveLength(6);
|
||||||
|
|
||||||
|
const expectedFieldNames = ['time', 'string', 'number', 'boolean', 'other', 'undefined'];
|
||||||
|
|
||||||
|
expect(allFields.map(f => f.name)).toEqual(expectedFieldNames);
|
||||||
|
|
||||||
|
expect(fieldCache.hasFieldOfType(FieldType.time)).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldOfType(FieldType.string)).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldOfType(FieldType.number)).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldOfType(FieldType.boolean)).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldOfType(FieldType.other)).toBeTruthy();
|
||||||
|
|
||||||
|
expect(fieldCache.getFields(FieldType.time).map(f => f.name)).toEqual([expectedFieldNames[0]]);
|
||||||
|
expect(fieldCache.getFields(FieldType.string).map(f => f.name)).toEqual([expectedFieldNames[1]]);
|
||||||
|
expect(fieldCache.getFields(FieldType.number).map(f => f.name)).toEqual([expectedFieldNames[2]]);
|
||||||
|
expect(fieldCache.getFields(FieldType.boolean).map(f => f.name)).toEqual([expectedFieldNames[3]]);
|
||||||
|
expect(fieldCache.getFields(FieldType.other).map(f => f.name)).toEqual([
|
||||||
|
expectedFieldNames[4],
|
||||||
|
expectedFieldNames[5],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(fieldCache.fields[0].name).toEqual(expectedFieldNames[0]);
|
||||||
|
expect(fieldCache.fields[1].name).toEqual(expectedFieldNames[1]);
|
||||||
|
expect(fieldCache.fields[2].name).toEqual(expectedFieldNames[2]);
|
||||||
|
expect(fieldCache.fields[3].name).toEqual(expectedFieldNames[3]);
|
||||||
|
expect(fieldCache.fields[4].name).toEqual(expectedFieldNames[4]);
|
||||||
|
expect(fieldCache.fields[5].name).toEqual(expectedFieldNames[5]);
|
||||||
|
expect(fieldCache.fields[6]).toBeUndefined();
|
||||||
|
|
||||||
|
expect(fieldCache.getFirstFieldOfType(FieldType.time)!.name).toEqual(expectedFieldNames[0]);
|
||||||
|
expect(fieldCache.getFirstFieldOfType(FieldType.string)!.name).toEqual(expectedFieldNames[1]);
|
||||||
|
expect(fieldCache.getFirstFieldOfType(FieldType.number)!.name).toEqual(expectedFieldNames[2]);
|
||||||
|
expect(fieldCache.getFirstFieldOfType(FieldType.boolean)!.name).toEqual(expectedFieldNames[3]);
|
||||||
|
expect(fieldCache.getFirstFieldOfType(FieldType.other)!.name).toEqual(expectedFieldNames[4]);
|
||||||
|
|
||||||
|
expect(fieldCache.hasFieldNamed('tim')).toBeFalsy();
|
||||||
|
expect(fieldCache.hasFieldNamed('time')).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldNamed('string')).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldNamed('number')).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldNamed('boolean')).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldNamed('other')).toBeTruthy();
|
||||||
|
expect(fieldCache.hasFieldNamed('undefined')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(fieldCache.getFieldByName('time')!.name).toEqual(expectedFieldNames[0]);
|
||||||
|
expect(fieldCache.getFieldByName('string')!.name).toEqual(expectedFieldNames[1]);
|
||||||
|
expect(fieldCache.getFieldByName('number')!.name).toEqual(expectedFieldNames[2]);
|
||||||
|
expect(fieldCache.getFieldByName('boolean')!.name).toEqual(expectedFieldNames[3]);
|
||||||
|
expect(fieldCache.getFieldByName('other')!.name).toEqual(expectedFieldNames[4]);
|
||||||
|
expect(fieldCache.getFieldByName('undefined')!.name).toEqual(expectedFieldNames[5]);
|
||||||
|
expect(fieldCache.getFieldByName('null')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
232
packages/grafana-data/src/utils/dataFrameHelper.ts
Normal file
232
packages/grafana-data/src/utils/dataFrameHelper.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import { Field, FieldType, DataFrame, Vector, FieldDTO, DataFrameDTO } from '../types/dataFrame';
|
||||||
|
import { Labels, QueryResultMeta } from '../types/data';
|
||||||
|
import { guessFieldTypeForField, guessFieldTypeFromValue } from './processDataFrame';
|
||||||
|
import { ArrayVector } from './vector';
|
||||||
|
import isArray from 'lodash/isArray';
|
||||||
|
|
||||||
|
export class DataFrameHelper implements DataFrame {
|
||||||
|
refId?: string;
|
||||||
|
meta?: QueryResultMeta;
|
||||||
|
name?: string;
|
||||||
|
fields: Field[];
|
||||||
|
labels?: Labels;
|
||||||
|
length = 0; // updated so it is the length of all fields
|
||||||
|
|
||||||
|
private fieldByName: { [key: string]: Field } = {};
|
||||||
|
private fieldByType: { [key: string]: Field[] } = {};
|
||||||
|
|
||||||
|
constructor(data?: DataFrame | DataFrameDTO) {
|
||||||
|
if (!data) {
|
||||||
|
data = { fields: [] }; //
|
||||||
|
}
|
||||||
|
this.refId = data.refId;
|
||||||
|
this.meta = data.meta;
|
||||||
|
this.name = data.name;
|
||||||
|
this.labels = data.labels;
|
||||||
|
this.fields = [];
|
||||||
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
|
this.addField(data.fields[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFieldFor(value: any, name?: string): Field {
|
||||||
|
if (!name) {
|
||||||
|
name = `Field ${this.fields.length + 1}`;
|
||||||
|
}
|
||||||
|
return this.addField({
|
||||||
|
name,
|
||||||
|
type: guessFieldTypeFromValue(value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the direction of all fields
|
||||||
|
*/
|
||||||
|
reverse() {
|
||||||
|
for (const f of this.fields) {
|
||||||
|
if (isArray(f.values)) {
|
||||||
|
const arr = f.values as any[];
|
||||||
|
arr.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTypeIndex(field: Field) {
|
||||||
|
// Make sure it has a type
|
||||||
|
if (field.type === FieldType.other) {
|
||||||
|
const t = guessFieldTypeForField(field);
|
||||||
|
if (t) {
|
||||||
|
field.type = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.fieldByType[field.type]) {
|
||||||
|
this.fieldByType[field.type] = [];
|
||||||
|
}
|
||||||
|
this.fieldByType[field.type].push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
addField(f: Field | FieldDTO): Field {
|
||||||
|
const type = f.type || FieldType.other;
|
||||||
|
const values =
|
||||||
|
!f.values || isArray(f.values)
|
||||||
|
? new ArrayVector(f.values as any[] | undefined) // array or empty
|
||||||
|
: (f.values as Vector);
|
||||||
|
|
||||||
|
// And a name
|
||||||
|
let name = f.name;
|
||||||
|
if (!name) {
|
||||||
|
if (type === FieldType.time) {
|
||||||
|
name = `Time ${this.fields.length + 1}`;
|
||||||
|
} else {
|
||||||
|
name = `Column ${this.fields.length + 1}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const field: Field = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
config: f.config || {},
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
this.updateTypeIndex(field);
|
||||||
|
|
||||||
|
if (this.fieldByName[field.name]) {
|
||||||
|
console.warn('Duplicate field names in DataFrame: ', field.name);
|
||||||
|
} else {
|
||||||
|
this.fieldByName[field.name] = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the lengths all match
|
||||||
|
if (field.values.length !== this.length) {
|
||||||
|
if (field.values.length > this.length) {
|
||||||
|
// Add `null` to all other values
|
||||||
|
const newlen = field.values.length;
|
||||||
|
for (const fx of this.fields) {
|
||||||
|
const arr = fx.values as ArrayVector;
|
||||||
|
while (fx.values.length !== newlen) {
|
||||||
|
arr.buffer.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.length = field.values.length;
|
||||||
|
} else {
|
||||||
|
const arr = field.values as ArrayVector;
|
||||||
|
while (field.values.length !== this.length) {
|
||||||
|
arr.buffer.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fields.push(field);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will add each value to the corresponding column
|
||||||
|
*/
|
||||||
|
appendRow(row: any[]) {
|
||||||
|
for (let i = this.fields.length; i < row.length; i++) {
|
||||||
|
this.addFieldFor(row[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first line may change the field types
|
||||||
|
if (this.length < 1) {
|
||||||
|
this.fieldByType = {};
|
||||||
|
for (let i = 0; i < this.fields.length; i++) {
|
||||||
|
const f = this.fields[i];
|
||||||
|
if (!f.type || f.type === FieldType.other) {
|
||||||
|
f.type = guessFieldTypeFromValue(row[i]);
|
||||||
|
}
|
||||||
|
this.updateTypeIndex(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.fields.length; i++) {
|
||||||
|
const f = this.fields[i];
|
||||||
|
let v = row[i];
|
||||||
|
if (!f.parse) {
|
||||||
|
f.parse = makeFieldParser(v, f);
|
||||||
|
}
|
||||||
|
v = f.parse(v);
|
||||||
|
|
||||||
|
const arr = f.values as ArrayVector;
|
||||||
|
arr.buffer.push(v); // may be undefined
|
||||||
|
}
|
||||||
|
this.length++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add any values that match the field names
|
||||||
|
*/
|
||||||
|
appendRowFrom(obj: { [key: string]: any }) {
|
||||||
|
for (const f of this.fields) {
|
||||||
|
const v = obj[f.name];
|
||||||
|
if (!f.parse) {
|
||||||
|
f.parse = makeFieldParser(v, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr = f.values as ArrayVector;
|
||||||
|
arr.buffer.push(f.parse(v)); // may be undefined
|
||||||
|
}
|
||||||
|
this.length++;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFields(type?: FieldType): Field[] {
|
||||||
|
if (!type) {
|
||||||
|
return [...this.fields]; // All fields
|
||||||
|
}
|
||||||
|
const fields = this.fieldByType[type];
|
||||||
|
if (fields) {
|
||||||
|
return [...fields];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFieldOfType(type: FieldType): boolean {
|
||||||
|
const types = this.fieldByType[type];
|
||||||
|
return types && types.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstFieldOfType(type: FieldType): Field | undefined {
|
||||||
|
const arr = this.fieldByType[type];
|
||||||
|
if (arr && arr.length > 0) {
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFieldNamed(name: string): boolean {
|
||||||
|
return !!this.fieldByName[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first field with the given name.
|
||||||
|
*/
|
||||||
|
getFieldByName(name: string): Field | undefined {
|
||||||
|
return this.fieldByName[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFieldParser(value: string, field: Field): (value: string) => any {
|
||||||
|
if (!field.type) {
|
||||||
|
if (field.name === 'time' || field.name === 'Time') {
|
||||||
|
field.type = FieldType.time;
|
||||||
|
} else {
|
||||||
|
field.type = guessFieldTypeFromValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === FieldType.number) {
|
||||||
|
return (value: string) => {
|
||||||
|
return parseFloat(value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will convert anything that starts with "T" to true
|
||||||
|
if (field.type === FieldType.boolean) {
|
||||||
|
return (value: string) => {
|
||||||
|
return !(value[0] === 'F' || value[0] === 'f' || value[0] === '0');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just pass the string back
|
||||||
|
return (value: string) => value;
|
||||||
|
}
|
||||||
76
packages/grafana-data/src/utils/dataFrameView.test.ts
Normal file
76
packages/grafana-data/src/utils/dataFrameView.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { FieldType, DataFrameDTO } from '../types/index';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
import { DataFrameView } from './dataFrameView';
|
||||||
|
import { DateTime } from './moment_wrapper';
|
||||||
|
|
||||||
|
interface MySpecialObject {
|
||||||
|
time: DateTime;
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
more: string; // MISSING
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dataFrameView', () => {
|
||||||
|
const frame: DataFrameDTO = {
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'name', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const ext = new DataFrameHelper(frame);
|
||||||
|
const vector = new DataFrameView<MySpecialObject>(ext);
|
||||||
|
|
||||||
|
it('Should get a typed vector', () => {
|
||||||
|
expect(vector.length).toEqual(3);
|
||||||
|
|
||||||
|
const first = vector.get(0);
|
||||||
|
expect(first.time).toEqual(100);
|
||||||
|
expect(first.name).toEqual('a');
|
||||||
|
expect(first.value).toEqual(1);
|
||||||
|
expect(first.more).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support the spread operator', () => {
|
||||||
|
expect(vector.length).toEqual(3);
|
||||||
|
|
||||||
|
const first = vector.get(0);
|
||||||
|
const copy = { ...first };
|
||||||
|
expect(copy.time).toEqual(100);
|
||||||
|
expect(copy.name).toEqual('a');
|
||||||
|
expect(copy.value).toEqual(1);
|
||||||
|
expect(copy.more).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should support array indexes', () => {
|
||||||
|
expect(vector.length).toEqual(3);
|
||||||
|
|
||||||
|
const first = vector.get(0) as any;
|
||||||
|
expect(first[0]).toEqual(100);
|
||||||
|
expect(first[1]).toEqual('a');
|
||||||
|
expect(first[2]).toEqual(1);
|
||||||
|
expect(first[3]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should advertise the property names for each field', () => {
|
||||||
|
expect(vector.length).toEqual(3);
|
||||||
|
const first = vector.get(0);
|
||||||
|
const keys = Object.keys(first);
|
||||||
|
expect(keys).toEqual(['time', 'name', 'value']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a weird side effect that the object values change after interation', () => {
|
||||||
|
expect(vector.length).toEqual(3);
|
||||||
|
|
||||||
|
// Get the first value
|
||||||
|
const first = vector.get(0);
|
||||||
|
expect(first.name).toEqual('a');
|
||||||
|
|
||||||
|
// Then get the second one
|
||||||
|
const second = vector.get(1);
|
||||||
|
|
||||||
|
// the values for 'first' have changed
|
||||||
|
expect(first.name).toEqual('b');
|
||||||
|
expect(first.name).toEqual(second.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
67
packages/grafana-data/src/utils/dataFrameView.ts
Normal file
67
packages/grafana-data/src/utils/dataFrameView.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { DataFrame, Vector } from '../types/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This abstraction will present the contents of a DataFrame as if
|
||||||
|
* it were a well typed javascript object Vector.
|
||||||
|
*
|
||||||
|
* NOTE: The contents of the object returned from `view.get(index)`
|
||||||
|
* are optimized for use in a loop. All calls return the same object
|
||||||
|
* but the index has changed.
|
||||||
|
*
|
||||||
|
* For example, the three objects:
|
||||||
|
* const first = view.get(0);
|
||||||
|
* const second = view.get(1);
|
||||||
|
* const third = view.get(2);
|
||||||
|
* will point to the contents at index 2
|
||||||
|
*
|
||||||
|
* If you need three different objects, consider something like:
|
||||||
|
* const first = { ... view.get(0) };
|
||||||
|
* const second = { ... view.get(1) };
|
||||||
|
* const third = { ... view.get(2) };
|
||||||
|
*/
|
||||||
|
export class DataFrameView<T = any> implements Vector<T> {
|
||||||
|
private index = 0;
|
||||||
|
private obj: T;
|
||||||
|
|
||||||
|
constructor(private data: DataFrame) {
|
||||||
|
const obj = ({} as unknown) as T;
|
||||||
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
|
const field = data.fields[i];
|
||||||
|
const getter = () => {
|
||||||
|
return field.values.get(this.index);
|
||||||
|
};
|
||||||
|
if (!(obj as any).hasOwnProperty(field.name)) {
|
||||||
|
Object.defineProperty(obj, field.name, {
|
||||||
|
enumerable: true, // Shows up as enumerable property
|
||||||
|
get: getter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.defineProperty(obj, i, {
|
||||||
|
enumerable: false, // Don't enumerate array index
|
||||||
|
get: getter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.obj = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(idx: number) {
|
||||||
|
this.index = idx;
|
||||||
|
return this.obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
const arr: T[] = [];
|
||||||
|
for (let i = 0; i < this.data.length; i++) {
|
||||||
|
arr.push({ ...this.get(i) });
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return this.toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { FieldType } from '../types/index';
|
|
||||||
import { FieldCache } from './fieldCache';
|
|
||||||
|
|
||||||
describe('FieldCache', () => {
|
|
||||||
it('when creating a new FieldCache from fields should be able to query cache', () => {
|
|
||||||
const fields = [
|
|
||||||
{ name: 'time', type: FieldType.time },
|
|
||||||
{ name: 'string', type: FieldType.string },
|
|
||||||
{ name: 'number', type: FieldType.number },
|
|
||||||
{ name: 'boolean', type: FieldType.boolean },
|
|
||||||
{ name: 'other', type: FieldType.other },
|
|
||||||
{ name: 'undefined' },
|
|
||||||
];
|
|
||||||
const fieldCache = new FieldCache(fields);
|
|
||||||
const allFields = fieldCache.getFields();
|
|
||||||
expect(allFields).toHaveLength(6);
|
|
||||||
|
|
||||||
const expectedFields = [
|
|
||||||
{ ...fields[0], index: 0 },
|
|
||||||
{ ...fields[1], index: 1 },
|
|
||||||
{ ...fields[2], index: 2 },
|
|
||||||
{ ...fields[3], index: 3 },
|
|
||||||
{ ...fields[4], index: 4 },
|
|
||||||
{ ...fields[5], type: FieldType.other, index: 5 },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(allFields).toMatchObject(expectedFields);
|
|
||||||
|
|
||||||
expect(fieldCache.hasFieldOfType(FieldType.time)).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldOfType(FieldType.string)).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldOfType(FieldType.number)).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldOfType(FieldType.boolean)).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldOfType(FieldType.other)).toBeTruthy();
|
|
||||||
|
|
||||||
expect(fieldCache.getFields(FieldType.time)).toMatchObject([expectedFields[0]]);
|
|
||||||
expect(fieldCache.getFields(FieldType.string)).toMatchObject([expectedFields[1]]);
|
|
||||||
expect(fieldCache.getFields(FieldType.number)).toMatchObject([expectedFields[2]]);
|
|
||||||
expect(fieldCache.getFields(FieldType.boolean)).toMatchObject([expectedFields[3]]);
|
|
||||||
expect(fieldCache.getFields(FieldType.other)).toMatchObject([expectedFields[4], expectedFields[5]]);
|
|
||||||
|
|
||||||
expect(fieldCache.getFieldByIndex(0)).toMatchObject(expectedFields[0]);
|
|
||||||
expect(fieldCache.getFieldByIndex(1)).toMatchObject(expectedFields[1]);
|
|
||||||
expect(fieldCache.getFieldByIndex(2)).toMatchObject(expectedFields[2]);
|
|
||||||
expect(fieldCache.getFieldByIndex(3)).toMatchObject(expectedFields[3]);
|
|
||||||
expect(fieldCache.getFieldByIndex(4)).toMatchObject(expectedFields[4]);
|
|
||||||
expect(fieldCache.getFieldByIndex(5)).toMatchObject(expectedFields[5]);
|
|
||||||
expect(fieldCache.getFieldByIndex(6)).toBeNull();
|
|
||||||
|
|
||||||
expect(fieldCache.getFirstFieldOfType(FieldType.time)).toMatchObject(expectedFields[0]);
|
|
||||||
expect(fieldCache.getFirstFieldOfType(FieldType.string)).toMatchObject(expectedFields[1]);
|
|
||||||
expect(fieldCache.getFirstFieldOfType(FieldType.number)).toMatchObject(expectedFields[2]);
|
|
||||||
expect(fieldCache.getFirstFieldOfType(FieldType.boolean)).toMatchObject(expectedFields[3]);
|
|
||||||
expect(fieldCache.getFirstFieldOfType(FieldType.other)).toMatchObject(expectedFields[4]);
|
|
||||||
|
|
||||||
expect(fieldCache.hasFieldNamed('tim')).toBeFalsy();
|
|
||||||
expect(fieldCache.hasFieldNamed('time')).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldNamed('string')).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldNamed('number')).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldNamed('boolean')).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldNamed('other')).toBeTruthy();
|
|
||||||
expect(fieldCache.hasFieldNamed('undefined')).toBeTruthy();
|
|
||||||
|
|
||||||
expect(fieldCache.getFieldByName('time')).toMatchObject(expectedFields[0]);
|
|
||||||
expect(fieldCache.getFieldByName('string')).toMatchObject(expectedFields[1]);
|
|
||||||
expect(fieldCache.getFieldByName('number')).toMatchObject(expectedFields[2]);
|
|
||||||
expect(fieldCache.getFieldByName('boolean')).toMatchObject(expectedFields[3]);
|
|
||||||
expect(fieldCache.getFieldByName('other')).toMatchObject(expectedFields[4]);
|
|
||||||
expect(fieldCache.getFieldByName('undefined')).toMatchObject(expectedFields[5]);
|
|
||||||
expect(fieldCache.getFieldByName('null')).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { Field, FieldType } from '../types/index';
|
|
||||||
|
|
||||||
export interface IndexedField extends Field {
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FieldCache {
|
|
||||||
private fields: Field[];
|
|
||||||
private fieldIndexByName: { [key: string]: number };
|
|
||||||
private fieldIndexByType: { [key: string]: number[] };
|
|
||||||
|
|
||||||
constructor(fields?: Field[]) {
|
|
||||||
this.fields = [];
|
|
||||||
this.fieldIndexByName = {};
|
|
||||||
this.fieldIndexByType = {};
|
|
||||||
this.fieldIndexByType[FieldType.time] = [];
|
|
||||||
this.fieldIndexByType[FieldType.string] = [];
|
|
||||||
this.fieldIndexByType[FieldType.number] = [];
|
|
||||||
this.fieldIndexByType[FieldType.boolean] = [];
|
|
||||||
this.fieldIndexByType[FieldType.other] = [];
|
|
||||||
|
|
||||||
if (fields) {
|
|
||||||
for (let n = 0; n < fields.length; n++) {
|
|
||||||
const field = fields[n];
|
|
||||||
this.addField(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addField(field: Field) {
|
|
||||||
this.fields.push({
|
|
||||||
type: FieldType.other,
|
|
||||||
...field,
|
|
||||||
});
|
|
||||||
const index = this.fields.length - 1;
|
|
||||||
this.fieldIndexByName[field.name] = index;
|
|
||||||
this.fieldIndexByType[field.type || FieldType.other].push(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFieldOfType(type: FieldType): boolean {
|
|
||||||
return this.fieldIndexByType[type] && this.fieldIndexByType[type].length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFields(type?: FieldType): IndexedField[] {
|
|
||||||
const fields: IndexedField[] = [];
|
|
||||||
for (let index = 0; index < this.fields.length; index++) {
|
|
||||||
const field = this.fields[index];
|
|
||||||
|
|
||||||
if (!type || field.type === type) {
|
|
||||||
fields.push({ ...field, index });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldByIndex(index: number): IndexedField | null {
|
|
||||||
return this.fields[index] ? { ...this.fields[index], index } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstFieldOfType(type: FieldType): IndexedField | null {
|
|
||||||
return this.hasFieldOfType(type)
|
|
||||||
? { ...this.fields[this.fieldIndexByType[type][0]], index: this.fieldIndexByType[type][0] }
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFieldNamed(name: string): boolean {
|
|
||||||
return this.fieldIndexByName[name] !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldByName(name: string): IndexedField | null {
|
|
||||||
return this.hasFieldNamed(name)
|
|
||||||
? { ...this.fields[this.fieldIndexByName[name]], index: this.fieldIndexByName[name] }
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,32 @@
|
|||||||
import { fieldReducers, ReducerID, reduceField } from './fieldReducer';
|
import { fieldReducers, ReducerID, reduceField } from './fieldReducer';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DataFrame } from '../types/data';
|
import { Field, FieldType } from '../types/index';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
import { ArrayVector } from './vector';
|
||||||
|
import { guessFieldTypeFromValue } from './processDataFrame';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a reducer and get back the value
|
* Run a reducer and get back the value
|
||||||
*/
|
*/
|
||||||
function reduce(series: DataFrame, fieldIndex: number, id: string): any {
|
function reduce(field: Field, id: string): any {
|
||||||
return reduceField({ series, fieldIndex, reducers: [id] })[id];
|
return reduceField({ field, reducers: [id] })[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createField<T>(name: string, values?: T[], type?: FieldType): Field<T> {
|
||||||
|
const arr = new ArrayVector(values);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
config: {},
|
||||||
|
type: type ? type : guessFieldTypeFromValue(arr.get(0)),
|
||||||
|
values: arr,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Stats Calculators', () => {
|
describe('Stats Calculators', () => {
|
||||||
const basicTable = {
|
const basicTable = new DataFrameHelper({
|
||||||
fields: [{ name: 'a' }, { name: 'b' }, { name: 'c' }],
|
fields: [{ name: 'a', values: [10, 20] }, { name: 'b', values: [20, 30] }, { name: 'c', values: [30, 40] }],
|
||||||
rows: [[10, 20, 30], [20, 30, 40]],
|
});
|
||||||
};
|
|
||||||
|
|
||||||
it('should load all standard stats', () => {
|
it('should load all standard stats', () => {
|
||||||
for (const id of Object.keys(ReducerID)) {
|
for (const id of Object.keys(ReducerID)) {
|
||||||
@@ -38,8 +50,7 @@ describe('Stats Calculators', () => {
|
|||||||
|
|
||||||
it('should calculate basic stats', () => {
|
it('should calculate basic stats', () => {
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
series: basicTable,
|
field: basicTable.fields[0],
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: ['first', 'last', 'mean'],
|
reducers: ['first', 'last', 'mean'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,9 +65,9 @@ describe('Stats Calculators', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should support a single stat also', () => {
|
it('should support a single stat also', () => {
|
||||||
|
basicTable.fields[0].calcs = undefined; // clear the cache
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
series: basicTable,
|
field: basicTable.fields[0],
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: ['first'],
|
reducers: ['first'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,8 +78,7 @@ describe('Stats Calculators', () => {
|
|||||||
|
|
||||||
it('should get non standard stats', () => {
|
it('should get non standard stats', () => {
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
series: basicTable,
|
field: basicTable.fields[0],
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: [ReducerID.distinctCount, ReducerID.changeCount],
|
reducers: [ReducerID.distinctCount, ReducerID.changeCount],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,8 +88,7 @@ describe('Stats Calculators', () => {
|
|||||||
|
|
||||||
it('should calculate step', () => {
|
it('should calculate step', () => {
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
series: { fields: [{ name: 'A' }], rows: [[100], [200], [300], [400]] },
|
field: createField('x', [100, 200, 300, 400]),
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: [ReducerID.step, ReducerID.delta],
|
reducers: [ReducerID.step, ReducerID.delta],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,53 +97,38 @@ describe('Stats Calculators', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('consistenly check allIsNull/allIsZero', () => {
|
it('consistenly check allIsNull/allIsZero', () => {
|
||||||
const empty = {
|
const empty = createField('x');
|
||||||
fields: [{ name: 'A' }],
|
const allNull = createField('x', [null, null, null, null]);
|
||||||
rows: [],
|
const allUndefined = createField('x', [undefined, undefined, undefined, undefined]);
|
||||||
};
|
const allZero = createField('x', [0, 0, 0, 0]);
|
||||||
const allNull = ({
|
|
||||||
fields: [{ name: 'A' }],
|
|
||||||
rows: [null, null, null, null],
|
|
||||||
} as unknown) as DataFrame;
|
|
||||||
const allNull2 = {
|
|
||||||
fields: [{ name: 'A' }],
|
|
||||||
rows: [[null], [null], [null], [null]],
|
|
||||||
};
|
|
||||||
const allZero = {
|
|
||||||
fields: [{ name: 'A' }],
|
|
||||||
rows: [[0], [0], [0], [0]],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(reduce(empty, 0, ReducerID.allIsNull)).toEqual(true);
|
expect(reduce(empty, ReducerID.allIsNull)).toEqual(true);
|
||||||
expect(reduce(allNull, 0, ReducerID.allIsNull)).toEqual(true);
|
expect(reduce(allNull, ReducerID.allIsNull)).toEqual(true);
|
||||||
expect(reduce(allNull2, 0, ReducerID.allIsNull)).toEqual(true);
|
expect(reduce(allUndefined, ReducerID.allIsNull)).toEqual(true);
|
||||||
|
|
||||||
expect(reduce(empty, 0, ReducerID.allIsZero)).toEqual(false);
|
expect(reduce(empty, ReducerID.allIsZero)).toEqual(false);
|
||||||
expect(reduce(allNull, 0, ReducerID.allIsZero)).toEqual(false);
|
expect(reduce(allNull, ReducerID.allIsZero)).toEqual(false);
|
||||||
expect(reduce(allNull2, 0, ReducerID.allIsZero)).toEqual(false);
|
expect(reduce(allZero, ReducerID.allIsZero)).toEqual(true);
|
||||||
expect(reduce(allZero, 0, ReducerID.allIsZero)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('consistent results for first/last value with null', () => {
|
it('consistent results for first/last value with null', () => {
|
||||||
const info = [
|
const info = [
|
||||||
{
|
{
|
||||||
rows: [[null], [200], [null]], // first/last value is null
|
data: [null, 200, null], // first/last value is null
|
||||||
result: 200,
|
result: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rows: [[null], [null], [null]], // All null
|
data: [null, null, null], // All null
|
||||||
result: undefined,
|
result: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rows: [], // Empty row
|
data: [undefined, undefined, undefined], // Empty row
|
||||||
result: undefined,
|
result: undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const fields = [{ name: 'A' }];
|
|
||||||
|
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
series: { rows: info[0].rows, fields },
|
field: createField('x', info[0].data),
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: [ReducerID.first, ReducerID.last, ReducerID.firstNotNull, ReducerID.lastNotNull], // uses standard path
|
reducers: [ReducerID.first, ReducerID.last, ReducerID.firstNotNull, ReducerID.lastNotNull], // uses standard path
|
||||||
});
|
});
|
||||||
expect(stats[ReducerID.first]).toEqual(null);
|
expect(stats[ReducerID.first]).toEqual(null);
|
||||||
@@ -146,21 +140,19 @@ describe('Stats Calculators', () => {
|
|||||||
for (const input of info) {
|
for (const input of info) {
|
||||||
for (const reducer of reducers) {
|
for (const reducer of reducers) {
|
||||||
const v1 = reduceField({
|
const v1 = reduceField({
|
||||||
series: { rows: input.rows, fields },
|
field: createField('x', input.data),
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: [reducer, ReducerID.mean], // uses standard path
|
reducers: [reducer, ReducerID.mean], // uses standard path
|
||||||
})[reducer];
|
})[reducer];
|
||||||
|
|
||||||
const v2 = reduceField({
|
const v2 = reduceField({
|
||||||
series: { rows: input.rows, fields },
|
field: createField('x', input.data),
|
||||||
fieldIndex: 0,
|
|
||||||
reducers: [reducer], // uses optimized path
|
reducers: [reducer], // uses optimized path
|
||||||
})[reducer];
|
})[reducer];
|
||||||
|
|
||||||
if (v1 !== v2 || v1 !== input.result) {
|
if (v1 !== v2 || v1 !== input.result) {
|
||||||
const msg =
|
const msg =
|
||||||
`Invalid ${reducer} result for: ` +
|
`Invalid ${reducer} result for: ` +
|
||||||
input.rows.join(', ') +
|
input.data.join(', ') +
|
||||||
` Expected: ${input.result}` + // configured
|
` Expected: ${input.result}` + // configured
|
||||||
` Recieved: Multiple: ${v1}, Single: ${v2}`;
|
` Recieved: Multiple: ${v1}, Single: ${v2}`;
|
||||||
expect(msg).toEqual(null);
|
expect(msg).toEqual(null);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
|
||||||
import { DataFrame, NullValueMode } from '../types';
|
import { NullValueMode, Field } from '../types';
|
||||||
import { Registry, RegistryItem } from './registry';
|
import { Registry, RegistryItem } from './registry';
|
||||||
|
|
||||||
export enum ReducerID {
|
export enum ReducerID {
|
||||||
@@ -33,7 +33,7 @@ export interface FieldCalcs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Internal function
|
// Internal function
|
||||||
type FieldReducer = (data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
||||||
|
|
||||||
export interface FieldReducerInfo extends RegistryItem {
|
export interface FieldReducerInfo extends RegistryItem {
|
||||||
// Internal details
|
// Internal details
|
||||||
@@ -43,52 +43,76 @@ export interface FieldReducerInfo extends RegistryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ReduceFieldOptions {
|
interface ReduceFieldOptions {
|
||||||
series: DataFrame;
|
field: Field;
|
||||||
fieldIndex: number;
|
|
||||||
reducers: string[]; // The stats to calculate
|
reducers: string[]; // The stats to calculate
|
||||||
nullValueMode?: NullValueMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns an object with a key for each selected stat
|
* @returns an object with a key for each selected stat
|
||||||
*/
|
*/
|
||||||
export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||||
const { series, fieldIndex, reducers, nullValueMode } = options;
|
const { field, reducers } = options;
|
||||||
|
|
||||||
if (!reducers || reducers.length < 1) {
|
if (!field || !reducers || reducers.length < 1) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.calcs) {
|
||||||
|
// Find the values we need to calculate
|
||||||
|
const missing: string[] = [];
|
||||||
|
for (const s of reducers) {
|
||||||
|
if (!field.calcs.hasOwnProperty(s)) {
|
||||||
|
missing.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.length < 1) {
|
||||||
|
return {
|
||||||
|
...field.calcs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queue = fieldReducers.list(reducers);
|
const queue = fieldReducers.list(reducers);
|
||||||
|
|
||||||
// Return early for empty series
|
// Return early for empty series
|
||||||
// This lets the concrete implementations assume at least one row
|
// This lets the concrete implementations assume at least one row
|
||||||
if (!series.rows || series.rows.length < 1) {
|
const data = field.values;
|
||||||
const calcs = {} as FieldCalcs;
|
if (data.length < 1) {
|
||||||
|
const calcs = { ...field.calcs } as FieldCalcs;
|
||||||
for (const reducer of queue) {
|
for (const reducer of queue) {
|
||||||
calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
|
calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
|
||||||
}
|
}
|
||||||
return calcs;
|
return (field.calcs = calcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { nullValueMode } = field.config;
|
||||||
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
||||||
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
||||||
|
|
||||||
// Avoid calculating all the standard stats if possible
|
// Avoid calculating all the standard stats if possible
|
||||||
if (queue.length === 1 && queue[0].reduce) {
|
if (queue.length === 1 && queue[0].reduce) {
|
||||||
return queue[0].reduce(series, fieldIndex, ignoreNulls, nullAsZero);
|
const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
|
||||||
|
field.calcs = {
|
||||||
|
...field.calcs,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now everything can use the standard stats
|
// For now everything can use the standard stats
|
||||||
let values = doStandardCalcs(series, fieldIndex, ignoreNulls, nullAsZero);
|
let values = doStandardCalcs(field, ignoreNulls, nullAsZero);
|
||||||
for (const reducer of queue) {
|
for (const reducer of queue) {
|
||||||
if (!values.hasOwnProperty(reducer.id) && reducer.reduce) {
|
if (!values.hasOwnProperty(reducer.id) && reducer.reduce) {
|
||||||
values = {
|
values = {
|
||||||
...values,
|
...values,
|
||||||
...reducer.reduce(series, fieldIndex, ignoreNulls, nullAsZero),
|
...reducer.reduce(field, ignoreNulls, nullAsZero),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
field.calcs = {
|
||||||
|
...field.calcs,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +224,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
const calcs = {
|
const calcs = {
|
||||||
sum: 0,
|
sum: 0,
|
||||||
max: -Number.MAX_VALUE,
|
max: -Number.MAX_VALUE,
|
||||||
@@ -223,9 +247,10 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|||||||
// Just used for calcutations -- not exposed as a stat
|
// Just used for calcutations -- not exposed as a stat
|
||||||
previousDeltaUp: true,
|
previousDeltaUp: true,
|
||||||
} as FieldCalcs;
|
} as FieldCalcs;
|
||||||
|
const data = field.values;
|
||||||
|
|
||||||
for (let i = 0; i < data.rows.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
let currentValue = data.rows[i] ? data.rows[i][fieldIndex] : null;
|
let currentValue = data.get(i);
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
calcs.first = currentValue;
|
calcs.first = currentValue;
|
||||||
}
|
}
|
||||||
@@ -260,7 +285,7 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|||||||
if (calcs.lastNotNull! > currentValue) {
|
if (calcs.lastNotNull! > currentValue) {
|
||||||
// counter reset
|
// counter reset
|
||||||
calcs.previousDeltaUp = false;
|
calcs.previousDeltaUp = false;
|
||||||
if (i === data.rows.length - 1) {
|
if (i === data.length - 1) {
|
||||||
// reset on last
|
// reset on last
|
||||||
calcs.delta += currentValue;
|
calcs.delta += currentValue;
|
||||||
}
|
}
|
||||||
@@ -326,18 +351,14 @@ function doStandardCalcs(data: DataFrame, fieldIndex: number, ignoreNulls: boole
|
|||||||
return calcs;
|
return calcs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateFirst(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
function calculateFirst(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
return { first: data.rows[0][fieldIndex] };
|
return { first: field.values.get(0) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateFirstNotNull(
|
function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
data: DataFrame,
|
const data = field.values;
|
||||||
fieldIndex: number,
|
for (let idx = 0; idx < data.length; idx++) {
|
||||||
ignoreNulls: boolean,
|
const v = data.get(idx);
|
||||||
nullAsZero: boolean
|
|
||||||
): FieldCalcs {
|
|
||||||
for (let idx = 0; idx < data.rows.length; idx++) {
|
|
||||||
const v = data.rows[idx][fieldIndex];
|
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
return { firstNotNull: v };
|
return { firstNotNull: v };
|
||||||
}
|
}
|
||||||
@@ -345,19 +366,16 @@ function calculateFirstNotNull(
|
|||||||
return { firstNotNull: undefined };
|
return { firstNotNull: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateLast(data: DataFrame, fieldIndex: number, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
function calculateLast(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
return { last: data.rows[data.rows.length - 1][fieldIndex] };
|
const data = field.values;
|
||||||
|
return { last: data.get(data.length - 1) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateLastNotNull(
|
function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
data: DataFrame,
|
const data = field.values;
|
||||||
fieldIndex: number,
|
let idx = data.length - 1;
|
||||||
ignoreNulls: boolean,
|
|
||||||
nullAsZero: boolean
|
|
||||||
): FieldCalcs {
|
|
||||||
let idx = data.rows.length - 1;
|
|
||||||
while (idx >= 0) {
|
while (idx >= 0) {
|
||||||
const v = data.rows[idx--][fieldIndex];
|
const v = data.get(idx--);
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
return { lastNotNull: v };
|
return { lastNotNull: v };
|
||||||
}
|
}
|
||||||
@@ -365,17 +383,13 @@ function calculateLastNotNull(
|
|||||||
return { lastNotNull: undefined };
|
return { lastNotNull: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateChangeCount(
|
function calculateChangeCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
data: DataFrame,
|
const data = field.values;
|
||||||
fieldIndex: number,
|
|
||||||
ignoreNulls: boolean,
|
|
||||||
nullAsZero: boolean
|
|
||||||
): FieldCalcs {
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let first = true;
|
let first = true;
|
||||||
let last: any = null;
|
let last: any = null;
|
||||||
for (let i = 0; i < data.rows.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
let currentValue = data.rows[i][fieldIndex];
|
let currentValue = data.get(i);
|
||||||
if (currentValue === null) {
|
if (currentValue === null) {
|
||||||
if (ignoreNulls) {
|
if (ignoreNulls) {
|
||||||
continue;
|
continue;
|
||||||
@@ -394,15 +408,11 @@ function calculateChangeCount(
|
|||||||
return { changeCount: count };
|
return { changeCount: count };
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateDistinctCount(
|
function calculateDistinctCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
data: DataFrame,
|
const data = field.values;
|
||||||
fieldIndex: number,
|
|
||||||
ignoreNulls: boolean,
|
|
||||||
nullAsZero: boolean
|
|
||||||
): FieldCalcs {
|
|
||||||
const distinct = new Set<any>();
|
const distinct = new Set<any>();
|
||||||
for (let i = 0; i < data.rows.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
let currentValue = data.rows[i][fieldIndex];
|
let currentValue = data.get(i);
|
||||||
if (currentValue === null) {
|
if (currentValue === null) {
|
||||||
if (ignoreNulls) {
|
if (ignoreNulls) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ export * from './logs';
|
|||||||
export * from './labels';
|
export * from './labels';
|
||||||
export * from './labels';
|
export * from './labels';
|
||||||
export * from './object';
|
export * from './object';
|
||||||
export * from './fieldCache';
|
|
||||||
export * from './moment_wrapper';
|
export * from './moment_wrapper';
|
||||||
export * from './thresholds';
|
export * from './thresholds';
|
||||||
|
export * from './dataFrameHelper';
|
||||||
|
export * from './dataFrameView';
|
||||||
|
export * from './vector';
|
||||||
|
|
||||||
export { getMappedValue } from './valueMappings';
|
export { getMappedValue } from './valueMappings';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LogLevel } from '../types/logs';
|
import { LogLevel } from '../types/logs';
|
||||||
import { DataFrame, FieldType } from '../types/data';
|
import { DataFrame, FieldType } from '../types/index';
|
||||||
|
import { ArrayVector } from './vector';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log level of a log line.
|
* Returns the log level of a log line.
|
||||||
@@ -33,12 +34,23 @@ export function getLogLevelFromKey(key: string): LogLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addLogLevelToSeries(series: DataFrame, lineIndex: number): DataFrame {
|
export function addLogLevelToSeries(series: DataFrame, lineIndex: number): DataFrame {
|
||||||
|
const levels = new ArrayVector<LogLevel>();
|
||||||
|
const lines = series.fields[lineIndex];
|
||||||
|
for (let i = 0; i < lines.values.length; i++) {
|
||||||
|
const line = lines.values.get(lineIndex);
|
||||||
|
levels.buffer.push(getLogLevel(line));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...series, // Keeps Tags, RefID etc
|
...series, // Keeps Tags, RefID etc
|
||||||
fields: [...series.fields, { name: 'LogLevel', type: FieldType.string }],
|
fields: [
|
||||||
rows: series.rows.map(row => {
|
...series.fields,
|
||||||
const line = row[lineIndex];
|
{
|
||||||
return [...row, getLogLevel(line)];
|
name: 'LogLevel',
|
||||||
}),
|
type: FieldType.string,
|
||||||
|
values: levels,
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import {
|
|||||||
toDataFrame,
|
toDataFrame,
|
||||||
guessFieldTypes,
|
guessFieldTypes,
|
||||||
guessFieldTypeFromValue,
|
guessFieldTypeFromValue,
|
||||||
|
sortDataFrame,
|
||||||
} from './processDataFrame';
|
} from './processDataFrame';
|
||||||
import { FieldType, TimeSeries, DataFrame, TableData } from '../types/data';
|
import { FieldType, TimeSeries, TableData, DataFrameDTO } from '../types/index';
|
||||||
import { dateTime } from './moment_wrapper';
|
import { dateTime } from './moment_wrapper';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
|
||||||
describe('toDataFrame', () => {
|
describe('toDataFrame', () => {
|
||||||
it('converts timeseries to series', () => {
|
it('converts timeseries to series', () => {
|
||||||
@@ -17,7 +19,15 @@ describe('toDataFrame', () => {
|
|||||||
};
|
};
|
||||||
let series = toDataFrame(input1);
|
let series = toDataFrame(input1);
|
||||||
expect(series.fields[0].name).toBe(input1.target);
|
expect(series.fields[0].name).toBe(input1.target);
|
||||||
expect(series.rows).toBe(input1.datapoints);
|
|
||||||
|
const v0 = series.fields[0].values;
|
||||||
|
const v1 = series.fields[1].values;
|
||||||
|
expect(v0.length).toEqual(2);
|
||||||
|
expect(v1.length).toEqual(2);
|
||||||
|
expect(v0.get(0)).toEqual(100);
|
||||||
|
expect(v0.get(1)).toEqual(200);
|
||||||
|
expect(v1.get(0)).toEqual(1);
|
||||||
|
expect(v1.get(1)).toEqual(2);
|
||||||
|
|
||||||
// Should fill a default name if target is empty
|
// Should fill a default name if target is empty
|
||||||
const input2 = {
|
const input2 = {
|
||||||
@@ -39,12 +49,23 @@ describe('toDataFrame', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps dataFrame unchanged', () => {
|
it('keeps dataFrame unchanged', () => {
|
||||||
const input = {
|
const input = toDataFrame({
|
||||||
fields: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
datapoints: [[100, 1], [200, 2]],
|
||||||
|
});
|
||||||
|
expect(input.length).toEqual(2);
|
||||||
|
|
||||||
|
// If the object is alreay a DataFrame, it should not change
|
||||||
|
const again = toDataFrame(input);
|
||||||
|
expect(again).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('migrate from 6.3 style rows', () => {
|
||||||
|
const oldDataFrame = {
|
||||||
|
fields: [{ name: 'A' }, { name: 'B' }, { name: 'C' }],
|
||||||
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
|
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
|
||||||
};
|
};
|
||||||
const series = toDataFrame(input);
|
const data = toDataFrame(oldDataFrame);
|
||||||
expect(series).toBe(input);
|
expect(data.length).toBe(oldDataFrame.rows.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Guess Colum Types from value', () => {
|
it('Guess Colum Types from value', () => {
|
||||||
@@ -68,14 +89,18 @@ describe('toDataFrame', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Guess Colum Types from series', () => {
|
it('Guess Colum Types from series', () => {
|
||||||
const series = {
|
const series = new DataFrameHelper({
|
||||||
fields: [{ name: 'A (number)' }, { name: 'B (strings)' }, { name: 'C (nulls)' }, { name: 'Time' }],
|
fields: [
|
||||||
rows: [[123, null, null, '2000'], [null, 'Hello', null, 'XXX']],
|
{ name: 'A (number)', values: [123, null] },
|
||||||
};
|
{ name: 'B (strings)', values: [null, 'Hello'] },
|
||||||
|
{ name: 'C (nulls)', values: [null, null] },
|
||||||
|
{ name: 'Time', values: ['2000', 1967] },
|
||||||
|
],
|
||||||
|
});
|
||||||
const norm = guessFieldTypes(series);
|
const norm = guessFieldTypes(series);
|
||||||
expect(norm.fields[0].type).toBe(FieldType.number);
|
expect(norm.fields[0].type).toBe(FieldType.number);
|
||||||
expect(norm.fields[1].type).toBe(FieldType.string);
|
expect(norm.fields[1].type).toBe(FieldType.string);
|
||||||
expect(norm.fields[2].type).toBeUndefined();
|
expect(norm.fields[2].type).toBe(FieldType.other);
|
||||||
expect(norm.fields[3].type).toBe(FieldType.time); // based on name
|
expect(norm.fields[3].type).toBe(FieldType.time); // based on name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -103,6 +128,7 @@ describe('SerisData backwards compatibility', () => {
|
|||||||
const series = toDataFrame(table);
|
const series = toDataFrame(table);
|
||||||
expect(isTableData(table)).toBeTruthy();
|
expect(isTableData(table)).toBeTruthy();
|
||||||
expect(isDataFrame(series)).toBeTruthy();
|
expect(isDataFrame(series)).toBeTruthy();
|
||||||
|
expect(series.fields[0].config.unit).toEqual('ms');
|
||||||
|
|
||||||
const roundtrip = toLegacyResponseData(series) as TimeSeries;
|
const roundtrip = toLegacyResponseData(series) as TimeSeries;
|
||||||
expect(isTableData(roundtrip)).toBeTruthy();
|
expect(isTableData(roundtrip)).toBeTruthy();
|
||||||
@@ -110,23 +136,46 @@ describe('SerisData backwards compatibility', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('converts DataFrame to TableData to series and back again', () => {
|
it('converts DataFrame to TableData to series and back again', () => {
|
||||||
const series: DataFrame = {
|
const json: DataFrameDTO = {
|
||||||
refId: 'Z',
|
refId: 'Z',
|
||||||
meta: {
|
meta: {
|
||||||
somethign: 8,
|
somethign: 8,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'T', type: FieldType.time }, // first
|
{ name: 'T', type: FieldType.time, values: [1, 2, 3] },
|
||||||
{ name: 'N', type: FieldType.number, filterable: true },
|
{ name: 'N', type: FieldType.number, config: { filterable: true }, values: [100, 200, 300] },
|
||||||
{ name: 'S', type: FieldType.string, filterable: true },
|
{ name: 'S', type: FieldType.string, config: { filterable: true }, values: ['1', '2', '3'] },
|
||||||
],
|
],
|
||||||
rows: [[1, 100, '1'], [2, 200, '2'], [3, 300, '3']],
|
|
||||||
};
|
};
|
||||||
|
const series = toDataFrame(json);
|
||||||
const table = toLegacyResponseData(series) as TableData;
|
const table = toLegacyResponseData(series) as TableData;
|
||||||
expect(table.meta).toBe(series.meta);
|
|
||||||
expect(table.refId).toBe(series.refId);
|
expect(table.refId).toBe(series.refId);
|
||||||
|
expect(table.meta).toEqual(series.meta);
|
||||||
|
|
||||||
const names = table.columns.map(c => c.text);
|
const names = table.columns.map(c => c.text);
|
||||||
expect(names).toEqual(['T', 'N', 'S']);
|
expect(names).toEqual(['T', 'N', 'S']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sorted DataFrame', () => {
|
||||||
|
const frame = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'fist', type: FieldType.time, values: [1, 2, 3] },
|
||||||
|
{ name: 'second', type: FieldType.string, values: ['a', 'b', 'c'] },
|
||||||
|
{ name: 'third', type: FieldType.number, values: [2000, 3000, 1000] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
it('Should sort numbers', () => {
|
||||||
|
const sorted = sortDataFrame(frame, 0, true);
|
||||||
|
expect(sorted.length).toEqual(3);
|
||||||
|
expect(sorted.fields[0].values.toJSON()).toEqual([3, 2, 1]);
|
||||||
|
expect(sorted.fields[1].values.toJSON()).toEqual(['c', 'b', 'a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should sort strings', () => {
|
||||||
|
const sorted = sortDataFrame(frame, 1, true);
|
||||||
|
expect(sorted.length).toEqual(3);
|
||||||
|
expect(sorted.fields[0].values.toJSON()).toEqual([3, 2, 1]);
|
||||||
|
expect(sorted.fields[1].values.toJSON()).toEqual(['c', 'b', 'a']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,61 +4,122 @@ import isString from 'lodash/isString';
|
|||||||
import isBoolean from 'lodash/isBoolean';
|
import isBoolean from 'lodash/isBoolean';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataFrame, Field, TimeSeries, FieldType, TableData, Column, GraphSeriesXY } from '../types/index';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
Field,
|
||||||
|
FieldConfig,
|
||||||
|
TimeSeries,
|
||||||
|
FieldType,
|
||||||
|
TableData,
|
||||||
|
Column,
|
||||||
|
GraphSeriesXY,
|
||||||
|
TimeSeriesValue,
|
||||||
|
FieldDTO,
|
||||||
|
DataFrameDTO,
|
||||||
|
} from '../types/index';
|
||||||
import { isDateTime } from './moment_wrapper';
|
import { isDateTime } from './moment_wrapper';
|
||||||
|
import { ArrayVector, SortedVector } from './vector';
|
||||||
|
import { DataFrameHelper } from './dataFrameHelper';
|
||||||
|
|
||||||
function convertTableToDataFrame(table: TableData): DataFrame {
|
function convertTableToDataFrame(table: TableData): DataFrame {
|
||||||
|
const fields = table.columns.map(c => {
|
||||||
|
const { text, ...disp } = c;
|
||||||
return {
|
return {
|
||||||
// rename the 'text' to 'name' field
|
name: text, // rename 'text' to the 'name' field
|
||||||
fields: table.columns.map(c => {
|
config: (disp || {}) as FieldConfig,
|
||||||
const { text, ...field } = c;
|
values: new ArrayVector(),
|
||||||
const f = field as Field;
|
type: FieldType.other,
|
||||||
f.name = text;
|
};
|
||||||
return f;
|
});
|
||||||
}),
|
// Fill in the field values
|
||||||
rows: table.rows,
|
for (const row of table.rows) {
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
fields[i].values.buffer.push(row[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const f of fields) {
|
||||||
|
const t = guessFieldTypeForField(f);
|
||||||
|
if (t) {
|
||||||
|
f.type = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields,
|
||||||
refId: table.refId,
|
refId: table.refId,
|
||||||
meta: table.meta,
|
meta: table.meta,
|
||||||
name: table.name,
|
name: table.name,
|
||||||
|
length: fields[0].values.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||||
return {
|
const fields = [
|
||||||
name: timeSeries.target,
|
|
||||||
fields: [
|
|
||||||
{
|
{
|
||||||
name: timeSeries.target || 'Value',
|
name: timeSeries.target || 'Value',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
|
config: {
|
||||||
unit: timeSeries.unit,
|
unit: timeSeries.unit,
|
||||||
},
|
},
|
||||||
|
values: new ArrayVector<TimeSeriesValue>(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Time',
|
name: 'Time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
config: {
|
||||||
unit: 'dateTimeAsIso',
|
unit: 'dateTimeAsIso',
|
||||||
},
|
},
|
||||||
],
|
values: new ArrayVector<number>(),
|
||||||
rows: timeSeries.datapoints,
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const point of timeSeries.datapoints) {
|
||||||
|
fields[0].values.buffer.push(point[0]);
|
||||||
|
fields[1].values.buffer.push(point[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: timeSeries.target,
|
||||||
labels: timeSeries.tags,
|
labels: timeSeries.tags,
|
||||||
refId: timeSeries.refId,
|
refId: timeSeries.refId,
|
||||||
meta: timeSeries.meta,
|
meta: timeSeries.meta,
|
||||||
|
fields,
|
||||||
|
length: timeSeries.datapoints.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is added temporarily while we convert the LogsModel
|
||||||
|
* to DataFrame. See: https://github.com/grafana/grafana/issues/18528
|
||||||
|
*/
|
||||||
function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
|
function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
|
||||||
|
const x = new ArrayVector();
|
||||||
|
const y = new ArrayVector();
|
||||||
|
for (let i = 0; i < graphSeries.data.length; i++) {
|
||||||
|
const row = graphSeries.data[i];
|
||||||
|
x.buffer.push(row[0]);
|
||||||
|
y.buffer.push(row[1]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: graphSeries.label,
|
name: graphSeries.label,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: graphSeries.label || 'Value',
|
name: graphSeries.label || 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: x,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Time',
|
name: 'Time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
config: {
|
||||||
unit: 'dateTimeAsIso',
|
unit: 'dateTimeAsIso',
|
||||||
},
|
},
|
||||||
|
values: y,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
rows: graphSeries.data,
|
length: x.buffer.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,20 +163,18 @@ export function guessFieldTypeFromValue(v: any): FieldType {
|
|||||||
/**
|
/**
|
||||||
* Looks at the data to guess the column type. This ignores any existing setting
|
* Looks at the data to guess the column type. This ignores any existing setting
|
||||||
*/
|
*/
|
||||||
export function guessFieldTypeFromSeries(series: DataFrame, index: number): FieldType | undefined {
|
export function guessFieldTypeForField(field: Field): FieldType | undefined {
|
||||||
const column = series.fields[index];
|
|
||||||
|
|
||||||
// 1. Use the column name to guess
|
// 1. Use the column name to guess
|
||||||
if (column.name) {
|
if (field.name) {
|
||||||
const name = column.name.toLowerCase();
|
const name = field.name.toLowerCase();
|
||||||
if (name === 'date' || name === 'time') {
|
if (name === 'date' || name === 'time') {
|
||||||
return FieldType.time;
|
return FieldType.time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check the first non-null value
|
// 2. Check the first non-null value
|
||||||
for (let i = 0; i < series.rows.length; i++) {
|
for (let i = 0; i < field.values.length; i++) {
|
||||||
const v = series.rows[i][index];
|
const v = field.values.get(i);
|
||||||
if (v !== null) {
|
if (v !== null) {
|
||||||
return guessFieldTypeFromValue(v);
|
return guessFieldTypeFromValue(v);
|
||||||
}
|
}
|
||||||
@@ -135,14 +194,14 @@ export const guessFieldTypes = (series: DataFrame): DataFrame => {
|
|||||||
// Somethign is missing a type return a modified copy
|
// Somethign is missing a type return a modified copy
|
||||||
return {
|
return {
|
||||||
...series,
|
...series,
|
||||||
fields: series.fields.map((field, index) => {
|
fields: series.fields.map(field => {
|
||||||
if (field.type) {
|
if (field.type && field.type !== FieldType.other) {
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
// Replace it with a calculated version
|
// Calculate a reasonable schema value
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
type: guessFieldTypeFromSeries(series, index),
|
type: guessFieldTypeForField(field) || FieldType.other,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -158,8 +217,23 @@ export const isDataFrame = (data: any): data is DataFrame => data && data.hasOwn
|
|||||||
|
|
||||||
export const toDataFrame = (data: any): DataFrame => {
|
export const toDataFrame = (data: any): DataFrame => {
|
||||||
if (data.hasOwnProperty('fields')) {
|
if (data.hasOwnProperty('fields')) {
|
||||||
|
// @deprecated -- remove in 6.5
|
||||||
|
if (data.hasOwnProperty('rows')) {
|
||||||
|
const v = new DataFrameHelper(data as DataFrameDTO);
|
||||||
|
const rows = data.rows as any[][];
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
v.appendRow(rows[i]);
|
||||||
|
}
|
||||||
|
// TODO: deprection warning
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataFrameDTO does not have length
|
||||||
|
if (data.hasOwnProperty('length')) {
|
||||||
return data as DataFrame;
|
return data as DataFrame;
|
||||||
}
|
}
|
||||||
|
return new DataFrameHelper(data as DataFrameDTO);
|
||||||
|
}
|
||||||
if (data.hasOwnProperty('datapoints')) {
|
if (data.hasOwnProperty('datapoints')) {
|
||||||
return convertTimeSeriesToDataFrame(data);
|
return convertTimeSeriesToDataFrame(data);
|
||||||
}
|
}
|
||||||
@@ -174,52 +248,129 @@ export const toDataFrame = (data: any): DataFrame => {
|
|||||||
throw new Error('Unsupported data format');
|
throw new Error('Unsupported data format');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toLegacyResponseData = (series: DataFrame): TimeSeries | TableData => {
|
export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData => {
|
||||||
const { fields, rows } = series;
|
const { fields } = frame;
|
||||||
|
|
||||||
|
const length = fields[0].values.length;
|
||||||
|
const rows: any[][] = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const row: any[] = [];
|
||||||
|
for (let j = 0; j < fields.length; j++) {
|
||||||
|
row.push(fields[j].values.get(i));
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.length === 2) {
|
if (fields.length === 2) {
|
||||||
const type = guessFieldTypeFromSeries(series, 1);
|
let type = fields[1].type;
|
||||||
|
if (!type) {
|
||||||
|
type = guessFieldTypeForField(fields[1]) || FieldType.other;
|
||||||
|
}
|
||||||
if (type === FieldType.time) {
|
if (type === FieldType.time) {
|
||||||
return {
|
return {
|
||||||
alias: fields[0].name || series.name,
|
alias: fields[0].name || frame.name,
|
||||||
target: fields[0].name || series.name,
|
target: fields[0].name || frame.name,
|
||||||
datapoints: rows,
|
datapoints: rows,
|
||||||
unit: fields[0].unit,
|
unit: fields[0].config ? fields[0].config.unit : undefined,
|
||||||
refId: series.refId,
|
refId: frame.refId,
|
||||||
meta: series.meta,
|
meta: frame.meta,
|
||||||
} as TimeSeries;
|
} as TimeSeries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: fields.map(f => {
|
columns: fields.map(f => {
|
||||||
const { name, ...column } = f;
|
const { name, config } = f;
|
||||||
|
if (config) {
|
||||||
|
// keep unit etc
|
||||||
|
const { ...column } = config;
|
||||||
(column as Column).text = name;
|
(column as Column).text = name;
|
||||||
return column as Column;
|
return column as Column;
|
||||||
|
}
|
||||||
|
return { text: name };
|
||||||
}),
|
}),
|
||||||
refId: series.refId,
|
refId: frame.refId,
|
||||||
meta: series.meta,
|
meta: frame.meta,
|
||||||
rows,
|
rows,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function sortDataFrame(data: DataFrame, sortIndex?: number, reverse = false): DataFrame {
|
export function sortDataFrame(data: DataFrame, sortIndex?: number, reverse = false): DataFrame {
|
||||||
if (isNumber(sortIndex)) {
|
const field = data.fields[sortIndex!];
|
||||||
const copy = {
|
if (!field) {
|
||||||
...data,
|
return data;
|
||||||
rows: [...data.rows].sort((a, b) => {
|
}
|
||||||
a = a[sortIndex];
|
|
||||||
b = b[sortIndex];
|
// Natural order
|
||||||
// Sort null or undefined separately from comparable values
|
const index: number[] = [];
|
||||||
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
|
for (let i = 0; i < data.length; i++) {
|
||||||
}),
|
index.push(i);
|
||||||
|
}
|
||||||
|
const values = field.values;
|
||||||
|
|
||||||
|
// Numeric Comparison
|
||||||
|
let compare = (a: number, b: number) => {
|
||||||
|
const vA = values.get(a);
|
||||||
|
const vB = values.get(b);
|
||||||
|
return vA - vB; // works for numbers!
|
||||||
};
|
};
|
||||||
|
|
||||||
if (reverse) {
|
// String Comparison
|
||||||
copy.rows.reverse();
|
if (field.type === FieldType.string) {
|
||||||
|
compare = (a: number, b: number) => {
|
||||||
|
const vA: string = values.get(a);
|
||||||
|
const vB: string = values.get(b);
|
||||||
|
return vA.localeCompare(vB);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy;
|
// Run the sort function
|
||||||
|
index.sort(compare);
|
||||||
|
if (reverse) {
|
||||||
|
index.reverse();
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
|
// Return a copy that maps sorted values
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
fields: data.fields.map(f => {
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
values: new SortedVector(f.values, index),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to get an array from each field value
|
||||||
|
*/
|
||||||
|
export function getDataFrameRow(data: DataFrame, row: number): any[] {
|
||||||
|
const values: any[] = [];
|
||||||
|
for (const field of data.fields) {
|
||||||
|
values.push(field.values.get(row));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy that does not include functions
|
||||||
|
*/
|
||||||
|
export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
|
||||||
|
const fields: FieldDTO[] = data.fields.map(f => {
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
config: f.config,
|
||||||
|
values: f.values.toJSON(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields,
|
||||||
|
refId: data.refId,
|
||||||
|
meta: data.meta,
|
||||||
|
name: data.name,
|
||||||
|
labels: data.labels,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
43
packages/grafana-data/src/utils/vector.test.ts
Normal file
43
packages/grafana-data/src/utils/vector.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ConstantVector, ScaledVector, ArrayVector, CircularVector } from './vector';
|
||||||
|
|
||||||
|
describe('Check Proxy Vector', () => {
|
||||||
|
it('should support constant values', () => {
|
||||||
|
const value = 3.5;
|
||||||
|
const v = new ConstantVector(value, 7);
|
||||||
|
expect(v.length).toEqual(7);
|
||||||
|
|
||||||
|
expect(v.get(0)).toEqual(value);
|
||||||
|
expect(v.get(1)).toEqual(value);
|
||||||
|
|
||||||
|
// Now check all of them
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
expect(v.get(i)).toEqual(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support multiply operations', () => {
|
||||||
|
const source = new ArrayVector([1, 2, 3, 4]);
|
||||||
|
const scale = 2.456;
|
||||||
|
const v = new ScaledVector(source, scale);
|
||||||
|
expect(v.length).toEqual(source.length);
|
||||||
|
// expect(v.push(10)).toEqual(source.length); // not implemented
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
expect(v.get(i)).toEqual(source.get(i) * scale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Check Circular Vector', () => {
|
||||||
|
it('should support constant values', () => {
|
||||||
|
const buffer = [3, 2, 1, 0];
|
||||||
|
const v = new CircularVector(buffer);
|
||||||
|
expect(v.length).toEqual(4);
|
||||||
|
expect(v.toJSON()).toEqual([3, 2, 1, 0]);
|
||||||
|
|
||||||
|
v.append(4);
|
||||||
|
expect(v.toJSON()).toEqual([4, 3, 2, 1]);
|
||||||
|
|
||||||
|
v.append(5);
|
||||||
|
expect(v.toJSON()).toEqual([5, 4, 3, 2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
133
packages/grafana-data/src/utils/vector.ts
Normal file
133
packages/grafana-data/src/utils/vector.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { Vector } from '../types/dataFrame';
|
||||||
|
|
||||||
|
export function vectorToArray<T>(v: Vector<T>): T[] {
|
||||||
|
const arr: T[] = [];
|
||||||
|
for (let i = 0; i < v.length; i++) {
|
||||||
|
arr[i] = v.get(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArrayVector<T = any> implements Vector<T> {
|
||||||
|
buffer: T[];
|
||||||
|
|
||||||
|
constructor(buffer?: T[]) {
|
||||||
|
this.buffer = buffer ? buffer : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.buffer[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return this.buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConstantVector<T = any> implements Vector<T> {
|
||||||
|
constructor(private value: T, private len: number) {}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
const arr: T[] = [];
|
||||||
|
for (let i = 0; i < this.length; i++) {
|
||||||
|
arr[i] = this.value;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return this.toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScaledVector implements Vector<number> {
|
||||||
|
constructor(private source: Vector<number>, private scale: number) {}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): number {
|
||||||
|
return this.source.get(index) * this.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): number[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): number[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CircularVector<T = any> implements Vector<T> {
|
||||||
|
buffer: T[];
|
||||||
|
index: number;
|
||||||
|
length: number;
|
||||||
|
|
||||||
|
constructor(buffer: T[]) {
|
||||||
|
this.length = buffer.length;
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(value: T) {
|
||||||
|
let idx = this.index - 1;
|
||||||
|
if (idx < 0) {
|
||||||
|
idx = this.length - 1;
|
||||||
|
}
|
||||||
|
this.buffer[idx] = value;
|
||||||
|
this.index = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.buffer[(index + this.index) % this.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Values are returned in the order defined by the input parameter
|
||||||
|
*/
|
||||||
|
export class SortedVector<T = any> implements Vector<T> {
|
||||||
|
constructor(private source: Vector<T>, private order: number[]) {}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.source.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.source.get(this.order[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): T[] {
|
||||||
|
return vectorToArray(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { StatsPicker } from '../StatsPicker/StatsPicker';
|
|||||||
// Types
|
// Types
|
||||||
import { FieldDisplayOptions, DEFAULT_FIELD_DISPLAY_VALUES_LIMIT } from '../../utils/fieldDisplay';
|
import { FieldDisplayOptions, DEFAULT_FIELD_DISPLAY_VALUES_LIMIT } from '../../utils/fieldDisplay';
|
||||||
import Select from '../Select/Select';
|
import Select from '../Select/Select';
|
||||||
import { Field, ReducerID, toNumberString, toIntegerOrUndefined, SelectableValue } from '@grafana/data';
|
import { ReducerID, toNumberString, toIntegerOrUndefined, SelectableValue, FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
const showOptions: Array<SelectableValue<boolean>> = [
|
const showOptions: Array<SelectableValue<boolean>> = [
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,7 @@ export class FieldDisplayEditor extends PureComponent<Props> {
|
|||||||
this.props.onChange({ ...this.props.value, calcs });
|
this.props.onChange({ ...this.props.value, calcs });
|
||||||
};
|
};
|
||||||
|
|
||||||
onDefaultsChange = (value: Partial<Field>) => {
|
onDefaultsChange = (value: FieldConfig) => {
|
||||||
this.props.onChange({ ...this.props.value, defaults: value });
|
this.props.onChange({ ...this.props.value, defaults: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FormLabel } from '../FormLabel/FormLabel';
|
|||||||
import { UnitPicker } from '../UnitPicker/UnitPicker';
|
import { UnitPicker } from '../UnitPicker/UnitPicker';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { toIntegerOrUndefined, Field, SelectableValue, toFloatOrUndefined, toNumberString } from '@grafana/data';
|
import { toIntegerOrUndefined, SelectableValue, FieldConfig, toFloatOrUndefined, toNumberString } from '@grafana/data';
|
||||||
|
|
||||||
import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
|
import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ const labelWidth = 6;
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
showMinMax: boolean;
|
showMinMax: boolean;
|
||||||
value: Partial<Field>;
|
value: FieldConfig;
|
||||||
onChange: (value: Partial<Field>, event?: React.SyntheticEvent<HTMLElement>) => void;
|
onChange: (value: FieldConfig, event?: React.SyntheticEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax }) => {
|
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax }) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getTheme } from '../../themes';
|
|||||||
|
|
||||||
import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
|
import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
|
||||||
import { ScopedVars, GrafanaThemeType } from '../../types/index';
|
import { ScopedVars, GrafanaThemeType } from '../../types/index';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame, FieldType, ArrayVector } from '@grafana/data';
|
||||||
import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
|
import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
|
||||||
import { number, boolean } from '@storybook/addon-knobs';
|
import { number, boolean } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
@@ -33,14 +33,19 @@ export function columnIndexToLeter(column: number) {
|
|||||||
export function makeDummyTable(columnCount: number, rowCount: number): DataFrame {
|
export function makeDummyTable(columnCount: number, rowCount: number): DataFrame {
|
||||||
return {
|
return {
|
||||||
fields: Array.from(new Array(columnCount), (x, i) => {
|
fields: Array.from(new Array(columnCount), (x, i) => {
|
||||||
|
const colId = columnIndexToLeter(i);
|
||||||
|
const values = new ArrayVector<string>();
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
values.buffer.push(colId + (i + 1));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: columnIndexToLeter(i),
|
name: colId,
|
||||||
|
type: FieldType.string,
|
||||||
|
config: {},
|
||||||
|
values,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
rows: Array.from(new Array(rowCount), (x, rowId) => {
|
length: rowCount,
|
||||||
const suffix = (rowId + 1).toString();
|
|
||||||
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
import { Themeable } from '../../types/theme';
|
import { Themeable } from '../../types/theme';
|
||||||
|
|
||||||
import { stringToJsRegex, DataFrame, sortDataFrame } from '@grafana/data';
|
import { stringToJsRegex, DataFrame, sortDataFrame, getDataFrameRow, ArrayVector, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TableCellBuilder,
|
TableCellBuilder,
|
||||||
@@ -107,7 +107,7 @@ export class Table extends Component<Props, State> {
|
|||||||
|
|
||||||
if (dataChanged || rotate !== prevProps.rotate) {
|
if (dataChanged || rotate !== prevProps.rotate) {
|
||||||
const { width, minColumnWidth } = this.props;
|
const { width, minColumnWidth } = this.props;
|
||||||
this.rotateWidth = Math.max(width / data.rows.length, minColumnWidth);
|
this.rotateWidth = Math.max(width / data.length, minColumnWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the data when data or sort changes
|
// Update the data when data or sort changes
|
||||||
@@ -146,7 +146,7 @@ export class Table extends Component<Props, State> {
|
|||||||
return {
|
return {
|
||||||
header: title,
|
header: title,
|
||||||
width: columnWidth,
|
width: columnWidth,
|
||||||
builder: getCellBuilder(col, style, this.props),
|
builder: getCellBuilder(col.config || {}, style, this.props),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -185,9 +185,9 @@ export class Table extends Component<Props, State> {
|
|||||||
if (row < 0) {
|
if (row < 0) {
|
||||||
this.doSort(column);
|
this.doSort(column);
|
||||||
} else {
|
} else {
|
||||||
const values = this.state.data.rows[row];
|
const field = this.state.data.fields[columnIndex];
|
||||||
const value = values[column];
|
const value = field.values.get(rowIndex);
|
||||||
console.log('CLICK', value, row);
|
console.log('CLICK', value, field.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,6 +201,9 @@ export class Table extends Component<Props, State> {
|
|||||||
if (!col) {
|
if (!col) {
|
||||||
col = {
|
col = {
|
||||||
name: '??' + columnIndex + '???',
|
name: '??' + columnIndex + '???',
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector(),
|
||||||
|
type: FieldType.other,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +229,7 @@ export class Table extends Component<Props, State> {
|
|||||||
const { data } = this.state;
|
const { data } = this.state;
|
||||||
|
|
||||||
const isHeader = row < 0;
|
const isHeader = row < 0;
|
||||||
const rowData = isHeader ? data.fields : data.rows[row];
|
const rowData = isHeader ? data.fields : getDataFrameRow(data, row); // TODO! improve
|
||||||
const value = rowData ? rowData[column] : '';
|
const value = rowData ? rowData[column] : '';
|
||||||
const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
|
const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
|
||||||
|
|
||||||
@@ -258,7 +261,7 @@ export class Table extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let columnCount = data.fields.length;
|
let columnCount = data.fields.length;
|
||||||
let rowCount = data.rows.length + (showHeader ? 1 : 0);
|
let rowCount = data.length + (showHeader ? 1 : 0);
|
||||||
|
|
||||||
let fixedColumnCount = Math.min(fixedColumns, columnCount);
|
let fixedColumnCount = Math.min(fixedColumns, columnCount);
|
||||||
let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
|
let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Table, Props } from './Table';
|
|||||||
import { ValueFormatter, getValueFormat, getColorFromHexRgbOrName } from '../../utils/index';
|
import { ValueFormatter, getValueFormat, getColorFromHexRgbOrName } from '../../utils/index';
|
||||||
import { GrafanaTheme } from '../../types/theme';
|
import { GrafanaTheme } from '../../types/theme';
|
||||||
import { InterpolateFunction } from '../../types/panel';
|
import { InterpolateFunction } from '../../types/panel';
|
||||||
import { Field, dateTime } from '@grafana/data';
|
import { Field, dateTime, FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
export interface TableCellBuilderOptions {
|
export interface TableCellBuilderOptions {
|
||||||
value: any;
|
value: any;
|
||||||
@@ -73,7 +73,7 @@ export interface ColumnStyle {
|
|||||||
// private replaceVariables: InterpolateFunction,
|
// private replaceVariables: InterpolateFunction,
|
||||||
// private fmt?:ValueFormatter) {
|
// private fmt?:ValueFormatter) {
|
||||||
|
|
||||||
export function getCellBuilder(schema: Field, style: ColumnStyle | null, props: Props): TableCellBuilder {
|
export function getCellBuilder(schema: FieldConfig, style: ColumnStyle | null, props: Props): TableCellBuilder {
|
||||||
if (!style) {
|
if (!style) {
|
||||||
return simpleCellBuilder;
|
return simpleCellBuilder;
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ class CellBuilderWithStyle {
|
|||||||
private mapper: ValueMapper,
|
private mapper: ValueMapper,
|
||||||
private style: ColumnStyle,
|
private style: ColumnStyle,
|
||||||
private theme: GrafanaTheme,
|
private theme: GrafanaTheme,
|
||||||
private column: Field,
|
private schema: FieldConfig,
|
||||||
private replaceVariables: InterpolateFunction,
|
private replaceVariables: InterpolateFunction,
|
||||||
private fmt?: ValueFormatter
|
private fmt?: ValueFormatter
|
||||||
) {}
|
) {}
|
||||||
@@ -244,7 +244,7 @@ class CellBuilderWithStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ??? I don't think this will still work!
|
// ??? I don't think this will still work!
|
||||||
if (this.column.filterable) {
|
if (this.schema.filterable) {
|
||||||
cellClasses.push('table-panel-cell-filterable');
|
cellClasses.push('table-panel-cell-filterable');
|
||||||
value = (
|
value = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ export class TableInputCSV extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<footer>
|
<footer>
|
||||||
{data.map((series, index) => {
|
{data.map((frame, index) => {
|
||||||
return (
|
return (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
Rows:{series.rows.length}, Columns:{series.fields.length}
|
Rows:{frame.length}, Columns:{frame.fields.length}
|
||||||
<i className="fa fa-check-circle" />
|
<i className="fa fa-check-circle" />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DataFrame } from '@grafana/data';
|
import { toDataFrame } from '@grafana/data';
|
||||||
import { ColumnStyle } from './TableCellBuilder';
|
import { ColumnStyle } from './TableCellBuilder';
|
||||||
import { getColorDefinitionByName } from '../../utils/namedColorsPalette';
|
import { getColorDefinitionByName } from '../../utils/namedColorsPalette';
|
||||||
|
|
||||||
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
|
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
|
||||||
|
|
||||||
export const migratedTestTable = {
|
export const migratedTestTable = toDataFrame({
|
||||||
type: 'table',
|
type: 'table',
|
||||||
fields: [
|
columns: [
|
||||||
{ name: 'Time' },
|
{ name: 'Time' },
|
||||||
{ name: 'Value' },
|
{ name: 'Value' },
|
||||||
{ name: 'Colored' },
|
{ name: 'Colored' },
|
||||||
@@ -22,7 +22,7 @@ export const migratedTestTable = {
|
|||||||
{ name: 'RangeMappingColored' },
|
{ name: 'RangeMappingColored' },
|
||||||
],
|
],
|
||||||
rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
|
rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
|
||||||
} as DataFrame;
|
});
|
||||||
|
|
||||||
export const migratedTestStyles: ColumnStyle[] = [
|
export const migratedTestStyles: ColumnStyle[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { ComponentType, ComponentClass } from 'react';
|
import { ComponentType, ComponentClass } from 'react';
|
||||||
import { TimeRange, RawTimeRange, TableData, TimeSeries, DataFrame, LogRowModel, LoadingState } from '@grafana/data';
|
import {
|
||||||
|
TimeRange,
|
||||||
|
RawTimeRange,
|
||||||
|
TableData,
|
||||||
|
TimeSeries,
|
||||||
|
DataFrame,
|
||||||
|
LogRowModel,
|
||||||
|
LoadingState,
|
||||||
|
DataFrameDTO,
|
||||||
|
} from '@grafana/data';
|
||||||
import { PluginMeta, GrafanaPlugin } from './plugin';
|
import { PluginMeta, GrafanaPlugin } from './plugin';
|
||||||
import { PanelData } from './panel';
|
import { PanelData } from './panel';
|
||||||
|
|
||||||
@@ -286,7 +295,7 @@ export interface ExploreStartPageProps {
|
|||||||
*/
|
*/
|
||||||
export type LegacyResponseData = TimeSeries | TableData | any;
|
export type LegacyResponseData = TimeSeries | TableData | any;
|
||||||
|
|
||||||
export type DataQueryResponseData = DataFrame | LegacyResponseData;
|
export type DataQueryResponseData = DataFrameDTO | LegacyResponseData;
|
||||||
|
|
||||||
export type DataStreamObserver = (event: DataStreamState) => void;
|
export type DataStreamObserver = (event: DataStreamState) => void;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MappingType, ValueMapping, DisplayValue } from '@grafana/data';
|
import { MappingType, ValueMapping, DisplayProcessor, DisplayValue } from '@grafana/data';
|
||||||
|
|
||||||
import { getDisplayProcessor, getColorFromThreshold, DisplayProcessor, getDecimalsForValue } from './displayValue';
|
import { getDisplayProcessor, getColorFromThreshold, getDecimalsForValue } from './displayValue';
|
||||||
|
|
||||||
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
|
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
|
||||||
processors.forEach(processor => {
|
processors.forEach(processor => {
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Threshold, getMappedValue, Field, DecimalInfo, DisplayValue, DecimalCount } from '@grafana/data';
|
import {
|
||||||
|
Threshold,
|
||||||
|
getMappedValue,
|
||||||
|
FieldConfig,
|
||||||
|
DisplayProcessor,
|
||||||
|
DecimalInfo,
|
||||||
|
DisplayValue,
|
||||||
|
DecimalCount,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { getValueFormat } from './valueFormats/valueFormats';
|
import { getValueFormat } from './valueFormats/valueFormats';
|
||||||
@@ -9,13 +17,8 @@ import { getColorFromHexRgbOrName } from './namedColorsPalette';
|
|||||||
// Types
|
// Types
|
||||||
import { GrafanaTheme, GrafanaThemeType } from '../types';
|
import { GrafanaTheme, GrafanaThemeType } from '../types';
|
||||||
|
|
||||||
export type DisplayProcessor = (value: any) => DisplayValue;
|
|
||||||
|
|
||||||
export interface DisplayValueOptions {
|
export interface DisplayValueOptions {
|
||||||
field?: Partial<Field>;
|
field?: FieldConfig;
|
||||||
|
|
||||||
// Alternative to empty string
|
|
||||||
noValue?: string;
|
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
isUtc?: boolean;
|
isUtc?: boolean;
|
||||||
@@ -62,7 +65,11 @@ export function getDisplayProcessor(options?: DisplayValueOptions): DisplayProce
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
text = options.noValue ? options.noValue : '';
|
if (field && field.noValue) {
|
||||||
|
text = field.noValue;
|
||||||
|
} else {
|
||||||
|
text = ''; // No data?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { text, numeric, color };
|
return { text, numeric, color };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
||||||
import { FieldType, ReducerID, Threshold } from '@grafana/data';
|
import { ReducerID, Threshold, DataFrameHelper } from '@grafana/data';
|
||||||
import { GrafanaThemeType } from '../types/theme';
|
import { GrafanaThemeType } from '../types/theme';
|
||||||
import { getTheme } from '../themes/index';
|
import { getTheme } from '../themes/index';
|
||||||
|
|
||||||
@@ -34,19 +34,14 @@ describe('FieldDisplay', () => {
|
|||||||
// Simple test dataset
|
// Simple test dataset
|
||||||
const options: GetFieldDisplayValuesOptions = {
|
const options: GetFieldDisplayValuesOptions = {
|
||||||
data: [
|
data: [
|
||||||
{
|
new DataFrameHelper({
|
||||||
name: 'Series Name',
|
name: 'Series Name',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Field 1', type: FieldType.string },
|
{ name: 'Field 1', values: ['a', 'b', 'c'] },
|
||||||
{ name: 'Field 2', type: FieldType.number },
|
{ name: 'Field 2', values: [1, 3, 5] },
|
||||||
{ name: 'Field 3', type: FieldType.number },
|
{ name: 'Field 3', values: [2, 4, 6] },
|
||||||
],
|
],
|
||||||
rows: [
|
}),
|
||||||
['a', 1, 2], // 0
|
|
||||||
['b', 3, 4], // 1
|
|
||||||
['c', 5, 6], // 2
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
replaceVariables: (value: string) => {
|
replaceVariables: (value: string) => {
|
||||||
return value; // Return it unchanged
|
return value; // Return it unchanged
|
||||||
@@ -140,7 +135,7 @@ describe('FieldDisplay', () => {
|
|||||||
{
|
{
|
||||||
name: 'No data',
|
name: 'No data',
|
||||||
fields: [],
|
fields: [],
|
||||||
rows: [],
|
length: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
replaceVariables: (value: string) => {
|
replaceVariables: (value: string) => {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import {
|
|||||||
ReducerID,
|
ReducerID,
|
||||||
reduceField,
|
reduceField,
|
||||||
FieldType,
|
FieldType,
|
||||||
NullValueMode,
|
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
FieldConfig,
|
||||||
DisplayValue,
|
DisplayValue,
|
||||||
GraphSeriesValue,
|
GraphSeriesValue,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
@@ -21,8 +20,8 @@ export interface FieldDisplayOptions {
|
|||||||
limit?: number; // if showing all values limit
|
limit?: number; // if showing all values limit
|
||||||
calcs: string[]; // when !values, pick one value for the whole field
|
calcs: string[]; // when !values, pick one value for the whole field
|
||||||
|
|
||||||
defaults: Partial<Field>; // Use these values unless otherwise stated
|
defaults: FieldConfig; // Use these values unless otherwise stated
|
||||||
override: Partial<Field>; // Set these values regardless of the source
|
override: FieldConfig; // Set these values regardless of the source
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VAR_SERIES_NAME = '__series_name';
|
export const VAR_SERIES_NAME = '__series_name';
|
||||||
@@ -60,7 +59,8 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldDisplay {
|
export interface FieldDisplay {
|
||||||
field: Field;
|
name: string; // NOT title!
|
||||||
|
field: FieldConfig;
|
||||||
display: DisplayValue;
|
display: DisplayValue;
|
||||||
sparkline?: GraphSeriesValue[][];
|
sparkline?: GraphSeriesValue[][];
|
||||||
}
|
}
|
||||||
@@ -109,45 +109,50 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
||||||
const field = getFieldProperties(defaults, series.fields[i], override);
|
const field = series.fields[i];
|
||||||
|
|
||||||
// Show all number fields
|
// Show all number fields
|
||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const config = getFieldProperties(defaults, field.config || {}, override);
|
||||||
|
|
||||||
if (!field.name) {
|
let name = field.name;
|
||||||
field.name = `Field[${s}]`; // it is a copy, so safe to edit
|
if (!name) {
|
||||||
|
name = `Field[${s}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: field.name };
|
scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: name };
|
||||||
|
|
||||||
const display = getDisplayProcessor({
|
const display = getDisplayProcessor({
|
||||||
field,
|
field: config,
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = field.title ? field.title : defaultTitle;
|
const title = config.title ? config.title : defaultTitle;
|
||||||
|
|
||||||
// Show all number fields
|
// Show all number fields
|
||||||
if (fieldOptions.values) {
|
if (fieldOptions.values) {
|
||||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||||
|
|
||||||
for (const row of series.rows) {
|
for (let j = 0; j < field.values.length; j++) {
|
||||||
// Add all the row variables
|
// Add all the row variables
|
||||||
if (usesCellValues) {
|
if (usesCellValues) {
|
||||||
for (let j = 0; j < series.fields.length; j++) {
|
for (let k = 0; k < series.fields.length; k++) {
|
||||||
scopedVars[VAR_CELL_PREFIX + j] = {
|
const f = series.fields[k];
|
||||||
value: row[j],
|
const v = f.values.get(j);
|
||||||
text: toString(row[j]),
|
scopedVars[VAR_CELL_PREFIX + k] = {
|
||||||
|
value: v,
|
||||||
|
text: toString(v),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayValue = display(row[i]);
|
const displayValue = display(field.values.get(j));
|
||||||
displayValue.title = replaceVariables(title, scopedVars);
|
displayValue.title = replaceVariables(title, scopedVars);
|
||||||
values.push({
|
values.push({
|
||||||
field,
|
name,
|
||||||
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,10 +163,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const results = reduceField({
|
const results = reduceField({
|
||||||
series,
|
field,
|
||||||
fieldIndex: i,
|
|
||||||
reducers: calcs, // The stats to calculate
|
reducers: calcs, // The stats to calculate
|
||||||
nullValueMode: NullValueMode.Null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Single sparkline for a field
|
// Single sparkline for a field
|
||||||
@@ -169,10 +172,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
timeColumn < 0
|
timeColumn < 0
|
||||||
? undefined
|
? undefined
|
||||||
: getFlotPairs({
|
: getFlotPairs({
|
||||||
rows: series.rows,
|
xField: series.fields[timeColumn],
|
||||||
xIndex: timeColumn,
|
yField: series.fields[i],
|
||||||
yIndex: i,
|
|
||||||
nullValueMode: NullValueMode.Null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const calc of calcs) {
|
for (const calc of calcs) {
|
||||||
@@ -180,7 +181,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
const displayValue = display(results[calc]);
|
const displayValue = display(results[calc]);
|
||||||
displayValue.title = replaceVariables(title, scopedVars);
|
displayValue.title = replaceVariables(title, scopedVars);
|
||||||
values.push({
|
values.push({
|
||||||
field,
|
name,
|
||||||
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
sparkline: points,
|
sparkline: points,
|
||||||
});
|
});
|
||||||
@@ -192,9 +194,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
|
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
values.push({
|
values.push({
|
||||||
|
name: 'No data',
|
||||||
field: {
|
field: {
|
||||||
...defaults,
|
...defaults,
|
||||||
name: 'No Data',
|
|
||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
numeric: 0,
|
numeric: 0,
|
||||||
@@ -222,7 +224,7 @@ const numericFieldProps: any = {
|
|||||||
* For numeric values, only valid numbers will be applied
|
* For numeric values, only valid numbers will be applied
|
||||||
* for units, 'none' will be skipped
|
* for units, 'none' will be skipped
|
||||||
*/
|
*/
|
||||||
export function applyFieldProperties(field: Field, props?: Partial<Field>): Field {
|
export function applyFieldProperties(field: FieldConfig, props?: FieldConfig): FieldConfig {
|
||||||
if (!props) {
|
if (!props) {
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
@@ -250,14 +252,11 @@ export function applyFieldProperties(field: Field, props?: Partial<Field>): Fiel
|
|||||||
copy[key] = val;
|
copy[key] = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return copy as Field;
|
return copy as FieldConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartialField = Partial<Field>;
|
export function getFieldProperties(...props: FieldConfig[]): FieldConfig {
|
||||||
|
let field = props[0] as FieldConfig;
|
||||||
export function getFieldProperties(...props: PartialField[]): Field {
|
|
||||||
let field = props[0] as Field;
|
|
||||||
|
|
||||||
for (let i = 1; i < props.length; i++) {
|
for (let i = 1; i < props.length; i++) {
|
||||||
field = applyFieldProperties(field, props[i]);
|
field = applyFieldProperties(field, props[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { getFlotPairs } from './flotPairs';
|
import { getFlotPairs } from './flotPairs';
|
||||||
|
import { DataFrameHelper } from '@grafana/data';
|
||||||
|
|
||||||
describe('getFlotPairs', () => {
|
describe('getFlotPairs', () => {
|
||||||
const rows = [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']];
|
const series = new DataFrameHelper({
|
||||||
|
fields: [
|
||||||
|
{ name: 'a', values: [1, 2, 3] },
|
||||||
|
{ name: 'b', values: [100, 200, 300] },
|
||||||
|
{ name: 'c', values: ['a', 'b', 'c'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
it('should get X and y', () => {
|
it('should get X and y', () => {
|
||||||
const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 1 });
|
const pairs = getFlotPairs({
|
||||||
|
xField: series.fields[0],
|
||||||
|
yField: series.fields[1],
|
||||||
|
});
|
||||||
|
|
||||||
expect(pairs.length).toEqual(3);
|
expect(pairs.length).toEqual(3);
|
||||||
expect(pairs[0].length).toEqual(2);
|
expect(pairs[0].length).toEqual(2);
|
||||||
@@ -13,7 +22,10 @@ describe('getFlotPairs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should work with strings', () => {
|
it('should work with strings', () => {
|
||||||
const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 2 });
|
const pairs = getFlotPairs({
|
||||||
|
xField: series.fields[0],
|
||||||
|
yField: series.fields[2],
|
||||||
|
});
|
||||||
|
|
||||||
expect(pairs.length).toEqual(3);
|
expect(pairs.length).toEqual(3);
|
||||||
expect(pairs[0].length).toEqual(2);
|
expect(pairs[0].length).toEqual(2);
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
// Types
|
// Types
|
||||||
import { NullValueMode, GraphSeriesValue } from '@grafana/data';
|
import { NullValueMode, GraphSeriesValue, Field } from '@grafana/data';
|
||||||
|
|
||||||
export interface FlotPairsOptions {
|
export interface FlotPairsOptions {
|
||||||
rows: any[][];
|
xField: Field;
|
||||||
xIndex: number;
|
yField: Field;
|
||||||
yIndex: number;
|
|
||||||
nullValueMode?: NullValueMode;
|
nullValueMode?: NullValueMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFlotPairs({ rows, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] {
|
export function getFlotPairs({ xField, yField, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] {
|
||||||
|
const vX = xField.values;
|
||||||
|
const vY = yField.values;
|
||||||
|
const length = vX.length;
|
||||||
|
if (vY.length !== length) {
|
||||||
|
throw new Error('Unexpected field length');
|
||||||
|
}
|
||||||
|
|
||||||
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
|
||||||
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
const nullAsZero = nullValueMode === NullValueMode.AsZero;
|
||||||
|
|
||||||
const pairs: any[][] = [];
|
const pairs: any[][] = [];
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
const x = rows[i][xIndex];
|
const x = vX.get(i);
|
||||||
let y = rows[i][yIndex];
|
let y = vY.get(i);
|
||||||
|
|
||||||
if (y === null) {
|
if (y === null) {
|
||||||
if (ignoreNulls) {
|
if (ignoreNulls) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
findCommonLabels,
|
findCommonLabels,
|
||||||
findUniqueLabels,
|
findUniqueLabels,
|
||||||
getLogLevel,
|
getLogLevel,
|
||||||
FieldCache,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
getLogLevelFromKey,
|
getLogLevelFromKey,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
@@ -20,11 +19,13 @@ import {
|
|||||||
LogsParser,
|
LogsParser,
|
||||||
LogLabelStatsModel,
|
LogLabelStatsModel,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
|
DataFrameHelper,
|
||||||
GraphSeriesXY,
|
GraphSeriesXY,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
dateTime,
|
dateTime,
|
||||||
toUtc,
|
toUtc,
|
||||||
NullValueMode,
|
NullValueMode,
|
||||||
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getThemeColor } from 'app/core/utils/colors';
|
import { getThemeColor } from 'app/core/utils/colors';
|
||||||
import { hasAnsiCodes } from 'app/core/utils/text';
|
import { hasAnsiCodes } from 'app/core/utils/text';
|
||||||
@@ -245,10 +246,11 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Grap
|
|||||||
return a[1] - b[1];
|
return a[1] - b[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// EEEP: converts GraphSeriesXY to DataFrame and back again!
|
||||||
|
const data = toDataFrame(series);
|
||||||
const points = getFlotPairs({
|
const points = getFlotPairs({
|
||||||
rows: series.datapoints,
|
xField: data.fields[1],
|
||||||
xIndex: 1,
|
yField: data.fields[0],
|
||||||
yIndex: 0,
|
|
||||||
nullValueMode: NullValueMode.Null,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,14 +338,56 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
|||||||
|
|
||||||
for (let i = 0; i < logSeries.length; i++) {
|
for (let i = 0; i < logSeries.length; i++) {
|
||||||
const series = logSeries[i];
|
const series = logSeries[i];
|
||||||
const fieldCache = new FieldCache(series.fields);
|
const data = new DataFrameHelper(series);
|
||||||
const uniqueLabels = findUniqueLabels(series.labels, commonLabels);
|
const uniqueLabels = findUniqueLabels(series.labels, commonLabels);
|
||||||
if (Object.keys(uniqueLabels).length > 0) {
|
if (Object.keys(uniqueLabels).length > 0) {
|
||||||
hasUniqueLabels = true;
|
hasUniqueLabels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = 0; j < series.rows.length; j++) {
|
const timeFieldIndex = data.getFirstFieldOfType(FieldType.time);
|
||||||
rows.push(processLogSeriesRow(series, fieldCache, j, uniqueLabels));
|
const stringField = data.getFirstFieldOfType(FieldType.string);
|
||||||
|
const logLevelField = data.getFieldByName('level');
|
||||||
|
|
||||||
|
let seriesLogLevel: LogLevel | undefined = undefined;
|
||||||
|
if (series.labels && Object.keys(series.labels).indexOf('level') !== -1) {
|
||||||
|
seriesLogLevel = getLogLevelFromKey(series.labels['level']);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < data.length; j++) {
|
||||||
|
const ts = timeFieldIndex.values.get(j);
|
||||||
|
const time = dateTime(ts);
|
||||||
|
const timeEpochMs = time.valueOf();
|
||||||
|
const timeFromNow = time.fromNow();
|
||||||
|
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
const timeUtc = toUtc(ts).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
|
||||||
|
const message = stringField.values.get(j);
|
||||||
|
|
||||||
|
let logLevel = LogLevel.unknown;
|
||||||
|
if (logLevelField) {
|
||||||
|
logLevel = getLogLevelFromKey(logLevelField.values.get(j));
|
||||||
|
} else if (seriesLogLevel) {
|
||||||
|
logLevel = seriesLogLevel;
|
||||||
|
} else {
|
||||||
|
logLevel = getLogLevel(message);
|
||||||
|
}
|
||||||
|
const hasAnsi = hasAnsiCodes(message);
|
||||||
|
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
logLevel,
|
||||||
|
timeFromNow,
|
||||||
|
timeEpochMs,
|
||||||
|
timeLocal,
|
||||||
|
timeUtc,
|
||||||
|
uniqueLabels,
|
||||||
|
hasAnsi,
|
||||||
|
searchWords,
|
||||||
|
entry: hasAnsi ? ansicolor.strip(message) : message,
|
||||||
|
raw: message,
|
||||||
|
labels: series.labels,
|
||||||
|
timestamp: ts,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,49 +417,3 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel {
|
|||||||
rows,
|
rows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processLogSeriesRow(
|
|
||||||
series: DataFrame,
|
|
||||||
fieldCache: FieldCache,
|
|
||||||
rowIndex: number,
|
|
||||||
uniqueLabels: Labels
|
|
||||||
): LogRowModel {
|
|
||||||
const row = series.rows[rowIndex];
|
|
||||||
const timeFieldIndex = fieldCache.getFirstFieldOfType(FieldType.time).index;
|
|
||||||
const ts = row[timeFieldIndex];
|
|
||||||
const stringFieldIndex = fieldCache.getFirstFieldOfType(FieldType.string).index;
|
|
||||||
const message = row[stringFieldIndex];
|
|
||||||
const time = dateTime(ts);
|
|
||||||
const timeEpochMs = time.valueOf();
|
|
||||||
const timeFromNow = time.fromNow();
|
|
||||||
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
const timeUtc = toUtc(ts).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
|
|
||||||
let logLevel = LogLevel.unknown;
|
|
||||||
const logLevelField = fieldCache.getFieldByName('level');
|
|
||||||
|
|
||||||
if (logLevelField) {
|
|
||||||
logLevel = getLogLevelFromKey(row[logLevelField.index]);
|
|
||||||
} else if (series.labels && Object.keys(series.labels).indexOf('level') !== -1) {
|
|
||||||
logLevel = getLogLevelFromKey(series.labels['level']);
|
|
||||||
} else {
|
|
||||||
logLevel = getLogLevel(message);
|
|
||||||
}
|
|
||||||
const hasAnsi = hasAnsiCodes(message);
|
|
||||||
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
logLevel,
|
|
||||||
timeFromNow,
|
|
||||||
timeEpochMs,
|
|
||||||
timeLocal,
|
|
||||||
timeUtc,
|
|
||||||
uniqueLabels,
|
|
||||||
hasAnsi,
|
|
||||||
searchWords,
|
|
||||||
entry: hasAnsi ? ansicolor.strip(message) : message,
|
|
||||||
raw: message,
|
|
||||||
labels: series.labels,
|
|
||||||
timestamp: ts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { DataFrame, FieldType, LogsModel, LogsMetaKind, LogsDedupStrategy, LogLevel } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
FieldType,
|
||||||
|
LogsModel,
|
||||||
|
LogsMetaKind,
|
||||||
|
LogsDedupStrategy,
|
||||||
|
LogLevel,
|
||||||
|
DataFrameHelper,
|
||||||
|
toDataFrame,
|
||||||
|
} from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
dedupLogRows,
|
dedupLogRows,
|
||||||
calculateFieldStats,
|
calculateFieldStats,
|
||||||
@@ -344,47 +353,46 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
|
|
||||||
it('given series without correct series name should return empty logs model', () => {
|
it('given series without correct series name should return empty logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
toDataFrame({
|
||||||
fields: [],
|
fields: [],
|
||||||
rows: [],
|
}),
|
||||||
},
|
|
||||||
];
|
];
|
||||||
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('given series without a time field should return empty logs model', () => {
|
it('given series without a time field should return empty logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
new DataFrameHelper({
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
values: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [],
|
}),
|
||||||
},
|
|
||||||
];
|
];
|
||||||
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('given series without a string field should return empty logs model', () => {
|
it('given series without a string field should return empty logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
new DataFrameHelper({
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
values: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [],
|
}),
|
||||||
},
|
|
||||||
];
|
];
|
||||||
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('given one series should return expected logs model', () => {
|
it('given one series should return expected logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
new DataFrameHelper({
|
||||||
labels: {
|
labels: {
|
||||||
filename: '/var/log/grafana/grafana.log',
|
filename: '/var/log/grafana/grafana.log',
|
||||||
job: 'grafana',
|
job: 'grafana',
|
||||||
@@ -393,26 +401,21 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
},
|
values: [
|
||||||
],
|
|
||||||
rows: [
|
|
||||||
[
|
|
||||||
'2019-04-26T09:28:11.352440161Z',
|
|
||||||
't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
|
't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
|
||||||
],
|
|
||||||
[
|
|
||||||
'2019-04-26T14:42:50.991981292Z',
|
|
||||||
't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
|
't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
|
||||||
],
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
];
|
];
|
||||||
const logsModel = dataFrameToLogsModel(series, 0);
|
const logsModel = dataFrameToLogsModel(series, 0);
|
||||||
expect(logsModel.hasUniqueLabels).toBeFalsy();
|
expect(logsModel.hasUniqueLabels).toBeFalsy();
|
||||||
@@ -450,23 +453,25 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
|
|
||||||
it('given one series without labels should return expected logs model', () => {
|
it('given one series without labels should return expected logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
new DataFrameHelper({
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
values: ['1970-01-01T00:00:01Z'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
values: ['WARN boooo'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'level',
|
name: 'level',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
values: ['dbug'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [['1970-01-01T00:00:01Z', 'WARN boooo', 'dbug']],
|
}),
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const logsModel = dataFrameToLogsModel(series, 0);
|
const logsModel = dataFrameToLogsModel(series, 0);
|
||||||
expect(logsModel.rows).toHaveLength(1);
|
expect(logsModel.rows).toHaveLength(1);
|
||||||
@@ -482,7 +487,7 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
|
|
||||||
it('given multiple series should return expected logs model', () => {
|
it('given multiple series should return expected logs model', () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
toDataFrame({
|
||||||
labels: {
|
labels: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
baz: '1',
|
baz: '1',
|
||||||
@@ -492,15 +497,16 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
{
|
{
|
||||||
name: 'ts',
|
name: 'ts',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
values: ['1970-01-01T00:00:01Z'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'line',
|
name: 'line',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
values: ['WARN boooo'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [['1970-01-01T00:00:01Z', 'WARN boooo']],
|
}),
|
||||||
},
|
toDataFrame({
|
||||||
{
|
|
||||||
name: 'logs',
|
name: 'logs',
|
||||||
labels: {
|
labels: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
@@ -511,14 +517,15 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
values: ['1970-01-01T00:00:00Z', '1970-01-01T00:00:02Z'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
|
values: ['INFO 1', 'INFO 2'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rows: [['1970-01-01T00:00:00Z', 'INFO 1'], ['1970-01-01T00:00:02Z', 'INFO 2']],
|
}),
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const logsModel = dataFrameToLogsModel(series, 0);
|
const logsModel = dataFrameToLogsModel(series, 0);
|
||||||
expect(logsModel.hasUniqueLabels).toBeTruthy();
|
expect(logsModel.hasUniqueLabels).toBeTruthy();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState, toDataFrame } from '@grafana/data';
|
||||||
import { PanelData, DataQueryRequest } from '@grafana/ui';
|
import { PanelData, DataQueryRequest } from '@grafana/ui';
|
||||||
import { filterPanelDataToQuery } from './QueryEditorRow';
|
import { filterPanelDataToQuery } from './QueryEditorRow';
|
||||||
|
|
||||||
@@ -10,14 +10,14 @@ function makePretendRequest(requestId: string, subRequests?: DataQueryRequest[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('filterPanelDataToQuery', () => {
|
describe('filterPanelDataToQuery', () => {
|
||||||
const data = {
|
const data: PanelData = {
|
||||||
state: LoadingState.Done,
|
state: LoadingState.Done,
|
||||||
series: [
|
series: [
|
||||||
{ refId: 'A', fields: [{ name: 'AAA' }], rows: [], meta: {} },
|
toDataFrame({ refId: 'A', fields: [{ name: 'AAA' }], meta: {} }),
|
||||||
{ refId: 'B', fields: [{ name: 'B111' }], rows: [], meta: {} },
|
toDataFrame({ refId: 'B', fields: [{ name: 'B111' }], meta: {} }),
|
||||||
{ refId: 'B', fields: [{ name: 'B222' }], rows: [], meta: {} },
|
toDataFrame({ refId: 'B', fields: [{ name: 'B222' }], meta: {} }),
|
||||||
{ refId: 'B', fields: [{ name: 'B333' }], rows: [], meta: {} },
|
toDataFrame({ refId: 'B', fields: [{ name: 'B333' }], meta: {} }),
|
||||||
{ refId: 'C', fields: [{ name: 'CCCC' }], rows: [], meta: { requestId: 'sub3' } },
|
toDataFrame({ refId: 'C', fields: [{ name: 'CCCC' }], meta: { requestId: 'sub3' } }),
|
||||||
],
|
],
|
||||||
error: {
|
error: {
|
||||||
refId: 'B',
|
refId: 'B',
|
||||||
@@ -28,7 +28,7 @@ describe('filterPanelDataToQuery', () => {
|
|||||||
makePretendRequest('sub2'),
|
makePretendRequest('sub2'),
|
||||||
makePretendRequest('sub3'),
|
makePretendRequest('sub3'),
|
||||||
]),
|
]),
|
||||||
} as PanelData;
|
};
|
||||||
|
|
||||||
it('should not have an error unless the refId matches', () => {
|
it('should not have an error unless the refId matches', () => {
|
||||||
const panelData = filterPanelDataToQuery(data, 'A');
|
const panelData = filterPanelDataToQuery(data, 'A');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PanelQueryRunner } from './PanelQueryRunner';
|
import { PanelQueryRunner } from './PanelQueryRunner';
|
||||||
import { PanelData, DataQueryRequest, DataStreamObserver, DataStreamState, ScopedVars } from '@grafana/ui';
|
import { PanelData, DataQueryRequest, DataStreamObserver, DataStreamState, ScopedVars } from '@grafana/ui';
|
||||||
|
|
||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState, DataFrameHelper } from '@grafana/data';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTime } from '@grafana/data';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
@@ -169,11 +169,10 @@ describe('PanelQueryRunner', () => {
|
|||||||
state: LoadingState.Streaming,
|
state: LoadingState.Streaming,
|
||||||
key: 'test-stream-1',
|
key: 'test-stream-1',
|
||||||
data: [
|
data: [
|
||||||
{
|
new DataFrameHelper({
|
||||||
rows: [],
|
|
||||||
fields: [],
|
fields: [],
|
||||||
name: 'I am a magic stream',
|
name: 'I am a magic stream',
|
||||||
},
|
}),
|
||||||
],
|
],
|
||||||
request: {
|
request: {
|
||||||
requestId: ctx.queryCalledWith.requestId,
|
requestId: ctx.queryCalledWith.requestId,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { toDataQueryError, PanelQueryState, getProcessedDataFrames } from './PanelQueryState';
|
import { toDataQueryError, PanelQueryState, getProcessedDataFrames } from './PanelQueryState';
|
||||||
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
|
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
|
||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState, getDataFrameRow } from '@grafana/data';
|
||||||
import { DataQueryResponse } from '@grafana/ui';
|
import { DataQueryResponse } from '@grafana/ui';
|
||||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||||
|
|
||||||
@@ -68,16 +68,18 @@ describe('getProcessedDataFrame', () => {
|
|||||||
const data = getProcessedDataFrames([null, input1, input2, null, null]);
|
const data = getProcessedDataFrames([null, input1, input2, null, null]);
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data[0].fields[0].name).toBe(input1.target);
|
expect(data[0].fields[0].name).toBe(input1.target);
|
||||||
expect(data[0].rows).toBe(input1.datapoints);
|
|
||||||
|
const cmp = [getDataFrameRow(data[0], 0), getDataFrameRow(data[0], 1)];
|
||||||
|
expect(cmp).toEqual(input1.datapoints);
|
||||||
|
|
||||||
// Default name
|
// Default name
|
||||||
expect(data[1].fields[0].name).toEqual('Value');
|
expect(data[1].fields[0].name).toEqual('Value');
|
||||||
|
|
||||||
// Every colun should have a name and a type
|
// Every colun should have a name and a type
|
||||||
for (const table of data) {
|
for (const table of data) {
|
||||||
for (const column of table.fields) {
|
for (const field of table.fields) {
|
||||||
expect(column.name).toBeDefined();
|
expect(field.name).toBeDefined();
|
||||||
expect(column.type).toBeDefined();
|
expect(field.type).toBeDefined();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -92,8 +94,7 @@ describe('getProcessedDataFrame', () => {
|
|||||||
|
|
||||||
function makeSeriesStub(refId: string) {
|
function makeSeriesStub(refId: string) {
|
||||||
return {
|
return {
|
||||||
fields: [{ name: 'a' }],
|
fields: [{ name: undefined }],
|
||||||
rows: [],
|
|
||||||
refId,
|
refId,
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,19 @@ import {
|
|||||||
updateTimeRangeAction,
|
updateTimeRangeAction,
|
||||||
runQueriesAction,
|
runQueriesAction,
|
||||||
} from '../actionTypes';
|
} from '../actionTypes';
|
||||||
import { DataFrame, LoadingState } from '@grafana/data';
|
import { DataFrame, LoadingState, toDataFrame } from '@grafana/data';
|
||||||
import { processQueryResultsEpic } from './processQueryResultsEpic';
|
import { processQueryResultsEpic } from './processQueryResultsEpic';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
const testContext = () => {
|
const testContext = () => {
|
||||||
const serieA: DataFrame = {
|
const serieA: DataFrame = toDataFrame({
|
||||||
fields: [],
|
fields: [],
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
rows: [],
|
});
|
||||||
};
|
const serieB: DataFrame = toDataFrame({
|
||||||
const serieB: DataFrame = {
|
|
||||||
fields: [],
|
fields: [],
|
||||||
refId: 'B',
|
refId: 'B',
|
||||||
rows: [],
|
});
|
||||||
};
|
|
||||||
const series = [serieA, serieB];
|
const series = [serieA, serieB];
|
||||||
const latency = 0;
|
const latency = 0;
|
||||||
const loadingState = LoadingState.Done;
|
const loadingState = LoadingState.Done;
|
||||||
|
|||||||
@@ -14,25 +14,28 @@ import {
|
|||||||
clearQueriesAction,
|
clearQueriesAction,
|
||||||
stateSaveAction,
|
stateSaveAction,
|
||||||
} from '../actionTypes';
|
} from '../actionTypes';
|
||||||
import { LoadingState, DataFrame, FieldType } from '@grafana/data';
|
import { LoadingState, DataFrame, FieldType, DataFrameHelper } from '@grafana/data';
|
||||||
import { DataQueryRequest } from '@grafana/ui';
|
import { DataQueryRequest } from '@grafana/ui';
|
||||||
|
|
||||||
const testContext = () => {
|
const testContext = () => {
|
||||||
const series: DataFrame[] = [
|
const series: DataFrame[] = [
|
||||||
{
|
new DataFrameHelper({
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'Value',
|
name: 'Value',
|
||||||
|
values: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Time',
|
name: 'Time',
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
|
config: {
|
||||||
unit: 'dateTimeAsIso',
|
unit: 'dateTimeAsIso',
|
||||||
},
|
},
|
||||||
],
|
values: [],
|
||||||
rows: [],
|
|
||||||
refId: 'A',
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
refId: 'A',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
const response = { data: series };
|
const response = { data: series };
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||||||
import flatten from 'app/core/utils/flatten';
|
import flatten from 'app/core/utils/flatten';
|
||||||
import * as queryDef from './query_def';
|
import * as queryDef from './query_def';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { DataFrame, toDataFrame, FieldType } from '@grafana/data';
|
import { DataFrame, toDataFrame, FieldType, DataFrameHelper } from '@grafana/data';
|
||||||
import { DataQueryResponse } from '@grafana/ui';
|
import { DataQueryResponse } from '@grafana/ui';
|
||||||
import { ElasticsearchAggregation } from './types';
|
import { ElasticsearchAggregation } from './types';
|
||||||
|
|
||||||
@@ -464,33 +464,38 @@ export class ElasticResponse {
|
|||||||
|
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
propNames = propNames.sort();
|
propNames = propNames.sort();
|
||||||
const series: DataFrame = {
|
const series = new DataFrameHelper({ fields: [] });
|
||||||
fields: [
|
|
||||||
{
|
series.addField({
|
||||||
name: this.targets[0].timeField,
|
name: this.targets[0].timeField,
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
},
|
}).parse = (v: any) => {
|
||||||
],
|
return v[0] || '';
|
||||||
rows: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (logMessageField) {
|
if (logMessageField) {
|
||||||
series.fields.push({
|
series.addField({
|
||||||
name: logMessageField,
|
name: logMessageField,
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
});
|
}).parse = (v: any) => {
|
||||||
|
return v || '';
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
series.fields.push({
|
series.addField({
|
||||||
name: '_source',
|
name: '_source',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
});
|
}).parse = (v: any) => {
|
||||||
|
return JSON.stringify(v, null, 2);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logLevelField) {
|
if (logLevelField) {
|
||||||
series.fields.push({
|
series.addField({
|
||||||
name: 'level',
|
name: 'level',
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
});
|
}).parse = (v: any) => {
|
||||||
|
return v || '';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const propName of propNames) {
|
for (const propName of propNames) {
|
||||||
@@ -498,35 +503,17 @@ export class ElasticResponse {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
series.fields.push({
|
series.addField({
|
||||||
name: propName,
|
name: propName,
|
||||||
type: FieldType.string,
|
type: FieldType.string,
|
||||||
});
|
}).parse = (v: any) => {
|
||||||
|
return v || '';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a row for each document
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const row: any[] = [];
|
series.appendRowFrom(doc);
|
||||||
row.push(doc[this.targets[0].timeField][0]);
|
|
||||||
|
|
||||||
if (logMessageField) {
|
|
||||||
row.push(doc[logMessageField] || '');
|
|
||||||
} else {
|
|
||||||
row.push(JSON.stringify(doc._source, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logLevelField) {
|
|
||||||
row.push(doc[logLevelField] || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const propName of propNames) {
|
|
||||||
if (doc.hasOwnProperty(propName)) {
|
|
||||||
row.push(doc[propName]);
|
|
||||||
} else {
|
|
||||||
row.push(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series.rows.push(row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataFrame.push(series);
|
dataFrame.push(series);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ElasticResponse } from '../elastic_response';
|
import { ElasticResponse } from '../elastic_response';
|
||||||
|
import { DataFrameHelper, DataFrameView } from '@grafana/data';
|
||||||
|
import { KeyValue } from '@grafana/ui';
|
||||||
|
|
||||||
describe('ElasticResponse', () => {
|
describe('ElasticResponse', () => {
|
||||||
let targets;
|
let targets;
|
||||||
@@ -858,19 +860,39 @@ describe('ElasticResponse', () => {
|
|||||||
|
|
||||||
it('should return histogram aggregation and documents', () => {
|
it('should return histogram aggregation and documents', () => {
|
||||||
expect(result.data.length).toBe(2);
|
expect(result.data.length).toBe(2);
|
||||||
expect(result.data[0].fields).toContainEqual({ name: '@timestamp', type: 'time' });
|
const logResults = result.data[0] as DataFrameHelper;
|
||||||
expect(result.data[0].fields).toContainEqual({ name: 'host', type: 'string' });
|
const fields = logResults.fields.map(f => {
|
||||||
expect(result.data[0].fields).toContainEqual({ name: 'message', type: 'string' });
|
return {
|
||||||
result.data[0].rows.forEach((row: any, i: number) => {
|
name: f.name,
|
||||||
|
type: f.type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fields).toContainEqual({ name: '@timestamp', type: 'time' });
|
||||||
|
expect(fields).toContainEqual({ name: 'host', type: 'string' });
|
||||||
|
expect(fields).toContainEqual({ name: 'message', type: 'string' });
|
||||||
|
|
||||||
|
let rows = new DataFrameView(logResults);
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const r = rows.get(i);
|
||||||
|
const row = [r._id, r._type, r._index, r._source];
|
||||||
expect(row).toContain(response.responses[0].hits.hits[i]._id);
|
expect(row).toContain(response.responses[0].hits.hits[i]._id);
|
||||||
expect(row).toContain(response.responses[0].hits.hits[i]._type);
|
expect(row).toContain(response.responses[0].hits.hits[i]._type);
|
||||||
expect(row).toContain(response.responses[0].hits.hits[i]._index);
|
expect(row).toContain(response.responses[0].hits.hits[i]._index);
|
||||||
expect(row).toContain(JSON.stringify(response.responses[0].hits.hits[i]._source, undefined, 2));
|
expect(row).toContain(JSON.stringify(response.responses[0].hits.hits[i]._source, undefined, 2));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Make a map from the histogram results
|
||||||
|
const hist: KeyValue<number> = {};
|
||||||
|
const histogramResults = new DataFrameHelper(result.data[1]);
|
||||||
|
rows = new DataFrameView(histogramResults);
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows.get(i);
|
||||||
|
hist[row.Time] = row.Count;
|
||||||
|
}
|
||||||
|
|
||||||
expect(result.data[1]).toHaveProperty('name', 'Count');
|
|
||||||
response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => {
|
response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => {
|
||||||
expect(result.data[1].rows).toContainEqual([bucket.doc_count, bucket.key]);
|
expect(hist[bucket.key]).toEqual(bucket.doc_count);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { toUtc } from '@grafana/data';
|
import { toUtc, DataFrame } from '@grafana/data';
|
||||||
|
|
||||||
describe('AzureMonitorDatasource', () => {
|
describe('AzureMonitorDatasource', () => {
|
||||||
const ctx: any = {
|
const ctx: any = {
|
||||||
@@ -132,11 +132,12 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
it('should return a list of datapoints', () => {
|
it('should return a list of datapoints', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
expect(results.data[0].name).toEqual('Percentage CPU');
|
const data = results.data[0] as DataFrame;
|
||||||
expect(results.data[0].rows[0][1]).toEqual(1558278660000);
|
expect(data.name).toEqual('Percentage CPU');
|
||||||
expect(results.data[0].rows[0][0]).toEqual(2.2075);
|
expect(data.fields[1].values.get(0)).toEqual(1558278660000);
|
||||||
expect(results.data[0].rows[1][1]).toEqual(1558278720000);
|
expect(data.fields[0].values.get(0)).toEqual(2.2075);
|
||||||
expect(results.data[0].rows[1][0]).toEqual(2.29);
|
expect(data.fields[1].values.get(1)).toEqual(1558278720000);
|
||||||
|
expect(data.fields[0].values.get(1)).toEqual(2.29);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import React, { PureComponent } from 'react';
|
|||||||
import { InputOptions } from './types';
|
import { InputOptions } from './types';
|
||||||
|
|
||||||
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, TableInputCSV } from '@grafana/ui';
|
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, TableInputCSV } from '@grafana/ui';
|
||||||
import { DataFrame, toCSV } from '@grafana/data';
|
import { DataFrame, DataFrameHelper } from '@grafana/data';
|
||||||
|
import { dataFrameToCSV } from './utils';
|
||||||
|
|
||||||
type InputSettings = DataSourceSettings<InputOptions>;
|
type InputSettings = DataSourceSettings<InputOptions>;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export class InputConfigEditor extends PureComponent<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
if (options.jsonData.data) {
|
if (options.jsonData.data) {
|
||||||
const text = toCSV(options.jsonData.data);
|
const text = dataFrameToCSV(options.jsonData.data);
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,12 +32,7 @@ export class InputConfigEditor extends PureComponent<Props, State> {
|
|||||||
onSeriesParsed = (data: DataFrame[], text: string) => {
|
onSeriesParsed = (data: DataFrame[], text: string) => {
|
||||||
const { options, onOptionsChange } = this.props;
|
const { options, onOptionsChange } = this.props;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = [
|
data = [new DataFrameHelper()];
|
||||||
{
|
|
||||||
fields: [],
|
|
||||||
rows: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
// data is a property on 'jsonData'
|
// data is a property on 'jsonData'
|
||||||
const jsonData = {
|
const jsonData = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import InputDatasource, { describeDataFrame } from './InputDatasource';
|
import InputDatasource, { describeDataFrame } from './InputDatasource';
|
||||||
import { InputQuery, InputOptions } from './types';
|
import { InputQuery, InputOptions } from './types';
|
||||||
import { readCSV } from '@grafana/data';
|
import { readCSV, DataFrame, DataFrameHelper } from '@grafana/data';
|
||||||
import { DataSourceInstanceSettings, PluginMeta } from '@grafana/ui';
|
import { DataSourceInstanceSettings, PluginMeta } from '@grafana/ui';
|
||||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ describe('InputDatasource', () => {
|
|||||||
return ds.query(options).then(rsp => {
|
return ds.query(options).then(rsp => {
|
||||||
expect(rsp.data.length).toBe(1);
|
expect(rsp.data.length).toBe(1);
|
||||||
|
|
||||||
const series = rsp.data[0];
|
const series: DataFrame = rsp.data[0];
|
||||||
expect(series.refId).toBe('Z');
|
expect(series.refId).toBe('Z');
|
||||||
expect(series.rows).toEqual(data[0].rows);
|
expect(series.fields[0].values).toEqual(data[0].fields[0].values);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -38,11 +38,10 @@ describe('InputDatasource', () => {
|
|||||||
expect(describeDataFrame(null)).toEqual('');
|
expect(describeDataFrame(null)).toEqual('');
|
||||||
expect(
|
expect(
|
||||||
describeDataFrame([
|
describeDataFrame([
|
||||||
{
|
new DataFrameHelper({
|
||||||
name: 'x',
|
name: 'x',
|
||||||
fields: [{ name: 'a' }],
|
fields: [{ name: 'a' }],
|
||||||
rows: [],
|
}),
|
||||||
},
|
|
||||||
])
|
])
|
||||||
).toEqual('1 Fields, 0 Rows');
|
).toEqual('1 Fields, 0 Rows');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ import {
|
|||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame, DataFrameDTO, toDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { InputQuery, InputOptions } from './types';
|
import { InputQuery, InputOptions } from './types';
|
||||||
|
|
||||||
export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
||||||
data: DataFrame[];
|
data: DataFrame[] = [];
|
||||||
|
|
||||||
constructor(instanceSettings: DataSourceInstanceSettings<InputOptions>) {
|
constructor(instanceSettings: DataSourceInstanceSettings<InputOptions>) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
|
|
||||||
this.data = instanceSettings.jsonData.data ? instanceSettings.jsonData.data : [];
|
if (instanceSettings.jsonData.data) {
|
||||||
|
this.data = instanceSettings.jsonData.data.map(v => toDataFrame(v));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,14 +49,14 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
|||||||
query(options: DataQueryRequest<InputQuery>): Promise<DataQueryResponse> {
|
query(options: DataQueryRequest<InputQuery>): Promise<DataQueryResponse> {
|
||||||
const results: DataFrame[] = [];
|
const results: DataFrame[] = [];
|
||||||
for (const query of options.targets) {
|
for (const query of options.targets) {
|
||||||
if (query.hide) {
|
let data = this.data;
|
||||||
continue;
|
if (query.data) {
|
||||||
|
data = query.data.map(v => toDataFrame(v));
|
||||||
}
|
}
|
||||||
const data = query.data ? query.data : this.data;
|
for (let i = 0; i < data.length; i++) {
|
||||||
for (const series of data) {
|
|
||||||
results.push({
|
results.push({
|
||||||
|
...data[i],
|
||||||
refId: query.refId,
|
refId: query.refId,
|
||||||
...series,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,8 +68,9 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
|||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
let info = `${this.data.length} Series:`;
|
let info = `${this.data.length} Series:`;
|
||||||
for (const series of this.data) {
|
for (const series of this.data) {
|
||||||
info += ` [${series.fields.length} Fields, ${series.rows.length} Rows]`;
|
const length = series.length;
|
||||||
rowCount += series.rows.length;
|
info += ` [${series.fields.length} Fields, ${length} Rows]`;
|
||||||
|
rowCount += length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowCount > 0) {
|
if (rowCount > 0) {
|
||||||
@@ -84,13 +87,23 @@ export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeDataFrame(data: DataFrame[]): string {
|
function getLength(data?: DataFrameDTO | DataFrame) {
|
||||||
|
if (!data || !data.fields || !data.fields.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (data.hasOwnProperty('length')) {
|
||||||
|
return (data as DataFrame).length;
|
||||||
|
}
|
||||||
|
return data.fields[0].values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describeDataFrame(data: Array<DataFrameDTO | DataFrame>): string {
|
||||||
if (!data || !data.length) {
|
if (!data || !data.length) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (data.length > 1) {
|
if (data.length > 1) {
|
||||||
const count = data.reduce((acc, series) => {
|
const count = data.reduce((acc, series) => {
|
||||||
return acc + series.rows.length;
|
return acc + getLength(series);
|
||||||
}, 0);
|
}, 0);
|
||||||
return `${data.length} Series, ${count} Rows`;
|
return `${data.length} Series, ${count} Rows`;
|
||||||
}
|
}
|
||||||
@@ -98,7 +111,8 @@ export function describeDataFrame(data: DataFrame[]): string {
|
|||||||
if (!series.fields) {
|
if (!series.fields) {
|
||||||
return 'Missing Fields';
|
return 'Missing Fields';
|
||||||
}
|
}
|
||||||
return `${series.fields.length} Fields, ${series.rows.length} Rows`;
|
const length = getLength(series);
|
||||||
|
return `${series.fields.length} Fields, ${length} Rows`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InputDatasource;
|
export default InputDatasource;
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { InputDatasource, describeDataFrame } from './InputDatasource';
|
|||||||
import { InputQuery, InputOptions } from './types';
|
import { InputQuery, InputOptions } from './types';
|
||||||
|
|
||||||
import { FormLabel, Select, QueryEditorProps, TableInputCSV } from '@grafana/ui';
|
import { FormLabel, Select, QueryEditorProps, TableInputCSV } from '@grafana/ui';
|
||||||
import { DataFrame, toCSV, SelectableValue } from '@grafana/data';
|
import { DataFrame, toCSV, SelectableValue, DataFrameHelper } from '@grafana/data';
|
||||||
|
|
||||||
|
import { dataFrameToCSV } from './utils';
|
||||||
|
|
||||||
type Props = QueryEditorProps<InputDatasource, InputQuery, InputOptions>;
|
type Props = QueryEditorProps<InputDatasource, InputQuery, InputOptions>;
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onComponentDidMount() {
|
onComponentDidMount() {
|
||||||
const { query } = this.props;
|
const { query } = this.props;
|
||||||
const text = query.data ? toCSV(query.data) : '';
|
const text = dataFrameToCSV(query.data);
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +41,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
data = [...datasource.data];
|
data = [...datasource.data];
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = [
|
data = [new DataFrameHelper()];
|
||||||
{
|
|
||||||
fields: [],
|
|
||||||
rows: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
this.setState({ text: toCSV(data) });
|
this.setState({ text: toCSV(data) });
|
||||||
}
|
}
|
||||||
@@ -56,12 +53,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
|
|||||||
const { query, onChange, onRunQuery } = this.props;
|
const { query, onChange, onRunQuery } = this.props;
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = [
|
data = [new DataFrameHelper()];
|
||||||
{
|
|
||||||
fields: [],
|
|
||||||
rows: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
onChange({ ...query, data });
|
onChange({ ...query, data });
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DataQuery, DataSourceJsonData } from '@grafana/ui';
|
import { DataQuery, DataSourceJsonData } from '@grafana/ui';
|
||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrameDTO } from '@grafana/data';
|
||||||
|
|
||||||
export interface InputQuery extends DataQuery {
|
export interface InputQuery extends DataQuery {
|
||||||
// Data saved in the panel
|
// Data saved in the panel
|
||||||
data?: DataFrame[];
|
data?: DataFrameDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InputOptions extends DataSourceJsonData {
|
export interface InputOptions extends DataSourceJsonData {
|
||||||
// Saved in the datasource and download with bootData
|
// Saved in the datasource and download with bootData
|
||||||
data?: DataFrame[];
|
data?: DataFrameDTO[];
|
||||||
}
|
}
|
||||||
|
|||||||
8
public/app/plugins/datasource/input/utils.ts
Normal file
8
public/app/plugins/datasource/input/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { toDataFrame, DataFrameDTO, toCSV } from '@grafana/data';
|
||||||
|
|
||||||
|
export function dataFrameToCSV(dto?: DataFrameDTO[]) {
|
||||||
|
if (!dto || !dto.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return toCSV(dto.map(v => toDataFrame(dto)));
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ describe('LokiDatasource', () => {
|
|||||||
const res = await ds.query(options);
|
const res = await ds.query(options);
|
||||||
|
|
||||||
const dataFrame = res.data[0] as DataFrame;
|
const dataFrame = res.data[0] as DataFrame;
|
||||||
expect(dataFrame.rows[0][1]).toBe('hello');
|
expect(dataFrame.fields[1].values.get(0)).toBe('hello');
|
||||||
expect(dataFrame.meta.limit).toBe(20);
|
expect(dataFrame.meta.limit).toBe(20);
|
||||||
expect(dataFrame.meta.searchWords).toEqual(['(?i)foo']);
|
expect(dataFrame.meta.searchWords).toEqual(['(?i)foo']);
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data.streams) {
|
if (!data.streams) {
|
||||||
return [{ ...logStreamToDataFrame(data), refId: target.refId }];
|
return [logStreamToDataFrame(data, target.refId)];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const stream of data.streams || []) {
|
for (const stream of data.streams || []) {
|
||||||
@@ -330,18 +330,17 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
const series: DataFrame[] = [];
|
const series: DataFrame[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const reverse = options && options.direction === 'FORWARD';
|
||||||
const result = await this._request('/api/prom/query', target);
|
const result = await this._request('/api/prom/query', target);
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
for (const stream of result.data.streams || []) {
|
for (const stream of result.data.streams || []) {
|
||||||
const dataFrame = logStreamToDataFrame(stream);
|
const dataFrame = logStreamToDataFrame(stream);
|
||||||
|
if (reverse) {
|
||||||
|
dataFrame.reverse();
|
||||||
|
}
|
||||||
series.push(dataFrame);
|
series.push(dataFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options && options.direction === 'FORWARD') {
|
|
||||||
if (series[0] && series[0].rows) {
|
|
||||||
series[0].rows.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: series,
|
data: series,
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ describe('convert loki response to DataFrame', () => {
|
|||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data[0].labels['foo']).toEqual('bar');
|
expect(data[0].labels['foo']).toEqual('bar');
|
||||||
expect(data[0].rows[0][0]).toEqual(streams[0].entries[0].ts);
|
expect(data[0].fields[0].values.get(0)).toEqual(streams[0].entries[0].ts);
|
||||||
expect(data[0].rows[0][1]).toEqual(streams[0].entries[0].line);
|
expect(data[0].fields[1].values.get(0)).toEqual(streams[0].entries[0].line);
|
||||||
expect(data[1].rows[0][0]).toEqual(streams[1].entries[0].ts);
|
expect(data[1].fields[0].values.get(0)).toEqual(streams[1].entries[0].ts);
|
||||||
expect(data[1].rows[0][1]).toEqual(streams[1].entries[0].line);
|
expect(data[1].fields[1].values.get(0)).toEqual(streams[1].entries[0].line);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { LokiLogsStream } from './types';
|
import { LokiLogsStream } from './types';
|
||||||
import { DataFrame, parseLabels, FieldType, Labels } from '@grafana/data';
|
import { parseLabels, FieldType, Labels, DataFrameHelper } from '@grafana/data';
|
||||||
|
|
||||||
export function logStreamToDataFrame(stream: LokiLogsStream): DataFrame {
|
export function logStreamToDataFrame(stream: LokiLogsStream, refId?: string): DataFrameHelper {
|
||||||
let labels: Labels = stream.parsedLabels;
|
let labels: Labels = stream.parsedLabels;
|
||||||
if (!labels && stream.labels) {
|
if (!labels && stream.labels) {
|
||||||
labels = parseLabels(stream.labels);
|
labels = parseLabels(stream.labels);
|
||||||
}
|
}
|
||||||
return {
|
const time: string[] = [];
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of stream.entries) {
|
||||||
|
time.push(entry.ts || entry.timestamp);
|
||||||
|
lines.push(entry.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataFrameHelper({
|
||||||
|
refId,
|
||||||
labels,
|
labels,
|
||||||
fields: [{ name: 'ts', type: FieldType.time }, { name: 'line', type: FieldType.string }],
|
fields: [
|
||||||
rows: stream.entries.map(entry => {
|
{ name: 'ts', type: FieldType.time, values: time }, // Time
|
||||||
return [entry.ts || entry.timestamp, entry.line];
|
{ name: 'line', type: FieldType.string, values: lines }, // Line
|
||||||
}),
|
],
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import defaults from 'lodash/defaults';
|
import defaults from 'lodash/defaults';
|
||||||
import { DataQueryRequest, DataQueryResponse, DataQueryError, DataStreamObserver, DataStreamState } from '@grafana/ui';
|
import { DataQueryRequest, DataQueryResponse, DataQueryError, DataStreamObserver, DataStreamState } from '@grafana/ui';
|
||||||
|
|
||||||
import { FieldType, DataFrame, LoadingState, LogLevel, CSVReader } from '@grafana/data';
|
import {
|
||||||
|
FieldType,
|
||||||
|
Field,
|
||||||
|
LoadingState,
|
||||||
|
LogLevel,
|
||||||
|
CSVReader,
|
||||||
|
DataFrameHelper,
|
||||||
|
CircularVector,
|
||||||
|
DataFrame,
|
||||||
|
} from '@grafana/data';
|
||||||
import { TestDataQuery, StreamingQuery } from './types';
|
import { TestDataQuery, StreamingQuery } from './types';
|
||||||
|
|
||||||
export const defaultQuery: StreamingQuery = {
|
export const defaultQuery: StreamingQuery = {
|
||||||
@@ -74,6 +83,10 @@ export class StreamWorker {
|
|||||||
last = -1;
|
last = -1;
|
||||||
timeoutId = 0;
|
timeoutId = 0;
|
||||||
|
|
||||||
|
// The values within
|
||||||
|
values: CircularVector[] = [];
|
||||||
|
data: DataFrame = { fields: [], length: 0 };
|
||||||
|
|
||||||
constructor(key: string, query: TestDataQuery, request: DataQueryRequest, observer: DataStreamObserver) {
|
constructor(key: string, query: TestDataQuery, request: DataQueryRequest, observer: DataStreamObserver) {
|
||||||
this.stream = {
|
this.stream = {
|
||||||
key,
|
key,
|
||||||
@@ -103,26 +116,25 @@ export class StreamWorker {
|
|||||||
}
|
}
|
||||||
this.query = query.stream;
|
this.query = query.stream;
|
||||||
this.stream.request = request; // OK?
|
this.stream.request = request; // OK?
|
||||||
console.log('Reuse Test Stream: ', this);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
appendRows(append: any[][]) {
|
appendRows(append: any[][]) {
|
||||||
// Trim the maximum row count
|
// Trim the maximum row count
|
||||||
const { query, stream } = this;
|
const { stream, values, data } = this;
|
||||||
const maxRows = query.buffer ? query.buffer : stream.request.maxDataPoints;
|
|
||||||
|
|
||||||
// Edit the first series
|
// Append all rows
|
||||||
const series = stream.data[0];
|
for (let i = 0; i < append.length; i++) {
|
||||||
let rows = series.rows.concat(append);
|
const row = append[i];
|
||||||
const extra = maxRows - rows.length;
|
for (let j = 0; j < values.length; j++) {
|
||||||
if (extra < 0) {
|
values[j].append(row[j]); // Circular buffer will kick out old entries
|
||||||
rows = rows.slice(extra * -1);
|
|
||||||
}
|
}
|
||||||
series.rows = rows;
|
}
|
||||||
|
// Clear any cached values
|
||||||
// Tell the event about only the rows that changed (it may want to process them)
|
for (let j = 0; j < data.fields.length; j++) {
|
||||||
stream.delta = [{ ...series, rows: append }];
|
data.fields[j].calcs = undefined;
|
||||||
|
}
|
||||||
|
stream.data = [data];
|
||||||
|
|
||||||
// Broadcast the changes
|
// Broadcast the changes
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
@@ -143,7 +155,7 @@ export class SignalWorker extends StreamWorker {
|
|||||||
constructor(key: string, query: TestDataQuery, request: DataQueryRequest, observer: DataStreamObserver) {
|
constructor(key: string, query: TestDataQuery, request: DataQueryRequest, observer: DataStreamObserver) {
|
||||||
super(key, query, request, observer);
|
super(key, query, request, observer);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.stream.data = [this.initBuffer(query.refId)];
|
this.initBuffer(query.refId);
|
||||||
this.looper();
|
this.looper();
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
@@ -162,33 +174,46 @@ export class SignalWorker extends StreamWorker {
|
|||||||
return row;
|
return row;
|
||||||
};
|
};
|
||||||
|
|
||||||
initBuffer(refId: string): DataFrame {
|
initBuffer(refId: string) {
|
||||||
const { speed, buffer } = this.query;
|
const { speed, buffer } = this.query;
|
||||||
const data = {
|
const request = this.stream.request;
|
||||||
fields: [{ name: 'Time', type: FieldType.time }, { name: 'Value', type: FieldType.number }],
|
const maxRows = buffer ? buffer : request.maxDataPoints;
|
||||||
rows: [],
|
const times = new CircularVector(new Array<number>(maxRows));
|
||||||
|
const vals = new CircularVector(new Array<number>(maxRows));
|
||||||
|
this.values = [times, vals];
|
||||||
|
|
||||||
|
const data = new DataFrameHelper({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: times }, // The time field
|
||||||
|
{ name: 'Value', type: FieldType.number, values: vals },
|
||||||
|
],
|
||||||
refId,
|
refId,
|
||||||
name: 'Signal ' + refId,
|
name: 'Signal ' + refId,
|
||||||
} as DataFrame;
|
});
|
||||||
|
|
||||||
for (let i = 0; i < this.bands; i++) {
|
for (let i = 0; i < this.bands; i++) {
|
||||||
const suffix = this.bands > 1 ? ` ${i + 1}` : '';
|
const suffix = this.bands > 1 ? ` ${i + 1}` : '';
|
||||||
data.fields.push({ name: 'Min' + suffix, type: FieldType.number });
|
const min = new CircularVector(new Array<number>(maxRows));
|
||||||
data.fields.push({ name: 'Max' + suffix, type: FieldType.number });
|
const max = new CircularVector(new Array<number>(maxRows));
|
||||||
|
this.values.push(min);
|
||||||
|
this.values.push(max);
|
||||||
|
|
||||||
|
data.addField({ name: 'Min' + suffix, type: FieldType.number, values: min });
|
||||||
|
data.addField({ name: 'Max' + suffix, type: FieldType.number, values: max });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('START', data);
|
console.log('START', data);
|
||||||
|
|
||||||
const request = this.stream.request;
|
|
||||||
|
|
||||||
this.value = Math.random() * 100;
|
this.value = Math.random() * 100;
|
||||||
const maxRows = buffer ? buffer : request.maxDataPoints;
|
|
||||||
let time = Date.now() - maxRows * speed;
|
let time = Date.now() - maxRows * speed;
|
||||||
for (let i = 0; i < maxRows; i++) {
|
for (let i = 0; i < maxRows; i++) {
|
||||||
data.rows.push(this.nextRow(time));
|
const row = this.nextRow(time);
|
||||||
|
for (let j = 0; j < this.values.length; j++) {
|
||||||
|
this.values[j].append(row[j]);
|
||||||
|
}
|
||||||
time += speed;
|
time += speed;
|
||||||
}
|
}
|
||||||
return data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
looper = () => {
|
looper = () => {
|
||||||
@@ -251,9 +276,10 @@ export class FetchWorker extends StreamWorker {
|
|||||||
return this.reader.read().then(this.processChunk);
|
return this.reader.read().then(this.processChunk);
|
||||||
};
|
};
|
||||||
|
|
||||||
onHeader = (series: DataFrame) => {
|
onHeader = (fields: Field[]) => {
|
||||||
series.refId = this.refId;
|
console.warn('TODO!!!', fields);
|
||||||
this.stream.data = [series];
|
// series.refId = this.refId;
|
||||||
|
// this.stream.data = [series];
|
||||||
};
|
};
|
||||||
|
|
||||||
onRow = (row: any[]) => {
|
onRow = (row: any[]) => {
|
||||||
@@ -269,7 +295,7 @@ export class LogsWorker extends StreamWorker {
|
|||||||
super(key, query, request, observer);
|
super(key, query, request, observer);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.stream.data = [this.initBuffer(query.refId)];
|
this.initBuffer(query.refId);
|
||||||
this.looper();
|
this.looper();
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
@@ -314,24 +340,34 @@ export class LogsWorker extends StreamWorker {
|
|||||||
return [time, '[' + this.getRandomLogLevel() + '] ' + this.getRandomLine()];
|
return [time, '[' + this.getRandomLogLevel() + '] ' + this.getRandomLine()];
|
||||||
};
|
};
|
||||||
|
|
||||||
initBuffer(refId: string): DataFrame {
|
initBuffer(refId: string) {
|
||||||
const { speed, buffer } = this.query;
|
const { speed, buffer } = this.query;
|
||||||
const data = {
|
|
||||||
fields: [{ name: 'Time', type: FieldType.time }, { name: 'Line', type: FieldType.string }],
|
|
||||||
rows: [],
|
|
||||||
refId,
|
|
||||||
name: 'Logs ' + refId,
|
|
||||||
} as DataFrame;
|
|
||||||
|
|
||||||
const request = this.stream.request;
|
const request = this.stream.request;
|
||||||
|
|
||||||
const maxRows = buffer ? buffer : request.maxDataPoints;
|
const maxRows = buffer ? buffer : request.maxDataPoints;
|
||||||
|
|
||||||
|
const times = new CircularVector(new Array(maxRows));
|
||||||
|
const lines = new CircularVector(new Array(maxRows));
|
||||||
|
|
||||||
|
this.values = [times, lines];
|
||||||
|
this.data = new DataFrameHelper({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: times },
|
||||||
|
{ name: 'Line', type: FieldType.string, values: lines },
|
||||||
|
],
|
||||||
|
refId,
|
||||||
|
name: 'Logs ' + refId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill up the buffer
|
||||||
let time = Date.now() - maxRows * speed;
|
let time = Date.now() - maxRows * speed;
|
||||||
for (let i = 0; i < maxRows; i++) {
|
for (let i = 0; i < maxRows; i++) {
|
||||||
data.rows.push(this.nextRow(time));
|
const row = this.nextRow(time);
|
||||||
|
times.append(row[0]);
|
||||||
|
lines.append(row[1]);
|
||||||
time += speed;
|
time += speed;
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
looper = () => {
|
looper = () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
PanelEditorProps,
|
PanelEditorProps,
|
||||||
Select,
|
Select,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Field } from '@grafana/data';
|
import { FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { Threshold, ValueMapping } from '@grafana/data';
|
import { Threshold, ValueMapping } from '@grafana/data';
|
||||||
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
||||||
@@ -41,7 +41,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
fieldOptions,
|
fieldOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
onDefaultsChange = (field: Partial<Field>) => {
|
onDefaultsChange = (field: FieldConfig) => {
|
||||||
this.onDisplayOptionsChanged({
|
this.onDisplayOptionsChanged({
|
||||||
...this.props.options.fieldOptions,
|
...this.props.options.fieldOptions,
|
||||||
defaults: field,
|
defaults: field,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
PanelOptionsGroup,
|
PanelOptionsGroup,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Field, Threshold, ValueMapping } from '@grafana/data';
|
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
|||||||
fieldOptions,
|
fieldOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
onDefaultsChange = (field: Partial<Field>) => {
|
onDefaultsChange = (field: FieldConfig) => {
|
||||||
this.onDisplayOptionsChanged({
|
this.onDisplayOptionsChanged({
|
||||||
...this.props.options.fieldOptions,
|
...this.props.options.fieldOptions,
|
||||||
defaults: field,
|
defaults: field,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { colors, getColorFromHexRgbOrName } from '@grafana/ui';
|
import { colors, getColorFromHexRgbOrName } from '@grafana/ui';
|
||||||
import { TimeRange, FieldCache, FieldType, Field, DataFrame } from '@grafana/data';
|
import { TimeRange, FieldType, Field, DataFrame, DataFrameHelper } from '@grafana/data';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
@@ -21,35 +21,24 @@ export class DataProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const series of dataList) {
|
for (const series of dataList) {
|
||||||
const { fields } = series;
|
const data = new DataFrameHelper(series);
|
||||||
const cache = new FieldCache(fields);
|
const time = data.getFirstFieldOfType(FieldType.time);
|
||||||
const time = cache.getFirstFieldOfType(FieldType.time);
|
|
||||||
|
|
||||||
if (!time) {
|
if (!time) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesName = series.name ? series.name : series.refId;
|
const seriesName = series.name ? series.name : series.refId;
|
||||||
|
for (const field of data.getFields(FieldType.number)) {
|
||||||
for (let i = 0; i < fields.length; i++) {
|
let name = field.config && field.config.title ? field.config.title : field.name;
|
||||||
if (fields[i].type !== FieldType.number) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const field = fields[i];
|
|
||||||
let name = field.title;
|
|
||||||
|
|
||||||
if (!field.title) {
|
|
||||||
name = field.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesName && dataList.length > 0 && name !== seriesName) {
|
if (seriesName && dataList.length > 0 && name !== seriesName) {
|
||||||
name = seriesName + ' ' + name;
|
name = seriesName + ' ' + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datapoints = [];
|
const datapoints = [];
|
||||||
for (const row of series.rows) {
|
for (let r = 0; r < data.length; r++) {
|
||||||
datapoints.push([row[i], row[time.index]]);
|
datapoints.push([field.values.get(r), time.values.get(r)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
list.push(this.toTimeSeries(field, name, datapoints, list.length, range));
|
list.push(this.toTimeSeries(field, name, datapoints, list.length, range));
|
||||||
@@ -76,7 +65,7 @@ export class DataProcessor {
|
|||||||
datapoints: datapoints || [],
|
datapoints: datapoints || [],
|
||||||
alias: alias,
|
alias: alias,
|
||||||
color: getColorFromHexRgbOrName(color, config.theme.type),
|
color: getColorFromHexRgbOrName(color, config.theme.type),
|
||||||
unit: field.unit,
|
unit: field.config ? field.config.unit : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (datapoints && datapoints.length > 0 && range) {
|
if (datapoints && datapoints.length > 0 && range) {
|
||||||
|
|||||||
@@ -34,12 +34,11 @@ describe('Graph DataProcessor', () => {
|
|||||||
{
|
{
|
||||||
name: 'series',
|
name: 'series',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'v1' }, // first
|
{ name: 'v1', values: [0.1, 0.2, 0.3] }, // first
|
||||||
{ name: 'v2' }, // second
|
{ name: 'v2', values: [1.1, 2.2, 3.3] }, // second
|
||||||
{ name: 'string' }, // skip
|
{ name: 'string', values: ['a', 'b', 'c'] }, // skip
|
||||||
{ name: 'time' }, // Time is last column
|
{ name: 'time', values: [1001, 1002, 1003] }, // Time is last column
|
||||||
],
|
],
|
||||||
rows: [[0.1, 1.1, 'a', 1001], [0.2, 2.2, 'b', 1002], [0.3, 3.3, 'c', 1003]],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -47,6 +46,7 @@ describe('Graph DataProcessor', () => {
|
|||||||
panel.xaxis.mode = 'series';
|
panel.xaxis.mode = 'series';
|
||||||
const series = processor.getSeriesList({ dataList });
|
const series = processor.getSeriesList({ dataList });
|
||||||
expect(series.length).toEqual(5);
|
expect(series.length).toEqual(5);
|
||||||
|
|
||||||
expect(series).toMatchSnapshot();
|
expect(series).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { colors, getFlotPairs, getColorFromHexRgbOrName, getDisplayProcessor, PanelData } from '@grafana/ui';
|
import { colors, getFlotPairs, getColorFromHexRgbOrName, getDisplayProcessor, PanelData } from '@grafana/ui';
|
||||||
import { NullValueMode, reduceField, FieldCache, FieldType, DisplayValue, GraphSeriesXY } from '@grafana/data';
|
import { NullValueMode, reduceField, DataFrameHelper, FieldType, DisplayValue, GraphSeriesXY } from '@grafana/data';
|
||||||
|
|
||||||
import { SeriesOptions, GraphOptions } from './types';
|
import { SeriesOptions, GraphOptions } from './types';
|
||||||
import { GraphLegendEditorLegendOptions } from './GraphLegendEditor';
|
import { GraphLegendEditorLegendOptions } from './GraphLegendEditor';
|
||||||
@@ -19,29 +19,22 @@ export const getGraphSeriesModel = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const series of data.series) {
|
for (const series of data.series) {
|
||||||
const fieldCache = new FieldCache(series.fields);
|
const data = new DataFrameHelper(series);
|
||||||
const timeColumn = fieldCache.getFirstFieldOfType(FieldType.time);
|
const timeColumn = data.getFirstFieldOfType(FieldType.time);
|
||||||
if (!timeColumn) {
|
if (!timeColumn) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberFields = fieldCache.getFields(FieldType.number);
|
for (const field of data.getFields(FieldType.number)) {
|
||||||
for (let i = 0; i < numberFields.length; i++) {
|
|
||||||
const field = numberFields[i];
|
|
||||||
// Use external calculator just to make sure it works :)
|
// Use external calculator just to make sure it works :)
|
||||||
const points = getFlotPairs({
|
const points = getFlotPairs({
|
||||||
rows: series.rows,
|
xField: timeColumn,
|
||||||
xIndex: timeColumn.index,
|
yField: field,
|
||||||
yIndex: field.index,
|
|
||||||
nullValueMode: NullValueMode.Null,
|
nullValueMode: NullValueMode.Null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (points.length > 0) {
|
if (points.length > 0) {
|
||||||
const seriesStats = reduceField({
|
const seriesStats = reduceField({ field, reducers: legendOptions.stats });
|
||||||
series,
|
|
||||||
reducers: legendOptions.stats,
|
|
||||||
fieldIndex: field.index,
|
|
||||||
});
|
|
||||||
let statsDisplayValues: DisplayValue[];
|
let statsDisplayValues: DisplayValue[];
|
||||||
|
|
||||||
if (legendOptions.stats) {
|
if (legendOptions.stats) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
FieldPropertiesEditor,
|
FieldPropertiesEditor,
|
||||||
PanelOptionsGroup,
|
PanelOptionsGroup,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { ValueMapping, Field } from '@grafana/data';
|
import { ValueMapping, FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { PieChartOptionsBox } from './PieChartOptionsBox';
|
import { PieChartOptionsBox } from './PieChartOptionsBox';
|
||||||
import { PieChartOptions } from './types';
|
import { PieChartOptions } from './types';
|
||||||
@@ -28,7 +28,7 @@ export class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChart
|
|||||||
fieldOptions,
|
fieldOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
onDefaultsChange = (field: Partial<Field>) => {
|
onDefaultsChange = (field: FieldConfig) => {
|
||||||
this.onDisplayOptionsChanged({
|
this.onDisplayOptionsChanged({
|
||||||
...this.props.options.fieldOptions,
|
...this.props.options.fieldOptions,
|
||||||
defaults: field,
|
defaults: field,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
FieldPropertiesEditor,
|
FieldPropertiesEditor,
|
||||||
PanelOptionsGroup,
|
PanelOptionsGroup,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Threshold, ValueMapping, Field } from '@grafana/data';
|
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
||||||
|
|
||||||
import { SingleStatOptions, SparklineOptions } from './types';
|
import { SingleStatOptions, SparklineOptions } from './types';
|
||||||
import { ColoringEditor } from './ColoringEditor';
|
import { ColoringEditor } from './ColoringEditor';
|
||||||
@@ -46,7 +46,7 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
sparkline,
|
sparkline,
|
||||||
});
|
});
|
||||||
|
|
||||||
onDefaultsChange = (field: Partial<Field>) => {
|
onDefaultsChange = (field: FieldConfig) => {
|
||||||
this.onDisplayOptionsChanged({
|
this.onDisplayOptionsChanged({
|
||||||
...this.props.options.fieldOptions,
|
...this.props.options.fieldOptions,
|
||||||
override: field,
|
override: field,
|
||||||
|
|||||||
Reference in New Issue
Block a user