Chore: Reorg packages (#20111)

Primarily- moving majority of the types and utils from @grafana/ui to @grafana/data

* Move types from grafana-ui to grafana-data

* Move valueFormats to grafana-data

* Move utils from grafana-ui to grafana-data

* Update imports in grafana-ui

* revert data's tsconfig change

* Update imports in grafana-runtime

* Fix import paths in grafana-ui

* Move rxjs to devDeps

* Core import updates batch 1

* Import updates batch 2

* Imports fix batch 3

* Imports fixes batch i don't know

* Fix imorts in grafana-toolkit

* Fix imports after master merge
This commit is contained in:
Dominik Prokop
2019-10-31 10:48:05 +01:00
committed by GitHub
parent 3e8c00dad1
commit 9b7748ec13
379 changed files with 984 additions and 892 deletions

View File

@@ -12,6 +12,7 @@
"url": "http://github.com/grafana/grafana.git"
},
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit",
@@ -37,8 +38,8 @@
"rollup-plugin-terser": "4.0.4",
"rollup-plugin-typescript2": "0.19.3",
"rollup-plugin-visualizer": "0.9.2",
"rxjs": "6.4.0",
"sinon": "1.17.6",
"typescript": "3.6.3"
},
"types": "src/index.ts"
}
}

View File

@@ -0,0 +1,194 @@
import { getDisplayProcessor, getColorFromThreshold } from './displayProcessor';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { ValueMapping, MappingType } from '../types/valueMapping';
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
processors.forEach(processor => {
const value = processor(input);
expect(value.text).toEqual(match.text);
if (match.hasOwnProperty('numeric')) {
expect(value.numeric).toEqual(match.numeric);
}
});
}
describe('Process simple display values', () => {
// Don't test float values here since the decimal formatting changes
const processors = [
// Without options, this shortcuts to a much easier implementation
getDisplayProcessor(),
// Add a simple option that is not used (uses a different base class)
getDisplayProcessor({ config: { min: 0, max: 100 } }),
// Add a simple option that is not used (uses a different base class)
getDisplayProcessor({ config: { unit: 'locale' } }),
];
it('support null', () => {
assertSame(null, processors, { text: '', numeric: NaN });
});
it('support undefined', () => {
assertSame(undefined, processors, { text: '', numeric: NaN });
});
it('support NaN', () => {
assertSame(NaN, processors, { text: 'NaN', numeric: NaN });
});
it('Integer', () => {
assertSame(3, processors, { text: '3', numeric: 3 });
});
it('Text to number', () => {
assertSame('3', processors, { text: '3', numeric: 3 });
});
it('Simple String', () => {
assertSame('hello', processors, { text: 'hello', numeric: NaN });
});
it('empty array', () => {
assertSame([], processors, { text: '', numeric: NaN });
});
it('array of text', () => {
assertSame(['a', 'b', 'c'], processors, { text: 'a,b,c', numeric: NaN });
});
it('array of numbers', () => {
assertSame([1, 2, 3], processors, { text: '1,2,3', numeric: NaN });
});
it('empty object', () => {
assertSame({}, processors, { text: '[object Object]', numeric: NaN });
});
it('boolean true', () => {
assertSame(true, processors, { text: 'true', numeric: 1 });
});
it('boolean false', () => {
assertSame(false, processors, { text: 'false', numeric: 0 });
});
});
describe('Get color from threshold', () => {
it('should get first threshold color when only one threshold', () => {
const thresholds = [{ index: 0, value: -Infinity, color: '#7EB26D' }];
expect(getColorFromThreshold(49, thresholds)).toEqual('#7EB26D');
});
it('should get the threshold color if value is same as a threshold', () => {
const thresholds = [
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
];
expect(getColorFromThreshold(50, thresholds)).toEqual('#EAB839');
});
it('should get the nearest threshold color between thresholds', () => {
const thresholds = [
{ index: 2, value: 75, color: '#6ED0E0' },
{ index: 1, value: 50, color: '#EAB839' },
{ index: 0, value: -Infinity, color: '#7EB26D' },
];
expect(getColorFromThreshold(55, thresholds)).toEqual('#EAB839');
});
});
describe('Format value', () => {
it('should return if value isNaN', () => {
const valueMappings: ValueMapping[] = [];
const value = 'N/A';
const instance = getDisplayProcessor({ config: { mappings: valueMappings } });
const result = instance(value);
expect(result.text).toEqual('N/A');
});
it('should return formatted value if there are no value mappings', () => {
const valueMappings: ValueMapping[] = [];
const value = '6';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const result = instance(value);
expect(result.text).toEqual('6.0');
});
it('should return formatted value if there are no matching value mappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
];
const value = '10';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const result = instance(value);
expect(result.text).toEqual('10.0');
});
it('should set auto decimals, 1 significant', () => {
const value = 3.23;
const instance = getDisplayProcessor({ config: { decimals: null } });
expect(instance(value).text).toEqual('3.2');
});
it('should set auto decimals, 2 significant', () => {
const value = 0.0245;
const instance = getDisplayProcessor({ config: { decimals: null } });
expect(instance(value).text).toEqual('0.025');
});
it('should use override decimals', () => {
const value = 100030303;
const instance = getDisplayProcessor({ config: { decimals: 2, unit: 'bytes' } });
expect(instance(value).text).toEqual('95.40 MiB');
});
it('should return mapped value if there are matching value mappings', () => {
const valueMappings: ValueMapping[] = [
{ id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '11';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
expect(instance(value).text).toEqual('1-20');
});
//
// Below is current behavior but I it's clearly not working great
//
it('with value 1000 and unit short', () => {
const value = 1000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.000 K');
});
it('with value 1200 and unit short', () => {
const value = 1200;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.200 K');
});
it('with value 1250 and unit short', () => {
const value = 1250;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.250 K');
});
it('with value 10000000 and unit short', () => {
const value = 1000000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
expect(instance(value).text).toEqual('1.000 Mil');
});
});

View File

@@ -0,0 +1,179 @@
// Libraries
import _ from 'lodash';
// Utils
import { getColorFromHexRgbOrName } from '../utils/namedColorsPalette';
// Types
import { FieldConfig } from '../types/dataFrame';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
import { DisplayProcessor, DisplayValue, DecimalCount, DecimalInfo } from '../types/displayValue';
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { Threshold } from '../types/threshold';
// import { GrafanaTheme, GrafanaThemeType, FieldConfig } from '../types/index';
interface DisplayProcessorOptions {
config?: FieldConfig;
// Context
isUtc?: boolean;
theme?: GrafanaTheme; // Will pick 'dark' if not defined
}
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
if (options && !_.isEmpty(options)) {
const field = options.config ? options.config : {};
const formatFunc = getValueFormat(field.unit || 'none');
return (value: any) => {
const { theme } = options;
const { mappings, thresholds } = field;
let color;
let text = _.toString(value);
let numeric = toNumber(value);
let shouldFormat = true;
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, value);
if (mappedValue) {
text = mappedValue.text;
const v = toNumber(text);
if (!isNaN(v)) {
numeric = v;
}
shouldFormat = false;
}
}
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
text = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
// Check if the formatted text mapped to a different value
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, text);
if (mappedValue) {
text = mappedValue.text;
}
}
}
if (thresholds && thresholds.length) {
color = getColorFromThreshold(numeric, thresholds, theme);
}
}
if (!text) {
if (field && field.noValue) {
text = field.noValue;
} else {
text = ''; // No data?
}
}
return { text, numeric, color };
};
}
return toStringProcessor;
}
/** Will return any value as a number or NaN */
function toNumber(value: any): number {
if (typeof value === 'number') {
return value;
}
if (value === null || value === undefined || Array.isArray(value)) {
return NaN; // lodash calls them 0
}
if (typeof value === 'boolean') {
return value ? 1 : 0;
}
return _.toNumber(value);
}
function toStringProcessor(value: any): DisplayValue {
return { text: _.toString(value), numeric: toNumber(value) };
}
export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string {
const themeType = theme ? theme.type : GrafanaThemeType.Dark;
if (thresholds.length === 1) {
return getColorFromHexRgbOrName(thresholds[0].color, themeType);
}
const atThreshold = thresholds.filter(threshold => value === threshold.value)[0];
if (atThreshold) {
return getColorFromHexRgbOrName(atThreshold.color, themeType);
}
const belowThreshold = thresholds.filter(threshold => value > threshold.value);
if (belowThreshold.length > 0) {
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
return getColorFromHexRgbOrName(nearestThreshold.color, themeType);
}
// Use the first threshold as the default color
return getColorFromHexRgbOrName(thresholds[0].color, themeType);
}
// function getSignificantDigitCount(n: number): number {
// // remove decimal and make positive
// n = Math.abs(parseInt(String(n).replace('.', ''), 10));
// if (n === 0) {
// return 0;
// }
//
// // kill the 0s at the end of n
// while (n !== 0 && n % 10 === 0) {
// n /= 10;
// }
//
// // get number of digits
// return Math.floor(Math.log(n) / Math.LN10) + 1;
// }
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
if (_.isNumber(decimalOverride)) {
// It's important that scaledDecimals is null here
return { decimals: decimalOverride, scaledDecimals: null };
}
let dec = -Math.floor(Math.log(value) / Math.LN10) + 1;
const magn = Math.pow(10, -dec);
const norm = value / magn; // norm is between 1.0 and 10.0
let size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
// reduce starting decimals if not needed
if (value % 1 === 0) {
dec = 0;
}
const decimals = Math.max(0, dec);
const scaledDecimals = decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return { decimals, scaledDecimals };
}

View File

@@ -0,0 +1,159 @@
import { getFieldProperties, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from '../transformations/fieldReducer';
import { Threshold } from '../types/threshold';
import { GrafanaTheme } from '../types/theme';
describe('FieldDisplay', () => {
it('Construct simple field properties', () => {
const f0 = {
min: 0,
max: 100,
};
const f1 = {
unit: 'ms',
dateFormat: '', // should be ignored
max: parseFloat('NOPE'), // should be ignored
min: null,
};
let field = getFieldProperties(f0, f1);
expect(field.min).toEqual(0);
expect(field.max).toEqual(100);
expect(field.unit).toEqual('ms');
// last one overrieds
const f2 = {
unit: 'none', // ignore 'none'
max: -100, // lower than min! should flip min/max
};
field = getFieldProperties(f0, f1, f2);
expect(field.max).toEqual(0);
expect(field.min).toEqual(-100);
expect(field.unit).toEqual('ms');
});
// Simple test dataset
const options: GetFieldDisplayValuesOptions = {
data: [
toDataFrame({
name: 'Series Name',
fields: [
{ name: 'Field 1', values: ['a', 'b', 'c'] },
{ name: 'Field 2', values: [1, 3, 5] },
{ name: 'Field 3', values: [2, 4, 6] },
],
}),
],
replaceVariables: (value: string) => {
return value; // Return it unchanged
},
fieldOptions: {
calcs: [],
override: {},
defaults: {},
},
theme: {} as GrafanaTheme,
};
it('show first numeric values', () => {
const display = getFieldDisplayValues({
...options,
fieldOptions: {
calcs: [ReducerID.first],
override: {},
defaults: {
title: '$__cell_0 * $__field_name * $__series_name',
},
},
});
expect(display.map(v => v.display.text)).toEqual(['1', '2']);
// expect(display.map(v => v.display.title)).toEqual([
// 'a * Field 1 * Series Name', // 0
// 'b * Field 2 * Series Name', // 1
// ]);
});
it('show last numeric values', () => {
const display = getFieldDisplayValues({
...options,
fieldOptions: {
calcs: [ReducerID.last],
override: {},
defaults: {},
},
});
expect(display.map(v => v.display.numeric)).toEqual([5, 6]);
});
it('show all numeric values', () => {
const display = getFieldDisplayValues({
...options,
fieldOptions: {
values: true, //
limit: 1000,
calcs: [],
override: {},
defaults: {},
},
});
expect(display.map(v => v.display.numeric)).toEqual([1, 3, 5, 2, 4, 6]);
});
it('show 2 numeric values (limit)', () => {
const display = getFieldDisplayValues({
...options,
fieldOptions: {
values: true, //
limit: 2,
calcs: [],
override: {},
defaults: {},
},
});
expect(display.map(v => v.display.numeric)).toEqual([1, 3]); // First 2 are from the first field
});
it('should restore -Infinity value for base threshold', () => {
const field = getFieldProperties({
thresholds: [
({
color: '#73BF69',
value: null,
} as unknown) as Threshold,
{
color: '#F2495C',
value: 50,
},
],
});
expect(field.thresholds!.length).toEqual(2);
expect(field.thresholds![0].value).toBe(-Infinity);
});
it('Should return field thresholds when there is no data', () => {
const options: GetFieldDisplayValuesOptions = {
data: [
{
name: 'No data',
fields: [],
length: 0,
},
],
replaceVariables: (value: string) => {
return value;
},
fieldOptions: {
calcs: [],
override: {},
defaults: {
thresholds: [{ color: '#F2495C', value: 50 }],
},
},
theme: {} as GrafanaTheme,
};
const display = getFieldDisplayValues(options);
expect(display[0].field.thresholds!.length).toEqual(1);
});
});

View File

@@ -0,0 +1,279 @@
import toNumber from 'lodash/toNumber';
import toString from 'lodash/toString';
import { getDisplayProcessor } from './displayProcessor';
import { getFlotPairs } from '../utils/flotPairs';
import { FieldConfig, DataFrame, FieldType } from '../types/dataFrame';
import { InterpolateFunction } from '../types/panel';
import { DataFrameView } from '../dataframe/DataFrameView';
import { GraphSeriesValue } from '../types/graph';
import { DisplayValue } from '../types/displayValue';
import { GrafanaTheme } from '../types/theme';
import { ReducerID, reduceField } from '../transformations/fieldReducer';
import { ScopedVars } from '../types/ScopedVars';
import { getTimeField } from '../dataframe/processDataFrame';
export interface FieldDisplayOptions {
values?: boolean; // If true show each row value
limit?: number; // if showing all values limit
calcs: string[]; // when !values, pick one value for the whole field
defaults: FieldConfig; // Use these values unless otherwise stated
override: FieldConfig; // Set these values regardless of the source
}
// TODO: use built in variables, same as for data links?
export const VAR_SERIES_NAME = '__series.name';
export const VAR_FIELD_NAME = '__field.name';
export const VAR_CALC = '__calc';
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
function getTitleTemplate(title: string | undefined, stats: string[], data?: DataFrame[]): string {
// If the title exists, use it as a template variable
if (title) {
return title;
}
if (!data || !data.length) {
return 'No Data';
}
let fieldCount = 0;
for (const field of data[0].fields) {
if (field.type === FieldType.number) {
fieldCount++;
}
}
const parts: string[] = [];
if (stats.length > 1) {
parts.push('${' + VAR_CALC + '}');
}
if (data.length > 1) {
parts.push('${' + VAR_SERIES_NAME + '}');
}
if (fieldCount > 1 || !parts.length) {
parts.push('${' + VAR_FIELD_NAME + '}');
}
return parts.join(' ');
}
export interface FieldDisplay {
name: string; // The field name (title is in display)
field: FieldConfig;
display: DisplayValue;
sparkline?: GraphSeriesValue[][];
// Expose to the original values for delayed inspection (DataLinks etc)
view?: DataFrameView;
colIndex?: number; // The field column index
rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction)
}
export interface GetFieldDisplayValuesOptions {
data?: DataFrame[];
fieldOptions: FieldDisplayOptions;
replaceVariables: InterpolateFunction;
sparkline?: boolean; // Calculate the sparkline
theme: GrafanaTheme;
}
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
const { data, replaceVariables, fieldOptions } = options;
const { defaults, override } = fieldOptions;
const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
const values: FieldDisplay[] = [];
if (data) {
let hitLimit = false;
const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
const scopedVars: ScopedVars = {};
for (let s = 0; s < data.length && !hitLimit; s++) {
let series = data[s];
if (!series.name) {
series = {
...series,
name: series.refId ? series.refId : `Series[${s}]`,
};
}
scopedVars['__series'] = { text: 'Series', value: { name: series.name } };
const { timeField } = getTimeField(series);
const view = new DataFrameView(series);
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
const field = series.fields[i];
// Show all number fields
if (field.type !== FieldType.number) {
continue;
}
const config = getFieldProperties(defaults, field.config || {}, override);
let name = field.name;
if (!name) {
name = `Field[${s}]`;
}
scopedVars['__field'] = { text: 'Field', value: { name } };
const display = getDisplayProcessor({
config,
theme: options.theme,
});
const title = config.title ? config.title : defaultTitle;
// Show all rows
if (fieldOptions.values) {
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
for (let j = 0; j < field.values.length; j++) {
// Add all the row variables
if (usesCellValues) {
for (let k = 0; k < series.fields.length; k++) {
const f = series.fields[k];
const v = f.values.get(j);
scopedVars[VAR_CELL_PREFIX + k] = {
value: v,
text: toString(v),
};
}
}
const displayValue = display(field.values.get(j));
displayValue.title = replaceVariables(title, scopedVars);
values.push({
name,
field: config,
display: displayValue,
view,
colIndex: i,
rowIndex: j,
});
if (values.length >= limit) {
hitLimit = true;
break;
}
}
} else {
const results = reduceField({
field,
reducers: calcs, // The stats to calculate
});
let sparkline: GraphSeriesValue[][] | undefined = undefined;
// Single sparkline for every reducer
if (options.sparkline && timeField) {
sparkline = getFlotPairs({
xField: timeField,
yField: series.fields[i],
});
}
for (const calc of calcs) {
scopedVars[VAR_CALC] = { value: calc, text: calc };
const displayValue = display(results[calc]);
displayValue.title = replaceVariables(title, scopedVars);
values.push({
name: calc,
field: config,
display: displayValue,
sparkline,
view,
colIndex: i,
});
}
}
}
}
}
if (values.length === 0) {
values.push({
name: 'No data',
field: {
...defaults,
},
display: {
numeric: 0,
text: 'No data',
},
});
} else if (values.length === 1 && !fieldOptions.defaults.title) {
// Don't show title for single item
values[0].display.title = undefined;
}
return values;
};
const numericFieldProps: any = {
decimals: true,
min: true,
max: true,
};
/**
* Returns a version of the field with the overries applied. Any property with
* value: null | undefined | empty string are skipped.
*
* For numeric values, only valid numbers will be applied
* for units, 'none' will be skipped
*/
export function applyFieldProperties(field: FieldConfig, props?: FieldConfig): FieldConfig {
if (!props) {
return field;
}
const keys = Object.keys(props);
if (!keys.length) {
return field;
}
const copy = { ...field } as any; // make a copy that we will manipulate directly
for (const key of keys) {
const val = (props as any)[key];
if (val === null || val === undefined) {
continue;
}
if (numericFieldProps[key]) {
const num = toNumber(val);
if (!isNaN(num)) {
copy[key] = num;
}
} else if (val) {
// skips empty string
if (key === 'unit' && val === 'none') {
continue;
}
copy[key] = val;
}
}
return copy as FieldConfig;
}
export function getFieldProperties(...props: FieldConfig[]): FieldConfig {
let field = props[0] as FieldConfig;
for (let i = 1; i < props.length; i++) {
field = applyFieldProperties(field, props[i]);
}
// First value is always -Infinity
if (field.thresholds && field.thresholds.length) {
field.thresholds[0].value = -Infinity;
}
// Verify that max > min
if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
return {
...field,
min: field.max,
max: field.min,
};
}
return field;
}

View File

@@ -0,0 +1,2 @@
export * from './fieldDisplay';
export * from './displayProcessor';

View File

@@ -5,3 +5,5 @@ export * from './dataframe';
export * from './transformations';
export * from './datetime';
export * from './text';
export * from './valueFormats';
export * from './field';

View File

@@ -0,0 +1,71 @@
import { ComponentClass } from 'react';
import { KeyValue } from './data';
import { NavModel } from './navModel';
import { PluginMeta, GrafanaPlugin, PluginIncludeType } from './plugin';
export interface AppRootProps<T = KeyValue> {
meta: AppPluginMeta<T>;
path: string; // The URL path to this page
query: KeyValue; // The URL query parameters
/**
* Pass the nav model to the container... is there a better way?
*/
onNavChanged: (nav: NavModel) => void;
}
export interface AppPluginMeta<T = KeyValue> extends PluginMeta<T> {
// TODO anything specific to apps?
}
export class AppPlugin<T = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
// Content under: /a/${plugin-id}/*
root?: ComponentClass<AppRootProps<T>>;
rootNav?: NavModel; // Initial navigation model
// Old style pages
angularPages?: { [component: string]: any };
/**
* Called after the module has loaded, and before the app is used.
* This function may be called multiple times on the same instance.
* The first time, `this.meta` will be undefined
*/
init(meta: AppPluginMeta) {}
/**
* Set the component displayed under:
* /a/${plugin-id}/*
*/
setRootPage(root: ComponentClass<AppRootProps<T>>, rootNav?: NavModel) {
this.root = root;
this.rootNav = rootNav;
return this;
}
setComponentsFromLegacyExports(pluginExports: any) {
if (pluginExports.ConfigCtrl) {
this.angularConfigCtrl = pluginExports.ConfigCtrl;
}
if (this.meta && this.meta.includes) {
for (const include of this.meta.includes) {
if (include.type === PluginIncludeType.page && include.component) {
const exp = pluginExports[include.component];
if (!exp) {
console.warn('App Page uses unknown component: ', include.component, this.meta);
continue;
}
if (!this.angularPages) {
this.angularPages = {};
}
this.angularPages[include.component] = exp;
}
}
}
}
}

View File

@@ -1,4 +0,0 @@
export interface AppEvent<T> {
readonly name: string;
payload?: T;
}

View File

@@ -1,5 +1,10 @@
import { eventFactory } from './utils';
export interface AppEvent<T> {
readonly name: string;
payload?: T;
}
export type AlertPayload = [string, string?];
export const alertSuccess = eventFactory<AlertPayload>('alert-success');

View File

@@ -0,0 +1,559 @@
import { Observable } from 'rxjs';
import { ComponentType } from 'react';
import { PluginMeta, GrafanaPlugin } from './plugin';
import { PanelData } from './panel';
import { LogRowModel } from './logs';
import { AnnotationEvent, TimeSeries, TableData, LoadingState, KeyValue } from './data';
import { DataFrame, DataFrameDTO } from './dataFrame';
import { TimeRange, RawTimeRange } from './time';
import { ScopedVars } from './ScopedVars';
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
options: DataSourceSettings<JSONData, SecureJSONData>;
onOptionsChange: (options: DataSourceSettings<JSONData, SecureJSONData>) => void;
}
export class DataSourcePlugin<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> extends GrafanaPlugin<DataSourcePluginMeta> {
DataSourceClass: DataSourceConstructor<DSType, TQuery, TOptions>;
components: DataSourcePluginComponents<DSType, TQuery, TOptions>;
constructor(DataSourceClass: DataSourceConstructor<DSType, TQuery, TOptions>) {
super();
this.DataSourceClass = DataSourceClass;
this.components = {};
}
setConfigEditor(editor: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>) {
this.components.ConfigEditor = editor;
return this;
}
setConfigCtrl(ConfigCtrl: any) {
this.angularConfigCtrl = ConfigCtrl;
return this;
}
setQueryCtrl(QueryCtrl: any) {
this.components.QueryCtrl = QueryCtrl;
return this;
}
setAnnotationQueryCtrl(AnnotationsQueryCtrl: any) {
this.components.AnnotationsQueryCtrl = AnnotationsQueryCtrl;
return this;
}
setQueryEditor(QueryEditor: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>) {
this.components.QueryEditor = QueryEditor;
return this;
}
setExploreQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
this.components.ExploreQueryField = ExploreQueryField;
return this;
}
setExploreMetricsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
this.components.ExploreMetricsQueryField = ExploreQueryField;
return this;
}
setExploreLogsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
this.components.ExploreLogsQueryField = ExploreQueryField;
return this;
}
setExploreStartPage(ExploreStartPage: ComponentType<ExploreStartPageProps>) {
this.components.ExploreStartPage = ExploreStartPage;
return this;
}
setVariableQueryEditor(VariableQueryEditor: any) {
this.components.VariableQueryEditor = VariableQueryEditor;
return this;
}
setComponentsFromLegacyExports(pluginExports: any) {
this.angularConfigCtrl = pluginExports.ConfigCtrl;
this.components.QueryCtrl = pluginExports.QueryCtrl;
this.components.AnnotationsQueryCtrl = pluginExports.AnnotationsQueryCtrl;
this.components.ExploreQueryField = pluginExports.ExploreQueryField;
this.components.ExploreStartPage = pluginExports.ExploreStartPage;
this.components.QueryEditor = pluginExports.QueryEditor;
this.components.VariableQueryEditor = pluginExports.VariableQueryEditor;
}
}
export interface DataSourcePluginMeta extends PluginMeta {
builtIn?: boolean; // Is this for all
metrics?: boolean;
logs?: boolean;
annotations?: boolean;
alerting?: boolean;
mixed?: boolean;
hasQueryHelp?: boolean;
category?: string;
queryOptions?: PluginMetaQueryOptions;
sort?: number;
streaming?: boolean;
}
interface PluginMetaQueryOptions {
cacheTimeout?: boolean;
maxDataPoints?: boolean;
minInterval?: boolean;
}
export interface DataSourcePluginComponents<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> {
QueryCtrl?: any;
AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any;
QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>;
}
// Only exported for tests
export interface DataSourceConstructor<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> {
new (instanceSettings: DataSourceInstanceSettings<TOptions>, ...args: any[]): DSType;
}
/**
* The main data source abstraction interface, represents an instance of a data source
*
* Although this is a class, datasource implementations do not *yet* need to extend it.
* As such, we can not yet add functions with default implementations.
*/
export abstract class DataSourceApi<
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> {
/**
* Set in constructor
*/
readonly name: string;
/**
* Set in constructor
*/
readonly id: number;
/**
* min interval range
*/
interval?: string;
constructor(instanceSettings: DataSourceInstanceSettings<TOptions>) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
}
/**
* Imports queries from a different datasource
*/
importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
/**
* Initializes a datasource after instantiation
*/
init?: () => void;
/**
* Query for data, and optionally stream results
*/
abstract query(request: DataQueryRequest<TQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse>;
/**
* Test & verify datasource settings & connection details
*/
abstract testDatasource(): Promise<any>;
/**
* Get hints for query improvements
*/
getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
/**
* Convert a query to a simple text string
*/
getQueryDisplayText?(query: TQuery): string;
/**
* Retrieve context for a given log row
*/
getLogRowContext?: <TContextQueryOptions extends {}>(
row: LogRowModel,
options?: TContextQueryOptions
) => Promise<DataQueryResponse>;
/**
* Variable query action.
*/
metricFindQuery?(query: any, options?: any): Promise<MetricFindValue[]>;
/**
* Get tag keys for adhoc filters
*/
getTagKeys?(options?: any): Promise<MetricFindValue[]>;
/**
* Get tag values for adhoc filters
*/
getTagValues?(options: any): Promise<MetricFindValue[]>;
/**
* Set after constructor call, as the data source instance is the most common thing to pass around
* we attach the components to this instance for easy access
*/
components?: DataSourcePluginComponents<DataSourceApi<TQuery, TOptions>, TQuery, TOptions>;
/**
* static information about the datasource
*/
meta?: DataSourcePluginMeta;
/**
* Used by alerting to check if query contains template variables
*/
targetContainsTemplate?(query: TQuery): boolean;
/**
* Used in explore
*/
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
/**
* Used in explore
*/
getHighlighterExpression?(query: TQuery): string[];
/**
* Used in explore
*/
languageProvider?: any;
/**
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
*/
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
}
export interface QueryEditorProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> {
datasource: DSType;
query: TQuery;
onRunQuery: () => void;
onChange: (value: TQuery) => void;
/*
* Contains query response filtered by refId and possible query error
*/
data?: PanelData;
}
export enum DataSourceStatus {
Connected,
Disconnected,
}
export interface ExploreQueryFieldProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData
> extends QueryEditorProps<DSType, TQuery, TOptions> {
history: any[];
onHint?: (action: QueryFixAction) => void;
}
export interface ExploreStartPageProps {
datasource?: DataSourceApi;
onClickExample: (query: DataQuery) => void;
}
/**
* Starting in v6.2 DataFrame can represent both TimeSeries and TableData
*/
export type LegacyResponseData = TimeSeries | TableData | any;
export type DataQueryResponseData = DataFrame | DataFrameDTO | LegacyResponseData;
export type DataStreamObserver = (event: DataStreamState) => void;
export interface DataStreamState {
/**
* when Done or Error no more events will be processed
*/
state: LoadingState;
/**
* The key is used to identify unique sets of data within
* a response, and join or replace them before sending them to the panel.
*
* For example consider a query that streams four DataFrames (A,B,C,D)
* and multiple events with keys K1, and K2
*
* query(...) returns: {
* state:Streaming
* data:[A]
* }
*
* Events:
* 1. {key:K1, data:[B1]} >> PanelData: [A,B1]
* 2. {key:K2, data:[C2,D2]} >> PanelData: [A,B1,C2,D2]
* 3. {key:K1, data:[B3]} >> PanelData: [A,B3,C2,D2]
* 4. {key:K2, data:[C4]} >> PanelData: [A,B3,C4]
*
* NOTE: that PanelData will not report a `Done` state until all
* unique keys have returned with either `Error` or `Done` state.
*/
key: string;
/**
* The stream request. The properties of this request will be examined
* to determine if the stream matches the original query. If not, it
* will be unsubscribed.
*/
request: DataQueryRequest;
/**
* The streaming events return entire DataFrames. The DataSource
* sending the events is responsible for truncating any growing lists
* most likely to the requested `maxDataPoints`
*/
data?: DataFrame[];
/**
* Error in stream (but may still be running)
*/
error?: DataQueryError;
/**
* @deprecated: DO NOT USE IN ANYTHING NEW!!!!
*
* merging streaming rows should be handled in the DataSource
* and/or we should add metadata to this state event that
* indicates that the PanelQueryRunner should manage the row
* additions.
*/
delta?: DataFrame[];
/**
* Stop listening to this stream
*/
unsubscribe: () => void;
}
export interface DataQueryResponse {
/**
* The response data. When streaming, this may be empty
* or a partial result set
*/
data: DataQueryResponseData[];
/**
* When returning multiple partial responses or streams
* Use this key to inform Grafana how to combine the partial responses
* Multiple responses with same key are replaced (latest used)
*/
key?: string;
/**
* Use this to control which state the response should have
* Defaults to LoadingState.Done if state is not defined
*/
state?: LoadingState;
}
export interface DataQuery {
/**
* A - Z
*/
refId: string;
/**
* true if query is disabled (ie not executed / sent to TSDB)
*/
hide?: boolean;
/**
* Unique, guid like, string used in explore mode
*/
key?: string;
/**
* For mixed data sources the selected datasource is on the query level.
* For non mixed scenarios this is undefined.
*/
datasource?: string | null;
metric?: any;
}
export interface DataQueryError {
data?: {
message?: string;
error?: string;
};
message?: string;
status?: string;
statusText?: string;
refId?: string;
cancelled?: boolean;
}
export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
requestId: string; // Used to identify results and optionally cancel the request in backendSrv
timezone: string;
range: TimeRange;
rangeRaw?: RawTimeRange;
timeInfo?: string; // The query time description (blue text in the upper right)
targets: TQuery[];
panelId: number;
dashboardId: number;
cacheTimeout?: string;
interval: string;
intervalMs: number;
maxDataPoints: number;
scopedVars: ScopedVars;
// Request Timing
startTime: number;
endTime?: number;
}
export interface QueryFix {
type: string;
label: string;
action?: QueryFixAction;
}
export interface QueryFixAction {
type: string;
query?: string;
preventSubmit?: boolean;
}
export interface QueryHint {
type: string;
label: string;
fix?: QueryFix;
}
export interface MetricFindValue {
text: string;
}
export interface DataSourceJsonData {
authType?: string;
defaultRegion?: string;
}
/**
* Data Source instance edit model. This is returned from:
* /api/datasources
*/
export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJsonData, S = {}> {
id: number;
orgId: number;
name: string;
typeLogoUrl: string;
type: string;
access: string;
url: string;
password: string;
user: string;
database: string;
basicAuth: boolean;
basicAuthPassword: string;
basicAuthUser: string;
isDefault: boolean;
jsonData: T;
secureJsonData?: S;
secureJsonFields?: KeyValue<boolean>;
readOnly: boolean;
withCredentials: boolean;
version?: number;
}
/**
* Frontend settings model that is passed to Datasource constructor. This differs a bit from the model above
* as this data model is available to every user who has access to a data source (Viewers+). This is loaded
* in bootData (on page load), or from: /api/frontend/settings
*/
export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataSourceJsonData> {
id: number;
type: string;
name: string;
meta: DataSourcePluginMeta;
url?: string;
jsonData: T;
username?: string;
password?: string; // when access is direct, for some legacy datasources
database?: string;
/**
* This is the full Authorization header if basic auth is ennabled.
* Only available here when access is Browser (direct), when acess is Server (proxy)
* The basic auth header, username & password is never exposted to browser/Frontend
* so this will be emtpy then.
*/
basicAuth?: string;
withCredentials?: boolean;
}
export interface DataSourceSelectItem {
name: string;
value: string | null;
meta: DataSourcePluginMeta;
sort: string;
}
/**
* Options passed to the datasource.annotationQuery method. See docs/plugins/developing/datasource.md
*/
export interface AnnotationQueryRequest<MoreOptions = {}> {
range: TimeRange;
rangeRaw: RawTimeRange;
// Should be DataModel but cannot import that here from the main app. Needs to be moved to package first.
dashboard: any;
annotation: {
datasource: string;
enable: boolean;
name: string;
} & MoreOptions;
}
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
ts: number;
query: TQuery;
}
export abstract class LanguageProvider {
datasource!: DataSourceApi;
request!: (url: string, params?: any) => Promise<any>;
/**
* Returns startTask that resolves with a task list when main syntax is loaded.
* Task list consists of secondary promises that load more detailed language features.
*/
start!: () => Promise<any[]>;
startTask?: Promise<any[]>;
}

View File

@@ -13,7 +13,15 @@ export * from './graph';
export * from './ScopedVars';
export * from './transformations';
export * from './vector';
export * from './appEvent';
export * from './app';
export * from './datasource';
export * from './panel';
export * from './plugin';
export * from './theme';
import * as AppEvents from './events';
export { AppEvents };
import * as AppEvents from './appEvents';
import { AppEvent } from './appEvents';
export { AppEvent, AppEvents };
import * as PanelEvents from './panelEvents';
export { PanelEvents };

View File

@@ -0,0 +1,147 @@
import { ComponentClass, ComponentType } from 'react';
import { DataQueryRequest, DataQueryError } from './datasource';
import { PluginMeta, GrafanaPlugin } from './plugin';
import { ScopedVars } from './ScopedVars';
import { LoadingState } from './data';
import { DataFrame } from './dataFrame';
import { TimeRange, TimeZone, AbsoluteTimeRange } from './time';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelPluginMeta extends PluginMeta {
skipDataQuery?: boolean;
hideFromList?: boolean;
sort: number;
}
export interface PanelData {
state: LoadingState;
series: DataFrame[];
request?: DataQueryRequest;
error?: DataQueryError;
// Contains the range from the request or a shifted time range if a request uses relative time
timeRange: TimeRange;
}
export interface PanelProps<T = any> {
id: number; // ID within the current dashboard
data: PanelData;
timeRange: TimeRange;
timeZone: TimeZone;
options: T;
onOptionsChange: (options: T) => void;
renderCounter: number;
transparent: boolean;
width: number;
height: number;
replaceVariables: InterpolateFunction;
onChangeTimeRange: (timeRange: AbsoluteTimeRange) => void;
}
export interface PanelEditorProps<T = any> {
options: T;
onOptionsChange: (
options: T,
// callback can be used to run something right after update.
callback?: () => void
) => void;
data: PanelData;
}
export interface PanelModel<TOptions = any> {
id: number;
options: TOptions;
pluginVersion?: string;
}
/**
* Called when a panel is first loaded with current panel model
*/
export type PanelMigrationHandler<TOptions = any> = (panel: PanelModel<TOptions>) => Partial<TOptions>;
/**
* Called before a panel is initalized
*/
export type PanelTypeChangedHandler<TOptions = any> = (
options: Partial<TOptions>,
prevPluginId: string,
prevOptions: any
) => Partial<TOptions>;
export class PanelPlugin<TOptions = any> extends GrafanaPlugin<PanelPluginMeta> {
panel: ComponentType<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
defaults?: TOptions;
onPanelMigration?: PanelMigrationHandler<TOptions>;
onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
/**
* Legacy angular ctrl. If this exists it will be used instead of the panel
*/
angularPanelCtrl?: any;
constructor(panel: ComponentType<PanelProps<TOptions>>) {
super();
this.panel = panel;
}
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
this.editor = editor;
return this;
}
setDefaults(defaults: TOptions) {
this.defaults = defaults;
return this;
}
/**
* This function is called before the panel first loads if
* the current version is different than the version that was saved.
*
* This is a good place to support any changes to the options model
*/
setMigrationHandler(handler: PanelMigrationHandler) {
this.onPanelMigration = handler;
return this;
}
/**
* This function is called when the visualization was changed. This
* passes in the options that were used in the previous visualization
*/
setPanelChangeHandler(handler: PanelTypeChangedHandler) {
this.onPanelTypeChanged = handler;
return this;
}
}
export interface PanelSize {
width: number;
height: number;
}
export interface PanelMenuItem {
type?: 'submenu' | 'divider';
text?: string;
iconClassName?: string;
onClick?: () => void;
shortcut?: string;
subMenu?: PanelMenuItem[];
}
export interface AngularPanelMenuItem {
click: Function;
icon: string;
href: string;
divider: boolean;
text: string;
shortcut: string;
submenu: any[];
}
export enum VizOrientation {
Auto = 'auto',
Vertical = 'vertical',
Horizontal = 'horizontal',
}

View File

@@ -0,0 +1,33 @@
import { eventFactory } from './utils';
import { DataQueryError, DataQueryResponseData } from './datasource';
/** Payloads */
export interface PanelChangeViewPayload {
fullscreen?: boolean;
edit?: boolean;
panelId?: number;
toggle?: boolean;
}
export interface MenuElement {
text: string;
click: string;
role?: string;
shortcut?: string;
}
/** Events */
export const refresh = eventFactory('refresh');
export const componentDidMount = eventFactory('component-did-mount');
export const dataError = eventFactory<DataQueryError>('data-error');
export const dataReceived = eventFactory<DataQueryResponseData[]>('data-received');
export const dataSnapshotLoad = eventFactory<DataQueryResponseData[]>('data-snapshot-load');
export const editModeInitialized = eventFactory('init-edit-mode');
export const initPanelActions = eventFactory<MenuElement[]>('init-panel-actions');
export const panelChangeView = eventFactory<PanelChangeViewPayload>('panel-change-view');
export const panelInitialized = eventFactory('panel-initialized');
export const panelSizeChanged = eventFactory('panel-size-changed');
export const panelTeardown = eventFactory('panel-teardown');
export const render = eventFactory<any>('render');
export const viewModeChanged = eventFactory('view-mode-changed');

View File

@@ -0,0 +1,145 @@
import { ComponentClass } from 'react';
import { KeyValue } from './data';
export enum PluginState {
alpha = 'alpha', // Only included it `enable_alpha` is true
beta = 'beta', // Will show a warning banner
}
export enum PluginType {
panel = 'panel',
datasource = 'datasource',
app = 'app',
renderer = 'renderer',
}
export interface PluginMeta<T extends {} = KeyValue> {
id: string;
name: string;
type: PluginType;
info: PluginMetaInfo;
includes?: PluginInclude[];
state?: PluginState;
// System.load & relative URLS
module: string;
baseUrl: string;
// Define plugin requirements
dependencies?: PluginDependencies;
// Filled in by the backend
jsonData?: T;
secureJsonData?: KeyValue;
enabled?: boolean;
defaultNavUrl?: string;
hasUpdate?: boolean;
latestVersion?: string;
pinned?: boolean;
}
interface PluginDependencyInfo {
id: string;
name: string;
version: string;
type: PluginType;
}
export interface PluginDependencies {
grafanaVersion: string;
plugins: PluginDependencyInfo[];
}
export enum PluginIncludeType {
dashboard = 'dashboard',
page = 'page',
// Only valid for apps
panel = 'panel',
datasource = 'datasource',
}
export interface PluginInclude {
type: PluginIncludeType;
name: string;
path?: string;
icon?: string;
role?: string; // "Viewer", Admin, editor???
addToNav?: boolean; // Show in the sidebar... only if type=page?
// Angular app pages
component?: string;
}
interface PluginMetaInfoLink {
name: string;
url: string;
}
export interface PluginBuildInfo {
time?: number;
repo?: string;
branch?: string;
hash?: string;
number?: number;
pr?: number;
}
export interface ScreenshotInfo {
name: string;
path: string;
}
export interface PluginMetaInfo {
author: {
name: string;
url?: string;
};
description: string;
links: PluginMetaInfoLink[];
logos: {
large: string;
small: string;
};
build?: PluginBuildInfo;
screenshots: ScreenshotInfo[];
updated: string;
version: string;
}
export interface PluginConfigPageProps<T extends GrafanaPlugin> {
plugin: T;
query: KeyValue; // The URL query parameters
}
export interface PluginConfigPage<T extends GrafanaPlugin> {
title: string; // Display
icon?: string;
id: string; // Unique, in URL
body: ComponentClass<PluginConfigPageProps<T>>;
}
export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
// Meta is filled in by the plugin loading system
meta?: T;
// This is set if the plugin system had errors loading the plugin
loadError?: boolean;
// Config control (app/datasource)
angularConfigCtrl?: any;
// Show configuration tabs on the plugin page
configPages?: Array<PluginConfigPage<GrafanaPlugin>>;
// Tabs on the plugin page
addConfigPage(tab: PluginConfigPage<GrafanaPlugin>) {
if (!this.configPages) {
this.configPages = [];
}
this.configPages.push(tab);
return this;
}
}

View File

@@ -0,0 +1,236 @@
export enum GrafanaThemeType {
Light = 'light',
Dark = 'dark',
}
export interface GrafanaThemeCommons {
name: string;
// TODO: not sure if should be a part of theme
breakpoints: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
};
typography: {
fontFamily: {
sansSerif: string;
monospace: string;
};
size: {
root: string;
base: string;
xs: string;
sm: string;
md: string;
lg: string;
};
weight: {
light: number;
regular: number;
semibold: number;
bold: number;
};
lineHeight: {
xs: number; //1
sm: number; //1.1
md: number; // 4/3
lg: number; // 1.5
};
// TODO: Refactor to use size instead of custom defs
heading: {
h1: string;
h2: string;
h3: string;
h4: string;
h5: string;
h6: string;
};
link: {
decoration: string;
hoverDecoration: string;
};
};
spacing: {
insetSquishMd: string;
d: string;
xxs: string;
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
gutter: string;
// Next-gen forms spacing variables
// TODO: Move variables definition to respective components when implementing
formSpacingBase: number;
formMargin: string;
formFieldsetMargin: string;
formLegendMargin: string;
formInputHeight: string;
formButtonHeight: number;
formInputPaddingHorizontal: string;
// Used for icons do define spacing between icon and input field
// Applied on the right(prefix) or left(suffix)
formInputAffixPaddingHorizontal: string;
formInputMargin: string;
formLabelPadding: string;
formLabelMargin: string;
formValidationMessagePadding: string;
};
border: {
radius: {
sm: string;
md: string;
lg: string;
};
width: {
sm: string;
};
};
height: {
sm: string;
md: string;
lg: string;
};
panelPadding: number;
panelHeaderHeight: number;
zIndex: {
dropdown: string;
navbarFixed: string;
sidemenu: string;
tooltip: string;
modalBackdrop: string;
modal: string;
typeahead: string;
};
}
export interface GrafanaTheme extends GrafanaThemeCommons {
type: GrafanaThemeType;
isDark: boolean;
isLight: boolean;
background: {
dropdown: string;
scrollbar: string;
scrollbar2: string;
pageHeader: string;
};
colors: {
black: string;
white: string;
dark1: string;
dark2: string;
dark3: string;
dark4: string;
dark5: string;
dark6: string;
dark7: string;
dark8: string;
dark9: string;
dark10: string;
gray1: string;
gray2: string;
gray3: string;
gray4: string;
gray5: string;
gray6: string;
gray7: string;
// New greys palette used by next-gen form elements
gray98: string;
gray95: string;
gray85: string;
gray70: string;
gray33: string;
gray25: string;
gray15: string;
gray10: string;
gray05: string;
// New blues palette used by next-gen form elements
blue95: string;
blue85: string;
blue77: string;
// New reds palette used by next-gen form elements
red88: string;
grayBlue: string;
inputBlack: string;
// Accent colors
blue: string;
blueBase: string;
blueShade: string;
blueLight: string;
blueFaint: string;
redBase: string;
redShade: string;
greenBase: string;
greenShade: string;
red: string;
yellow: string;
purple: string;
variable: string;
orange: string;
orangeDark: string;
queryRed: string;
queryGreen: string;
queryPurple: string;
queryKeyword: string;
queryOrange: string;
brandPrimary: string;
brandSuccess: string;
brandWarning: string;
brandDanger: string;
// Status colors
online: string;
warn: string;
critical: string;
// Link colors
link: string;
linkDisabled: string;
linkHover: string;
linkExternal: string;
// Text colors
body: string;
text: string;
textStrong: string;
textWeak: string;
textFaint: string;
textEmphasis: string;
// TODO: move to background section
bodyBg: string;
pageBg: string;
headingColor: string;
pageHeaderBorder: string;
// Next-gen forms functional colors
formLabel: string;
formDescription: string;
formLegend: string;
formInputBg: string;
formInputBgDisabled: string;
formInputBorder: string;
formInputBorderHover: string;
formInputBorderActive: string;
formInputBorderInvalid: string;
formInputFocusOutline: string;
formInputText: string;
formInputTextStrong: string;
formInputTextWhite: string;
formValidationMessageText: string;
formValidationMessageBg: string;
};
shadow: {
pageHeader: string;
};
}

View File

@@ -1,4 +1,4 @@
import { AppEvent } from './appEvent';
import { AppEvent } from './appEvents';
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Subtract<T, K> = Omit<T, keyof K>;

View File

@@ -0,0 +1,62 @@
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
import { getFlotPairs, getFlotPairsConstant } from './flotPairs';
import { TimeRange } from '../types/time';
import { dateTime } from '../datetime/moment_wrapper';
describe('getFlotPairs', () => {
const series = new MutableDataFrame({
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', () => {
const pairs = getFlotPairs({
xField: series.fields[0],
yField: series.fields[1],
});
expect(pairs.length).toEqual(3);
expect(pairs[0].length).toEqual(2);
expect(pairs[0][0]).toEqual(1);
expect(pairs[0][1]).toEqual(100);
});
it('should work with strings', () => {
const pairs = getFlotPairs({
xField: series.fields[0],
yField: series.fields[2],
});
expect(pairs.length).toEqual(3);
expect(pairs[0].length).toEqual(2);
expect(pairs[0][0]).toEqual(1);
expect(pairs[0][1]).toEqual('a');
});
});
describe('getFlotPairsConstant', () => {
const makeRange = (from: number, to: number): TimeRange => ({
from: dateTime(from),
to: dateTime(to),
raw: { from: `${from}`, to: `${to}` },
});
it('should return an empty series on empty data', () => {
const range: TimeRange = makeRange(0, 1);
const pairs = getFlotPairsConstant([], range);
expect(pairs).toMatchObject([]);
});
it('should return an empty series on missing range', () => {
const pairs = getFlotPairsConstant([], {} as TimeRange);
expect(pairs).toMatchObject([]);
});
it('should return an constant series for range', () => {
const range: TimeRange = makeRange(0, 1);
const pairs = getFlotPairsConstant([[2, 123], [4, 456]], range);
expect(pairs).toMatchObject([[0, 123], [1, 123]]);
});
});

View File

@@ -0,0 +1,65 @@
import { Field } from '../types/dataFrame';
import { NullValueMode } from '../types/data';
import { GraphSeriesValue } from '../types/graph';
import { TimeRange } from '../types/time';
// Types
// import { NullValueMode, GraphSeriesValue, Field, TimeRange } from '@grafana/data';
export interface FlotPairsOptions {
xField: Field;
yField: Field;
nullValueMode?: NullValueMode;
}
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 nullAsZero = nullValueMode === NullValueMode.AsZero;
const pairs: any[][] = [];
for (let i = 0; i < length; i++) {
const x = vX.get(i);
let y = vY.get(i);
if (y === null) {
if (ignoreNulls) {
continue;
}
if (nullAsZero) {
y = 0;
}
}
// X must be a value
if (x === null) {
continue;
}
pairs.push([x, y]);
}
return pairs;
}
/**
* Returns a constant series based on the first value from the provide series.
* @param seriesData Series
* @param range Start and end time for the constant series
*/
export function getFlotPairsConstant(seriesData: GraphSeriesValue[][], range: TimeRange): GraphSeriesValue[][] {
if (!range.from || !range.to || !seriesData || seriesData.length === 0) {
return [];
}
const from = range.from.valueOf();
const to = range.to.valueOf();
const value = seriesData[0][1];
return [[from, value], [to, value]];
}

View File

@@ -6,5 +6,7 @@ export * from './labels';
export * from './labels';
export * from './object';
export * from './thresholds';
export * from './namedColorsPalette';
export { getMappedValue } from './valueMappings';
export { getFlotPairs, getFlotPairsConstant } from './flotPairs';

View File

@@ -0,0 +1,72 @@
import {
getColorName,
getColorDefinition,
getColorByName,
getColorFromHexRgbOrName,
getColorDefinitionByName,
} from './namedColorsPalette';
import { GrafanaThemeType } from '../types/theme';
describe('colors', () => {
const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue');
describe('getColorDefinition', () => {
it('returns undefined for unknown hex', () => {
expect(getColorDefinition('#ff0000', GrafanaThemeType.Light)).toBeUndefined();
expect(getColorDefinition('#ff0000', GrafanaThemeType.Dark)).toBeUndefined();
});
it('returns definition for known hex', () => {
expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue);
expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue);
});
});
describe('getColorName', () => {
it('returns undefined for unknown hex', () => {
expect(getColorName('#ff0000')).toBeUndefined();
});
it('returns name for known hex', () => {
expect(getColorName(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue.name);
expect(getColorName(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue.name);
});
});
describe('getColorByName', () => {
it('returns undefined for unknown color', () => {
expect(getColorByName('aruba-sunshine')).toBeUndefined();
});
it('returns color definiton for known color', () => {
expect(getColorByName(SemiDarkBlue.name)).toBe(SemiDarkBlue);
});
});
describe('getColorFromHexRgbOrName', () => {
it('returns black for unknown color', () => {
expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe('#000000');
});
it('returns dark hex variant for known color if theme not specified', () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark);
});
it("returns correct variant's hex for known color if theme specified", () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaThemeType.Light)).toBe(SemiDarkBlue.variants.light);
});
it('returns color if specified as hex or rgb/a', () => {
expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
expect(getColorFromHexRgbOrName('#FF0000')).toBe('#FF0000');
expect(getColorFromHexRgbOrName('#CCC')).toBe('#CCC');
expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
});
it('returns hex for named color that is not a part of named colors palette', () => {
expect(getColorFromHexRgbOrName('lime')).toBe('#00ff00');
});
});
});

View File

@@ -0,0 +1,187 @@
import flatten from 'lodash/flatten';
import tinycolor from 'tinycolor2';
import { GrafanaThemeType } from '../types/theme';
type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
export type Color =
| 'green'
| 'dark-green'
| 'semi-dark-green'
| 'light-green'
| 'super-light-green'
| 'yellow'
| 'dark-yellow'
| 'semi-dark-yellow'
| 'light-yellow'
| 'super-light-yellow'
| 'red'
| 'dark-red'
| 'semi-dark-red'
| 'light-red'
| 'super-light-red'
| 'blue'
| 'dark-blue'
| 'semi-dark-blue'
| 'light-blue'
| 'super-light-blue'
| 'orange'
| 'dark-orange'
| 'semi-dark-orange'
| 'light-orange'
| 'super-light-orange'
| 'purple'
| 'dark-purple'
| 'semi-dark-purple'
| 'light-purple'
| 'super-light-purple';
type ThemeVariants = {
dark: string;
light: string;
};
export type ColorDefinition = {
hue: Hue;
isPrimary?: boolean;
name: Color;
variants: ThemeVariants;
};
let colorsPaletteInstance: Map<Hue, ColorDefinition[]>;
const buildColorDefinition = (
hue: Hue,
name: Color,
[light, dark]: string[],
isPrimary?: boolean
): ColorDefinition => ({
hue,
name,
variants: {
light,
dark,
},
isPrimary: !!isPrimary,
});
export const getColorDefinitionByName = (name: Color): ColorDefinition => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0];
};
export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
return flatten(Array.from(getNamedColorPalette().values())).filter(
definition => definition.variants[theme] === hex
)[0];
};
const isHex = (color: string) => {
const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{3})$/gi;
return hexRegex.test(color);
};
export const getColorName = (color?: string, theme?: GrafanaThemeType): Color | undefined => {
if (!color) {
return undefined;
}
if (color.indexOf('rgb') > -1) {
return undefined;
}
if (isHex(color)) {
const definition = getColorDefinition(color, theme || GrafanaThemeType.Dark);
return definition ? definition.name : undefined;
}
return color as Color;
};
export const getColorByName = (colorName: string) => {
const definition = flatten(Array.from(getNamedColorPalette().values())).filter(
definition => definition.name === colorName
);
return definition.length > 0 ? definition[0] : undefined;
};
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType): string => {
if (color.indexOf('rgb') > -1 || isHex(color)) {
return color;
}
const colorDefinition = getColorByName(color);
if (!colorDefinition) {
return new tinycolor(color).toHexString();
}
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
};
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaThemeType) => {
return theme ? color.variants[theme] : color.variants.dark;
};
const buildNamedColorsPalette = () => {
const palette = new Map<Hue, ColorDefinition[]>();
const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']);
const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']);
const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']);
const SuperLightGreen = buildColorDefinition('green', 'super-light-green', ['#96D98D', '#C8F2C2']);
const BasicYellow = buildColorDefinition('yellow', 'yellow', ['#F2CC0C', '#FADE2A'], true);
const DarkYellow = buildColorDefinition('yellow', 'dark-yellow', ['#CC9D00', '#E0B400']);
const SemiDarkYellow = buildColorDefinition('yellow', 'semi-dark-yellow', ['#E0B400', '#F2CC0C']);
const LightYellow = buildColorDefinition('yellow', 'light-yellow', ['#FADE2A', '#FFEE52']);
const SuperLightYellow = buildColorDefinition('yellow', 'super-light-yellow', ['#FFEE52', '#FFF899']);
const BasicRed = buildColorDefinition('red', 'red', ['#E02F44', '#F2495C'], true);
const DarkRed = buildColorDefinition('red', 'dark-red', ['#AD0317', '#C4162A']);
const SemiDarkRed = buildColorDefinition('red', 'semi-dark-red', ['#C4162A', '#E02F44']);
const LightRed = buildColorDefinition('red', 'light-red', ['#F2495C', '#FF7383']);
const SuperLightRed = buildColorDefinition('red', 'super-light-red', ['#FF7383', '#FFA6B0']);
const BasicBlue = buildColorDefinition('blue', 'blue', ['#3274D9', '#5794F2'], true);
const DarkBlue = buildColorDefinition('blue', 'dark-blue', ['#1250B0', '#1F60C4']);
const SemiDarkBlue = buildColorDefinition('blue', 'semi-dark-blue', ['#1F60C4', '#3274D9']);
const LightBlue = buildColorDefinition('blue', 'light-blue', ['#5794F2', '#8AB8FF']);
const SuperLightBlue = buildColorDefinition('blue', 'super-light-blue', ['#8AB8FF', '#C0D8FF']);
const BasicOrange = buildColorDefinition('orange', 'orange', ['#FF780A', '#FF9830'], true);
const DarkOrange = buildColorDefinition('orange', 'dark-orange', ['#E55400', '#FA6400']);
const SemiDarkOrange = buildColorDefinition('orange', 'semi-dark-orange', ['#FA6400', '#FF780A']);
const LightOrange = buildColorDefinition('orange', 'light-orange', ['#FF9830', '#FFB357']);
const SuperLightOrange = buildColorDefinition('orange', 'super-light-orange', ['#FFB357', '#FFCB7D']);
const BasicPurple = buildColorDefinition('purple', 'purple', ['#A352CC', '#B877D9'], true);
const DarkPurple = buildColorDefinition('purple', 'dark-purple', ['#7C2EA3', '#8F3BB8']);
const SemiDarkPurple = buildColorDefinition('purple', 'semi-dark-purple', ['#8F3BB8', '#A352CC']);
const LightPurple = buildColorDefinition('purple', 'light-purple', ['#B877D9', '#CA95E5']);
const SuperLightPurple = buildColorDefinition('purple', 'super-light-purple', ['#CA95E5', '#DEB6F2']);
const greens = [BasicGreen, DarkGreen, SemiDarkGreen, LightGreen, SuperLightGreen];
const yellows = [BasicYellow, DarkYellow, SemiDarkYellow, LightYellow, SuperLightYellow];
const reds = [BasicRed, DarkRed, SemiDarkRed, LightRed, SuperLightRed];
const blues = [BasicBlue, DarkBlue, SemiDarkBlue, LightBlue, SuperLightBlue];
const oranges = [BasicOrange, DarkOrange, SemiDarkOrange, LightOrange, SuperLightOrange];
const purples = [BasicPurple, DarkPurple, SemiDarkPurple, LightPurple, SuperLightPurple];
palette.set('green', greens);
palette.set('yellow', yellows);
palette.set('red', reds);
palette.set('blue', blues);
palette.set('orange', oranges);
palette.set('purple', purples);
return palette;
};
export const getNamedColorPalette = () => {
if (colorsPaletteInstance) {
return colorsPaletteInstance;
}
colorsPaletteInstance = buildNamedColorsPalette();
return colorsPaletteInstance;
};

View File

@@ -0,0 +1,40 @@
import { toHex, toHex0x } from './arithmeticFormatters';
describe('hex', () => {
it('positive integer', () => {
const str = toHex(100, 0);
expect(str).toBe('64');
});
it('negative integer', () => {
const str = toHex(-100, 0);
expect(str).toBe('-64');
});
it('positive float', () => {
const str = toHex(50.52, 1);
expect(str).toBe('32.8');
});
it('negative float', () => {
const str = toHex(-50.333, 2);
expect(str).toBe('-32.547AE147AE14');
});
});
describe('hex 0x', () => {
it('positive integeter', () => {
const str = toHex0x(7999, 0);
expect(str).toBe('0x1F3F');
});
it('negative integer', () => {
const str = toHex0x(-584, 0);
expect(str).toBe('-0x248');
});
it('positive float', () => {
const str = toHex0x(74.443, 3);
expect(str).toBe('0x4A.716872B020C4');
});
it('negative float', () => {
const str = toHex0x(-65.458, 1);
expect(str).toBe('-0x41.8');
});
});

View File

@@ -0,0 +1,43 @@
import { toFixed } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
export function toPercent(size: number, decimals: DecimalCount) {
if (size === null) {
return '';
}
return toFixed(size, decimals) + '%';
}
export function toPercentUnit(size: number, decimals: DecimalCount) {
if (size === null) {
return '';
}
return toFixed(100 * size, decimals) + '%';
}
export function toHex0x(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
const hexString = toHex(value, decimals);
if (hexString.substring(0, 1) === '-') {
return '-0x' + hexString.substring(1);
}
return '0x' + hexString;
}
export function toHex(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
return parseFloat(toFixed(value, decimals))
.toString(16)
.toUpperCase();
}
export function sci(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
return value.toExponential(decimals as number);
}

View File

@@ -0,0 +1,344 @@
import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats';
import {
dateTimeAsIso,
dateTimeAsUS,
dateTimeFromNow,
toClockMilliseconds,
toClockSeconds,
toDays,
toDurationInHoursMinutesSeconds,
toDurationInMilliseconds,
toDurationInSeconds,
toHours,
toMicroSeconds,
toMilliSeconds,
toMinutes,
toNanoSeconds,
toSeconds,
toTimeTicks,
} from './dateTimeFormatters';
import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters';
import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters';
export const getCategories = (): ValueFormatCategory[] => [
{
name: 'Misc',
formats: [
{ name: 'none', id: 'none', fn: toFixed },
{
name: 'short',
id: 'short',
fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']),
},
{ name: 'percent (0-100)', id: 'percent', fn: toPercent },
{ name: 'percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit },
{ name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') },
{ name: 'decibel', id: 'dB', fn: toFixedUnit('dB') },
{ name: 'hexadecimal (0x)', id: 'hex0x', fn: toHex0x },
{ name: 'hexadecimal', id: 'hex', fn: toHex },
{ name: 'scientific notation', id: 'sci', fn: sci },
{ name: 'locale format', id: 'locale', fn: locale },
],
},
{
name: 'Acceleration',
formats: [
{ name: 'Meters/sec²', id: 'accMS2', fn: toFixedUnit('m/sec²') },
{ name: 'Feet/sec²', id: 'accFS2', fn: toFixedUnit('f/sec²') },
{ name: 'G unit', id: 'accG', fn: toFixedUnit('g') },
],
},
{
name: 'Angle',
formats: [
{ name: 'Degrees (°)', id: 'degree', fn: toFixedUnit('°') },
{ name: 'Radians', id: 'radian', fn: toFixedUnit('rad') },
{ name: 'Gradian', id: 'grad', fn: toFixedUnit('grad') },
{ name: 'Arc Minutes', id: 'arcmin', fn: toFixedUnit('arcmin') },
{ name: 'Arc Seconds', id: 'arcsec', fn: toFixedUnit('arcsec') },
],
},
{
name: 'Area',
formats: [
{ name: 'Square Meters (m²)', id: 'areaM2', fn: toFixedUnit('m²') },
{ name: 'Square Feet (ft²)', id: 'areaF2', fn: toFixedUnit('ft²') },
{ name: 'Square Miles (mi²)', id: 'areaMI2', fn: toFixedUnit('mi²') },
],
},
{
name: 'Computation',
formats: [
{ name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') },
{ name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) },
{ name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) },
{ name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) },
{ name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) },
{ name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) },
{ name: 'ZFLOP/s', id: 'zflops', fn: decimalSIPrefix('FLOP/s', 7) },
{ name: 'YFLOP/s', id: 'yflops', fn: decimalSIPrefix('FLOP/s', 8) },
],
},
{
name: 'Concentration',
formats: [
{ name: 'parts-per-million (ppm)', id: 'ppm', fn: toFixedUnit('ppm') },
{ name: 'parts-per-billion (ppb)', id: 'conppb', fn: toFixedUnit('ppb') },
{ name: 'nanogram per cubic meter (ng/m³)', id: 'conngm3', fn: toFixedUnit('ng/m³') },
{ name: 'nanogram per normal cubic meter (ng/Nm³)', id: 'conngNm3', fn: toFixedUnit('ng/Nm³') },
{ name: 'microgram per cubic meter (μg/m³)', id: 'conμgm3', fn: toFixedUnit('μg/m³') },
{ name: 'microgram per normal cubic meter (μg/Nm³)', id: 'conμgNm3', fn: toFixedUnit('μg/Nm³') },
{ name: 'milligram per cubic meter (mg/m³)', id: 'conmgm3', fn: toFixedUnit('mg/m³') },
{ name: 'milligram per normal cubic meter (mg/Nm³)', id: 'conmgNm3', fn: toFixedUnit('mg/Nm³') },
{ name: 'gram per cubic meter (g/m³)', id: 'congm3', fn: toFixedUnit('g/m³') },
{ name: 'gram per normal cubic meter (g/Nm³)', id: 'congNm3', fn: toFixedUnit('g/Nm³') },
{ name: 'milligrams per decilitre (mg/dL)', id: 'conmgdL', fn: toFixedUnit('mg/dL') },
{ name: 'millimoles per litre (mmol/L)', id: 'conmmolL', fn: toFixedUnit('mmol/L') },
],
},
{
name: 'Currency',
formats: [
{ name: 'Dollars ($)', id: 'currencyUSD', fn: currency('$') },
{ name: 'Pounds (£)', id: 'currencyGBP', fn: currency('£') },
{ name: 'Euro (€)', id: 'currencyEUR', fn: currency('€') },
{ name: 'Yen (¥)', id: 'currencyJPY', fn: currency('¥') },
{ name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') },
{ name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') },
{ name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') },
{ name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') },
{ name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') },
{ name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') },
{ name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') },
{ name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') },
{ name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') },
{ name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') },
{ name: 'Bitcoin (฿)', id: 'currencyBTC', fn: currency('฿') },
{ name: 'South African Rand (R)', id: 'currencyZAR', fn: currency('R') },
{ name: 'Indian Rupee (₹)', id: 'currencyINR', fn: currency('₹') },
],
},
{
name: 'Data (IEC)',
formats: [
{ name: 'bits', id: 'bits', fn: binarySIPrefix('b') },
{ name: 'bytes', id: 'bytes', fn: binarySIPrefix('B') },
{ name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) },
{ name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) },
{ name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) },
{ name: 'tebibytes', id: 'tbytes', fn: binarySIPrefix('B', 4) },
{ name: 'pebibytes', id: 'pbytes', fn: binarySIPrefix('B', 5) },
],
},
{
name: 'Data (Metric)',
formats: [
{ name: 'bits', id: 'decbits', fn: decimalSIPrefix('b') },
{ name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
{ name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
{ name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },
{ name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) },
{ name: 'terabytes', id: 'dectbytes', fn: decimalSIPrefix('B', 4) },
{ name: 'petabytes', id: 'decpbytes', fn: decimalSIPrefix('B', 5) },
],
},
{
name: 'Data Rate',
formats: [
{ name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') },
{ name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') },
{ name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('Bs') },
{ name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) },
{ name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) },
{ name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) },
{ name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) },
{ name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) },
{ name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) },
{ name: 'terabytes/sec', id: 'TBs', fn: decimalSIPrefix('Bs', 4) },
{ name: 'terabits/sec', id: 'Tbits', fn: decimalSIPrefix('bps', 4) },
{ name: 'petabytes/sec', id: 'PBs', fn: decimalSIPrefix('Bs', 5) },
{ name: 'petabits/sec', id: 'Pbits', fn: decimalSIPrefix('bps', 5) },
],
},
{
name: 'Date & Time',
formats: [
{ name: 'YYYY-MM-DD HH:mm:ss', id: 'dateTimeAsIso', fn: dateTimeAsIso },
{ name: 'DD/MM/YYYY h:mm:ss a', id: 'dateTimeAsUS', fn: dateTimeAsUS },
{ name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow },
],
},
{
name: 'Energy',
formats: [
{ name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') },
{ name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) },
{ name: 'Megawatt (MW)', id: 'megwatt', fn: decimalSIPrefix('W', 2) },
{ name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) },
{ name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') },
{ name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') },
{ name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) },
{ name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') },
{ name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) },
{ name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') },
{ name: 'Watt-hour per Kilogram (Wh/kg)', id: 'watthperkg', fn: decimalSIPrefix('Wh/kg') },
{ name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) },
{ name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W-Min', 1) },
{ name: 'Ampere-hour (Ah)', id: 'amph', fn: decimalSIPrefix('Ah') },
{ name: 'Kiloampere-hour (kAh)', id: 'kamph', fn: decimalSIPrefix('Ah', 1) },
{ name: 'Milliampere-hour (mAh)', id: 'mamph', fn: decimalSIPrefix('Ah', -1) },
{ name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') },
{ name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') },
{ name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') },
{ name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) },
{ name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) },
{ name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') },
{ name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) },
{ name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) },
{ name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') },
{ name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') },
{ name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') },
],
},
{
name: 'Flow',
formats: [
{ name: 'Gallons/min (gpm)', id: 'flowgpm', fn: toFixedUnit('gpm') },
{ name: 'Cubic meters/sec (cms)', id: 'flowcms', fn: toFixedUnit('cms') },
{ name: 'Cubic feet/sec (cfs)', id: 'flowcfs', fn: toFixedUnit('cfs') },
{ name: 'Cubic feet/min (cfm)', id: 'flowcfm', fn: toFixedUnit('cfm') },
{ name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('L/h') },
{ name: 'Litre/min (L/min)', id: 'flowlpm', fn: toFixedUnit('L/min') },
{ name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') },
{ name: 'Lux (lx)', id: 'lux', fn: toFixedUnit('lux') },
],
},
{
name: 'Force',
formats: [
{ name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') },
{ name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) },
{ name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') },
{ name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) },
],
},
{
name: 'Hash Rate',
formats: [
{ name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') },
{ name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) },
{ name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) },
{ name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) },
{ name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) },
{ name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) },
{ name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) },
],
},
{
name: 'Mass',
formats: [
{ name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) },
{ name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') },
{ name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) },
{ name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') },
],
},
{
name: 'length',
formats: [
{ name: 'millimeter (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) },
{ name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') },
{ name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') },
{ name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) },
{ name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') },
],
},
{
name: 'Pressure',
formats: [
{ name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) },
{ name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') },
{ name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) },
{ name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') },
{ name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') },
{ name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') },
{ name: 'PSI', id: 'pressurepsi', fn: scaledUnits(1000, ['psi', 'ksi', 'Mpsi']) },
],
},
{
name: 'Radiation',
formats: [
{ name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') },
{ name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') },
{ name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') },
{ name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') },
{ name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') },
{ name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') },
{ name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') },
{ name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') },
{ name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') },
],
},
{
name: 'Temperature',
formats: [
{ name: 'Celsius (°C)', id: 'celsius', fn: toFixedUnit('°C') },
{ name: 'Fahrenheit (°F)', id: 'fahrenheit', fn: toFixedUnit('°F') },
{ name: 'Kelvin (K)', id: 'kelvin', fn: toFixedUnit('K') },
],
},
{
name: 'Time',
formats: [
{ name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') },
{ name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds },
{ name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds },
{ name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds },
{ name: 'seconds (s)', id: 's', fn: toSeconds },
{ name: 'minutes (m)', id: 'm', fn: toMinutes },
{ name: 'hours (h)', id: 'h', fn: toHours },
{ name: 'days (d)', id: 'd', fn: toDays },
{ name: 'duration (ms)', id: 'dtdurationms', fn: toDurationInMilliseconds },
{ name: 'duration (s)', id: 'dtdurations', fn: toDurationInSeconds },
{ name: 'duration (hh:mm:ss)', id: 'dthms', fn: toDurationInHoursMinutesSeconds },
{ name: 'Timeticks (s/100)', id: 'timeticks', fn: toTimeTicks },
{ name: 'clock (ms)', id: 'clockms', fn: toClockMilliseconds },
{ name: 'clock (s)', id: 'clocks', fn: toClockSeconds },
],
},
{
name: 'Throughput',
formats: [
{ name: 'counts/sec (cps)', id: 'cps', fn: simpleCountUnit('cps') },
{ name: 'ops/sec (ops)', id: 'ops', fn: simpleCountUnit('ops') },
{ name: 'requests/sec (rps)', id: 'reqps', fn: simpleCountUnit('reqps') },
{ name: 'reads/sec (rps)', id: 'rps', fn: simpleCountUnit('rps') },
{ name: 'writes/sec (wps)', id: 'wps', fn: simpleCountUnit('wps') },
{ name: 'I/O ops/sec (iops)', id: 'iops', fn: simpleCountUnit('iops') },
{ name: 'counts/min (cpm)', id: 'cpm', fn: simpleCountUnit('cpm') },
{ name: 'ops/min (opm)', id: 'opm', fn: simpleCountUnit('opm') },
{ name: 'reads/min (rpm)', id: 'rpm', fn: simpleCountUnit('rpm') },
{ name: 'writes/min (wpm)', id: 'wpm', fn: simpleCountUnit('wpm') },
],
},
{
name: 'Velocity',
formats: [
{ name: 'meters/second (m/s)', id: 'velocityms', fn: toFixedUnit('m/s') },
{ name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') },
{ name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') },
{ name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') },
],
},
{
name: 'Volume',
formats: [
{ name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) },
{ name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') },
{ name: 'cubic meter', id: 'm3', fn: toFixedUnit('m³') },
{ name: 'Normal cubic meter', id: 'Nm3', fn: toFixedUnit('Nm³') },
{ name: 'cubic decimeter', id: 'dm3', fn: toFixedUnit('dm³') },
{ name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') },
],
},
];

View File

@@ -0,0 +1,244 @@
import {
dateTimeAsIso,
dateTimeAsUS,
dateTimeFromNow,
Interval,
toClock,
toDuration,
toDurationInMilliseconds,
toDurationInSeconds,
toDurationInHoursMinutesSeconds,
} from './dateTimeFormatters';
import { toUtc, dateTime } from '../datetime/moment_wrapper';
describe('date time formats', () => {
const epoch = 1505634997920;
const utcTime = toUtc(epoch);
const browserTime = dateTime(epoch);
it('should format as iso date', () => {
const expected = browserTime.format('YYYY-MM-DD HH:mm:ss');
const actual = dateTimeAsIso(epoch, 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as iso date (in UTC)', () => {
const expected = utcTime.format('YYYY-MM-DD HH:mm:ss');
const actual = dateTimeAsIso(epoch, 0, 0, true);
expect(actual).toBe(expected);
});
it('should format as iso date and skip date when today', () => {
const now = dateTime();
const expected = now.format('HH:mm:ss');
const actual = dateTimeAsIso(now.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as iso date (in UTC) and skip date when today', () => {
const now = toUtc();
const expected = now.format('HH:mm:ss');
const actual = dateTimeAsIso(now.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
});
it('should format as US date', () => {
const expected = browserTime.format('MM/DD/YYYY h:mm:ss a');
const actual = dateTimeAsUS(epoch, 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as US date (in UTC)', () => {
const expected = utcTime.format('MM/DD/YYYY h:mm:ss a');
const actual = dateTimeAsUS(epoch, 0, 0, true);
expect(actual).toBe(expected);
});
it('should format as US date and skip date when today', () => {
const now = dateTime();
const expected = now.format('h:mm:ss a');
const actual = dateTimeAsUS(now.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as US date (in UTC) and skip date when today', () => {
const now = toUtc();
const expected = now.format('h:mm:ss a');
const actual = dateTimeAsUS(now.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
});
it('should format as from now with days', () => {
const daysAgo = dateTime().add(-7, 'd');
const expected = '7 days ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as from now with days (in UTC)', () => {
const daysAgo = toUtc().add(-7, 'd');
const expected = '7 days ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
});
it('should format as from now with minutes', () => {
const daysAgo = dateTime().add(-2, 'm');
const expected = '2 minutes ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
expect(actual).toBe(expected);
});
it('should format as from now with minutes (in UTC)', () => {
const daysAgo = toUtc().add(-2, 'm');
const expected = '2 minutes ago';
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
expect(actual).toBe(expected);
});
});
describe('duration', () => {
it('0 milliseconds', () => {
const str = toDurationInMilliseconds(0, 0);
expect(str).toBe('0 milliseconds');
});
it('1 millisecond', () => {
const str = toDurationInMilliseconds(1, 0);
expect(str).toBe('1 millisecond');
});
it('-1 millisecond', () => {
const str = toDurationInMilliseconds(-1, 0);
expect(str).toBe('1 millisecond ago');
});
it('seconds', () => {
const str = toDurationInSeconds(1, 0);
expect(str).toBe('1 second');
});
it('minutes', () => {
const str = toDuration(1, 0, Interval.Minute);
expect(str).toBe('1 minute');
});
it('hours', () => {
const str = toDuration(1, 0, Interval.Hour);
expect(str).toBe('1 hour');
});
it('days', () => {
const str = toDuration(1, 0, Interval.Day);
expect(str).toBe('1 day');
});
it('weeks', () => {
const str = toDuration(1, 0, Interval.Week);
expect(str).toBe('1 week');
});
it('months', () => {
const str = toDuration(1, 0, Interval.Month);
expect(str).toBe('1 month');
});
it('years', () => {
const str = toDuration(1, 0, Interval.Year);
expect(str).toBe('1 year');
});
it('decimal days', () => {
const str = toDuration(1.5, 2, Interval.Day);
expect(str).toBe('1 day, 12 hours, 0 minutes');
});
it('decimal months', () => {
const str = toDuration(1.5, 3, Interval.Month);
expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
});
it('no decimals', () => {
const str = toDuration(38898367008, 0, Interval.Millisecond);
expect(str).toBe('1 year');
});
it('1 decimal', () => {
const str = toDuration(38898367008, 1, Interval.Millisecond);
expect(str).toBe('1 year, 2 months');
});
it('too many decimals', () => {
const str = toDuration(38898367008, 20, Interval.Millisecond);
expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
});
it('floating point error', () => {
const str = toDuration(36993906007, 8, Interval.Millisecond);
expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
});
it('1 dthms', () => {
const str = toDurationInHoursMinutesSeconds(1);
expect(str).toBe('00:00:01');
});
it('-1 dthms', () => {
const str = toDurationInHoursMinutesSeconds(-1);
expect(str).toBe('00:00:01 ago');
});
it('0 dthms', () => {
const str = toDurationInHoursMinutesSeconds(0);
expect(str).toBe('00:00:00');
});
});
describe('clock', () => {
it('size less than 1 second', () => {
const str = toClock(999, 0);
expect(str).toBe('999ms');
});
describe('size less than 1 minute', () => {
it('default', () => {
const str = toClock(59999);
expect(str).toBe('59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(59999, 0);
expect(str).toBe('59s');
});
});
describe('size less than 1 hour', () => {
it('default', () => {
const str = toClock(3599999);
expect(str).toBe('59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(3599999, 0);
expect(str).toBe('59m');
});
it('decimals equals 1', () => {
const str = toClock(3599999, 1);
expect(str).toBe('59m:59s');
});
});
describe('size greater than or equal 1 hour', () => {
it('default', () => {
const str = toClock(7199999);
expect(str).toBe('01h:59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(7199999, 0);
expect(str).toBe('01h');
});
it('decimals equals 1', () => {
const str = toClock(7199999, 1);
expect(str).toBe('01h:59m');
});
it('decimals equals 2', () => {
const str = toClock(7199999, 2);
expect(str).toBe('01h:59m:59s');
});
});
describe('size greater than or equal 1 day', () => {
it('default', () => {
const str = toClock(89999999);
expect(str).toBe('24h:59m:59s:999ms');
});
it('decimals equals 0', () => {
const str = toClock(89999999, 0);
expect(str).toBe('24h');
});
it('decimals equals 1', () => {
const str = toClock(89999999, 1);
expect(str).toBe('24h:59m');
});
it('decimals equals 2', () => {
const str = toClock(89999999, 2);
expect(str).toBe('24h:59m:59s');
});
});
});

View File

@@ -0,0 +1,334 @@
import { toDuration as duration, toUtc, dateTime } from '../datetime/moment_wrapper';
import { toFixed, toFixedScaled } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
interface IntervalsInSeconds {
[interval: string]: number;
}
export enum Interval {
Year = 'year',
Month = 'month',
Week = 'week',
Day = 'day',
Hour = 'hour',
Minute = 'minute',
Second = 'second',
Millisecond = 'millisecond',
}
const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
[Interval.Year]: 31536000,
[Interval.Month]: 2592000,
[Interval.Week]: 604800,
[Interval.Day]: 86400,
[Interval.Hour]: 3600,
[Interval.Minute]: 60,
[Interval.Second]: 1,
[Interval.Millisecond]: 0.001,
};
export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' ns';
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
} else if (Math.abs(size) < 1000000000) {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms');
} else if (Math.abs(size) < 60000000000) {
return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s');
} else {
return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min');
}
}
export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' µs';
} else if (Math.abs(size) < 1000000) {
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
} else {
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s');
}
}
export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 1000) {
return toFixed(size, decimals) + ' ms';
} else if (Math.abs(size) < 60000) {
// Less than 1 min
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
} else if (Math.abs(size) < 3600000) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
} else if (Math.abs(size) < 86400000) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
} else if (Math.abs(size) < 31536000000) {
// Less than one year, divide in days
return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
}
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
}
export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount {
if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) {
return value1 - value2;
}
return undefined;
}
export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
// Less than 1 µs, divide in ns
if (Math.abs(size) < 0.000001) {
return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns');
}
// Less than 1 ms, divide in µs
if (Math.abs(size) < 0.001) {
return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs');
}
// Less than 1 second, divide in ms
if (Math.abs(size) < 1) {
return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms');
}
if (Math.abs(size) < 60) {
return toFixed(size, decimals) + ' s';
} else if (Math.abs(size) < 3600) {
// Less than 1 hour, divide in minutes
return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
} else if (Math.abs(size) < 86400) {
// Less than one day, divide in hours
return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
} else if (Math.abs(size) < 604800) {
// Less than one week, divide in days
return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
} else if (Math.abs(size) < 31536000) {
// Less than one year, divide in week
return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
}
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
}
export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 60) {
return toFixed(size, decimals) + ' min';
} else if (Math.abs(size) < 1440) {
return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
} else if (Math.abs(size) < 10080) {
return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day');
} else if (Math.abs(size) < 604800) {
return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week');
} else {
return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year');
}
}
export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 24) {
return toFixed(size, decimals) + ' hour';
} else if (Math.abs(size) < 168) {
return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
} else if (Math.abs(size) < 8760) {
return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week');
} else {
return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year');
}
}
export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) {
if (size === null) {
return '';
}
if (Math.abs(size) < 7) {
return toFixed(size, decimals) + ' day';
} else if (Math.abs(size) < 365) {
return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
} else {
return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year');
}
}
export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string {
if (size === null) {
return '';
}
if (size === 0) {
return '0 ' + timeScale + 's';
}
if (size < 0) {
return toDuration(-size, decimals, timeScale) + ' ago';
}
const units = [
{ long: Interval.Year },
{ long: Interval.Month },
{ long: Interval.Week },
{ long: Interval.Day },
{ long: Interval.Hour },
{ long: Interval.Minute },
{ long: Interval.Second },
{ long: Interval.Millisecond },
];
// convert $size to milliseconds
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
const strings = [];
// after first value >= 1 print only $decimals more
let decrementDecimals = false;
let decimalsCount = 0;
if (decimals !== null || decimals !== undefined) {
decimalsCount = decimals as number;
}
for (let i = 0; i < units.length && decimalsCount >= 0; i++) {
const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
const value = size / interval;
if (value >= 1 || decrementDecimals) {
decrementDecimals = true;
const floor = Math.floor(value);
const unit = units[i].long + (floor !== 1 ? 's' : '');
strings.push(floor + ' ' + unit);
size = size % interval;
decimalsCount--;
}
}
return strings.join(', ');
}
export function toClock(size: number, decimals?: DecimalCount) {
if (size === null) {
return '';
}
// < 1 second
if (size < 1000) {
return toUtc(size).format('SSS\\m\\s');
}
// < 1 minute
if (size < 60000) {
let format = 'ss\\s:SSS\\m\\s';
if (decimals === 0) {
format = 'ss\\s';
}
return toUtc(size).format(format);
}
// < 1 hour
if (size < 3600000) {
let format = 'mm\\m:ss\\s:SSS\\m\\s';
if (decimals === 0) {
format = 'mm\\m';
} else if (decimals === 1) {
format = 'mm\\m:ss\\s';
}
return toUtc(size).format(format);
}
let format = 'mm\\m:ss\\s:SSS\\m\\s';
const hours = `${('0' + Math.floor(duration(size, 'milliseconds').asHours())).slice(-2)}h`;
if (decimals === 0) {
format = '';
} else if (decimals === 1) {
format = 'mm\\m';
} else if (decimals === 2) {
format = 'mm\\m:ss\\s';
}
return format ? `${hours}:${toUtc(size).format(format)}` : hours;
}
export function toDurationInMilliseconds(size: number, decimals: DecimalCount) {
return toDuration(size, decimals, Interval.Millisecond);
}
export function toDurationInSeconds(size: number, decimals: DecimalCount) {
return toDuration(size, decimals, Interval.Second);
}
export function toDurationInHoursMinutesSeconds(size: number): string {
if (size < 0) {
return toDurationInHoursMinutesSeconds(-size) + ' ago';
}
const strings = [];
const numHours = Math.floor(size / 3600);
const numMinutes = Math.floor((size % 3600) / 60);
const numSeconds = Math.floor((size % 3600) % 60);
numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours);
numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes);
numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds);
return strings.join(':');
}
export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) {
return toSeconds(size / 100, decimals, scaledDecimals);
}
export function toClockMilliseconds(size: number, decimals: DecimalCount) {
return toClock(size, decimals);
}
export function toClockSeconds(size: number, decimals: DecimalCount) {
return toClock(size * 1000, decimals);
}
export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? toUtc(value) : dateTime(value);
if (dateTime().isSame(value, 'day')) {
return time.format('HH:mm:ss');
}
return time.format('YYYY-MM-DD HH:mm:ss');
}
export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? toUtc(value) : dateTime(value);
if (dateTime().isSame(value, 'day')) {
return time.format('h:mm:ss a');
}
return time.format('MM/DD/YYYY h:mm:ss a');
}
export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) {
const time = isUtc ? toUtc(value) : dateTime(value);
return time.fromNow();
}

View File

@@ -0,0 +1 @@
export * from './valueFormats';

View File

@@ -0,0 +1,7 @@
import { currency } from './symbolFormatters';
describe('Currency', () => {
it('should format as usd', () => {
expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K');
});
});

View File

@@ -0,0 +1,31 @@
import { scaledUnits } from './valueFormats';
import { DecimalCount } from '../types/displayValue';
export function currency(symbol: string) {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
const scaled = scaler(size, decimals, scaledDecimals);
return symbol + scaled;
};
}
export function binarySIPrefix(unit: string, offset = 0) {
const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
const units = prefixes.map(p => {
return ' ' + p + unit;
});
return scaledUnits(1024, units);
}
export function decimalSIPrefix(unit: string, offset = 0) {
let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
prefixes = prefixes.slice(3 + (offset || 0));
const units = prefixes.map(p => {
return ' ' + p + unit;
});
return scaledUnits(1000, units);
}

View File

@@ -0,0 +1,56 @@
import { toFixed, getValueFormat, scaledUnits } from './valueFormats';
describe('valueFormats', () => {
describe('format edge cases', () => {
const negInf = Number.NEGATIVE_INFINITY.toLocaleString();
const posInf = Number.POSITIVE_INFINITY.toLocaleString();
it('toFixed should handle non number input gracefully', () => {
expect(toFixed(NaN)).toBe('NaN');
expect(toFixed(Number.NEGATIVE_INFINITY)).toBe(negInf);
expect(toFixed(Number.POSITIVE_INFINITY)).toBe(posInf);
});
it('scaledUnits should handle non number input gracefully', () => {
const disp = scaledUnits(5, ['a', 'b', 'c']);
expect(disp(NaN)).toBe('NaN');
expect(disp(Number.NEGATIVE_INFINITY)).toBe(negInf);
expect(disp(Number.POSITIVE_INFINITY)).toBe(posInf);
});
});
describe('toFixed and negative decimals', () => {
it('should treat as zero decimals', () => {
const str = toFixed(186.123, -2);
expect(str).toBe('186');
});
});
describe('ms format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('ms')(10000086.123, 1, null);
expect(str).toBe('2.8 hour');
});
});
describe('kbytes format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('kbytes')(10000000, 3, null);
expect(str).toBe('9.537 GiB');
});
});
describe('deckbytes format when scaled decimals is null do not use it', () => {
it('should use specified decimals', () => {
const str = getValueFormat('deckbytes')(10000000, 3, null);
expect(str).toBe('10.000 GB');
});
});
describe('ms format when scaled decimals is 0', () => {
it('should use scaledDecimals and add 3', () => {
const str = getValueFormat('ms')(1200, 0, 0);
expect(str).toBe('1.200 s');
});
});
});

View File

@@ -0,0 +1,180 @@
import { getCategories } from './categories';
import { DecimalCount } from '../types/displayValue';
export type ValueFormatter = (
value: number,
decimals?: DecimalCount,
scaledDecimals?: DecimalCount,
isUtc?: boolean
) => string;
export interface ValueFormat {
name: string;
id: string;
fn: ValueFormatter;
}
export interface ValueFormatCategory {
name: string;
formats: ValueFormat[];
}
interface ValueFormatterIndex {
[id: string]: ValueFormatter;
}
// Globals & formats cache
let categories: ValueFormatCategory[] = [];
const index: ValueFormatterIndex = {};
let hasBuiltIndex = false;
export function toFixed(value: number, decimals?: DecimalCount): string {
if (value === null) {
return '';
}
if (value === Number.NEGATIVE_INFINITY || value === Number.POSITIVE_INFINITY) {
return value.toLocaleString();
}
const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
const formatted = String(Math.round(value * factor) / factor);
// if exponent return directly
if (formatted.indexOf('e') !== -1 || value === 0) {
return formatted;
}
// If tickDecimals was specified, ensure that we have exactly that
// much precision; otherwise default to the value's own precision.
if (decimals != null) {
const decimalPos = formatted.indexOf('.');
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
}
}
return formatted;
}
export function toFixedScaled(
value: number,
decimals: DecimalCount,
scaledDecimals: DecimalCount,
additionalDecimals: number,
ext?: string
) {
if (scaledDecimals === null || scaledDecimals === undefined) {
return toFixed(value, decimals) + ext;
} else {
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
}
return toFixed(value, decimals) + ext;
}
export function toFixedUnit(unit: string): ValueFormatter {
return (size: number, decimals?: DecimalCount) => {
if (size === null) {
return '';
}
return toFixed(size, decimals) + ' ' + unit;
};
}
// Formatter which scales the unit string geometrically according to the given
// numeric factor. Repeatedly scales the value down by the factor until it is
// less than the factor in magnitude, or the end of the array is reached.
export function scaledUnits(factor: number, extArray: string[]) {
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
if (size === Number.NEGATIVE_INFINITY || size === Number.POSITIVE_INFINITY || isNaN(size)) {
return size.toLocaleString();
}
let steps = 0;
const limit = extArray.length;
while (Math.abs(size) >= factor) {
steps++;
size /= factor;
if (steps >= limit) {
return 'NA';
}
}
if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) {
decimals = scaledDecimals + 3 * steps;
}
return toFixed(size, decimals) + extArray[steps];
};
}
export function locale(value: number, decimals: DecimalCount) {
if (value == null) {
return '';
}
return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number });
}
export function simpleCountUnit(symbol: string) {
const units = ['', 'K', 'M', 'B', 'T'];
const scaler = scaledUnits(1000, units);
return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => {
if (size === null) {
return '';
}
const scaled = scaler(size, decimals, scaledDecimals);
return scaled + ' ' + symbol;
};
}
function buildFormats() {
categories = getCategories();
for (const cat of categories) {
for (const format of cat.formats) {
index[format.id] = format.fn;
}
}
hasBuiltIndex = true;
}
export function getValueFormat(id: string): ValueFormatter {
if (!hasBuiltIndex) {
buildFormats();
}
return index[id];
}
export function getValueFormatterIndex(): ValueFormatterIndex {
if (!hasBuiltIndex) {
buildFormats();
}
return index;
}
export function getValueFormats() {
if (!hasBuiltIndex) {
buildFormats();
}
return categories.map(cat => {
return {
text: cat.name,
submenu: cat.formats.map(format => {
return {
text: format.name,
value: format.id,
};
}),
};
});
}