mirror of https://github.com/grafana/grafana
DataFrame: convert from row based to a columnar value format (#18391)
parent
350b9a9494
commit
e59bae55d9
@ -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>; |
||||
} |
@ -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(); |
||||
}); |
||||
}); |
@ -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; |
||||
} |
@ -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); |
||||
}); |
||||
}); |
@ -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; |
||||
} |
||||
} |
@ -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]); |
||||
}); |
||||
}); |
@ -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); |
||||
} |
||||
} |
@ -1,12 +1,12 @@ |
||||
import { DataQuery, DataSourceJsonData } from '@grafana/ui'; |
||||
import { DataFrame } from '@grafana/data'; |
||||
import { DataFrameDTO } from '@grafana/data'; |
||||
|
||||
export interface InputQuery extends DataQuery { |
||||
// Data saved in the panel
|
||||
data?: DataFrame[]; |
||||
data?: DataFrameDTO[]; |
||||
} |
||||
|
||||
export interface InputOptions extends DataSourceJsonData { |
||||
// Saved in the datasource and download with bootData
|
||||
data?: DataFrame[]; |
||||
data?: DataFrameDTO[]; |
||||
} |
||||
|
@ -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))); |
||||
} |
@ -1,16 +1,25 @@ |
||||
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; |
||||
if (!labels && 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, |
||||
fields: [{ name: 'ts', type: FieldType.time }, { name: 'line', type: FieldType.string }], |
||||
rows: stream.entries.map(entry => { |
||||
return [entry.ts || entry.timestamp, entry.line]; |
||||
}), |
||||
}; |
||||
fields: [ |
||||
{ name: 'ts', type: FieldType.time, values: time }, // Time
|
||||
{ name: 'line', type: FieldType.string, values: lines }, // Line
|
||||
], |
||||
}); |
||||
} |
||||
|
Loading…
Reference in new issue