mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
FieldConfig: add thresholds and color modes (#21273)
This commit is contained in:
parent
36aad1c101
commit
d9e9843a10
@ -52,7 +52,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -169,7 +169,7 @@
|
||||
"orientation": "vertical",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -244,7 +244,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -351,7 +351,7 @@
|
||||
"orientation": "vertical",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -430,7 +430,7 @@
|
||||
"orientation": "vertical",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
|
@ -58,7 +58,7 @@
|
||||
"orientation": "vertical",
|
||||
"showUnfilled": false
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "C",
|
||||
@ -159,7 +159,7 @@
|
||||
"orientation": "vertical",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "H",
|
||||
@ -249,7 +249,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": false
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
@ -325,7 +325,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
@ -401,7 +401,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": false
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
@ -477,7 +477,7 @@
|
||||
"orientation": "horizontal",
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Inside",
|
||||
|
@ -69,7 +69,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "A longer title",
|
||||
@ -160,7 +160,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "AB",
|
||||
@ -251,7 +251,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -336,7 +336,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -424,7 +424,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
@ -512,7 +512,7 @@
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "6.6.0-pre",
|
||||
"pluginVersion": "6.5.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
|
@ -1,7 +1,31 @@
|
||||
import { getDisplayProcessor, getColorFromThreshold } from './displayProcessor';
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
|
||||
import { ValueMapping, MappingType } from '../types/valueMapping';
|
||||
import { FieldType } from '../types';
|
||||
import { FieldType, Threshold, GrafanaTheme, Field, FieldConfig, ThresholdsMode } from '../types';
|
||||
import { getScaleCalculator, sortThresholds } from './scale';
|
||||
import { ArrayVector } from '../vector';
|
||||
import { validateFieldConfig } from './fieldOverrides';
|
||||
|
||||
function getDisplayProcessorFromConfig(config: FieldConfig) {
|
||||
return getDisplayProcessor({
|
||||
field: {
|
||||
config,
|
||||
type: FieldType.number,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getColorFromThreshold(value: number, steps: Threshold[], theme?: GrafanaTheme): string {
|
||||
const field: Field = {
|
||||
name: 'test',
|
||||
config: { thresholds: { mode: ThresholdsMode.Absolute, steps: sortThresholds(steps) } },
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([]),
|
||||
};
|
||||
validateFieldConfig(field.config!);
|
||||
const calc = getScaleCalculator(field, theme);
|
||||
return calc(value).color!;
|
||||
}
|
||||
|
||||
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
|
||||
processors.forEach(processor => {
|
||||
@ -20,10 +44,10 @@ describe('Process simple display values', () => {
|
||||
getDisplayProcessor(),
|
||||
|
||||
// Add a simple option that is not used (uses a different base class)
|
||||
getDisplayProcessor({ config: { min: 0, max: 100 } }),
|
||||
getDisplayProcessorFromConfig({ min: 0, max: 100 }),
|
||||
|
||||
// Add a simple option that is not used (uses a different base class)
|
||||
getDisplayProcessor({ config: { unit: 'locale' } }),
|
||||
getDisplayProcessorFromConfig({ unit: 'locale' }),
|
||||
];
|
||||
|
||||
it('support null', () => {
|
||||
@ -108,7 +132,7 @@ describe('Format value', () => {
|
||||
it('should return if value isNaN', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = 'N/A';
|
||||
const instance = getDisplayProcessor({ config: { mappings: valueMappings } });
|
||||
const instance = getDisplayProcessorFromConfig({ mappings: valueMappings });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -119,7 +143,7 @@ describe('Format value', () => {
|
||||
const valueMappings: ValueMapping[] = [];
|
||||
const value = '6';
|
||||
|
||||
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -132,7 +156,7 @@ describe('Format value', () => {
|
||||
{ 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 instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
|
||||
const result = instance(value);
|
||||
|
||||
@ -141,20 +165,20 @@ describe('Format value', () => {
|
||||
|
||||
it('should set auto decimals, 1 significant', () => {
|
||||
const value = 3.23;
|
||||
const instance = getDisplayProcessor({ config: { decimals: null } });
|
||||
const instance = getDisplayProcessorFromConfig({ 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 } });
|
||||
const instance = getDisplayProcessorFromConfig({ 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' } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 2, unit: 'bytes' });
|
||||
const disp = instance(value);
|
||||
expect(disp.text).toEqual('95.40');
|
||||
expect(disp.suffix).toEqual(' MiB');
|
||||
@ -166,7 +190,7 @@ describe('Format value', () => {
|
||||
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
|
||||
];
|
||||
const value = '11';
|
||||
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
|
||||
expect(instance(value).text).toEqual('1-20');
|
||||
});
|
||||
@ -176,7 +200,7 @@ describe('Format value', () => {
|
||||
{ id: 1, operator: '', text: '', type: MappingType.ValueToText, value: '1' },
|
||||
];
|
||||
const value = '1';
|
||||
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
|
||||
expect(instance(value).text).toEqual('');
|
||||
expect(instance(value).numeric).toEqual(1);
|
||||
@ -188,7 +212,7 @@ describe('Format value', () => {
|
||||
|
||||
it('with value 1000 and unit short', () => {
|
||||
const value = 1000;
|
||||
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
|
||||
const disp = instance(value);
|
||||
expect(disp.text).toEqual('1.000');
|
||||
expect(disp.suffix).toEqual(' K');
|
||||
@ -196,7 +220,7 @@ describe('Format value', () => {
|
||||
|
||||
it('with value 1200 and unit short', () => {
|
||||
const value = 1200;
|
||||
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
|
||||
const disp = instance(value);
|
||||
expect(disp.text).toEqual('1.200');
|
||||
expect(disp.suffix).toEqual(' K');
|
||||
@ -204,7 +228,7 @@ describe('Format value', () => {
|
||||
|
||||
it('with value 1250 and unit short', () => {
|
||||
const value = 1250;
|
||||
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
|
||||
const disp = instance(value);
|
||||
expect(disp.text).toEqual('1.250');
|
||||
expect(disp.suffix).toEqual(' K');
|
||||
@ -212,7 +236,7 @@ describe('Format value', () => {
|
||||
|
||||
it('with value 10000000 and unit short', () => {
|
||||
const value = 1000000;
|
||||
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
|
||||
const disp = instance(value);
|
||||
expect(disp.text).toEqual('1.000');
|
||||
expect(disp.suffix).toEqual(' Mil');
|
||||
@ -222,10 +246,12 @@ describe('Format value', () => {
|
||||
describe('Date display options', () => {
|
||||
it('should format UTC dates', () => {
|
||||
const processor = getDisplayProcessor({
|
||||
type: FieldType.time,
|
||||
isUtc: true,
|
||||
config: {
|
||||
unit: 'xyz', // ignore non-date formats
|
||||
field: {
|
||||
type: FieldType.time,
|
||||
config: {
|
||||
unit: 'xyz', // ignore non-date formats
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(processor(0).text).toEqual('1970-01-01 00:00:00');
|
||||
@ -233,10 +259,12 @@ describe('Date display options', () => {
|
||||
|
||||
it('should pick configured time format', () => {
|
||||
const processor = getDisplayProcessor({
|
||||
type: FieldType.time,
|
||||
isUtc: true,
|
||||
config: {
|
||||
unit: 'dateTimeAsUS', // A configurable date format
|
||||
field: {
|
||||
type: FieldType.time,
|
||||
config: {
|
||||
unit: 'dateTimeAsUS', // ignore non-date formats
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(processor(0).text).toEqual('01/01/1970 12:00:00 am');
|
||||
@ -244,10 +272,12 @@ describe('Date display options', () => {
|
||||
|
||||
it('respect the configured date format', () => {
|
||||
const processor = getDisplayProcessor({
|
||||
type: FieldType.time,
|
||||
isUtc: true,
|
||||
config: {
|
||||
unit: 'time:YYYY',
|
||||
field: {
|
||||
type: FieldType.time,
|
||||
config: {
|
||||
unit: 'time:YYYY', // ignore non-date formats
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(processor(0).text).toEqual('1970');
|
||||
|
@ -1,22 +1,18 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
|
||||
// Utils
|
||||
import { getColorFromHexRgbOrName } from '../utils/namedColorsPalette';
|
||||
|
||||
// Types
|
||||
import { FieldConfig, FieldType } from '../types/dataFrame';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||
import { Field, FieldType } from '../types/dataFrame';
|
||||
import { GrafanaTheme } 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 { DEFAULT_DATE_TIME_FORMAT } from '../datetime';
|
||||
import { KeyValue } from '../types';
|
||||
import { getScaleCalculator } from './scale';
|
||||
|
||||
interface DisplayProcessorOptions {
|
||||
type?: FieldType;
|
||||
config?: FieldConfig;
|
||||
field: Partial<Field>;
|
||||
|
||||
// Context
|
||||
isUtc?: boolean;
|
||||
@ -31,80 +27,81 @@ const timeFormats: KeyValue<boolean> = {
|
||||
};
|
||||
|
||||
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
|
||||
if (options && !_.isEmpty(options)) {
|
||||
const field = options.config ? options.config : {};
|
||||
if (!options || _.isEmpty(options) || !options.field) {
|
||||
return toStringProcessor;
|
||||
}
|
||||
const { field } = options;
|
||||
const config = field.config ?? {};
|
||||
|
||||
if (options.type === FieldType.time) {
|
||||
if (field.unit && timeFormats[field.unit]) {
|
||||
// Currently selected unit is valid for time fields
|
||||
} else if (field.unit && field.unit.startsWith('time:')) {
|
||||
// Also OK
|
||||
} else {
|
||||
field.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`;
|
||||
if (field.type === FieldType.time) {
|
||||
if (config.unit && timeFormats[config.unit]) {
|
||||
// Currently selected unit is valid for time fields
|
||||
} else if (config.unit && config.unit.startsWith('time:')) {
|
||||
// Also OK
|
||||
} else {
|
||||
config.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`;
|
||||
}
|
||||
}
|
||||
|
||||
const formatFunc = getValueFormat(config.unit || 'none');
|
||||
const scaleFunc = getScaleCalculator(field as Field, options.theme);
|
||||
|
||||
return (value: any) => {
|
||||
const { mappings } = config;
|
||||
|
||||
let text = _.toString(value);
|
||||
let numeric = toNumber(value);
|
||||
let prefix: string | undefined = undefined;
|
||||
let suffix: string | undefined = undefined;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const formatFunc = getValueFormat(field.unit || 'none');
|
||||
if (!isNaN(numeric)) {
|
||||
if (shouldFormat && !_.isBoolean(value)) {
|
||||
const { decimals, scaledDecimals } = getDecimalsForValue(value, config.decimals);
|
||||
const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
|
||||
text = v.text;
|
||||
suffix = v.suffix;
|
||||
prefix = v.prefix;
|
||||
|
||||
return (value: any) => {
|
||||
const { theme } = options;
|
||||
const { mappings, thresholds } = field;
|
||||
let color;
|
||||
|
||||
let text = _.toString(value);
|
||||
let numeric = toNumber(value);
|
||||
let prefix: string | undefined = undefined;
|
||||
let suffix: string | undefined = undefined;
|
||||
|
||||
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);
|
||||
const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
|
||||
text = v.text;
|
||||
suffix = v.suffix;
|
||||
prefix = v.prefix;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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 the value along with scale info
|
||||
if (text) {
|
||||
return { text, numeric, prefix, suffix, ...scaleFunc(numeric) };
|
||||
}
|
||||
return { text, numeric, color, prefix, suffix };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return toStringProcessor;
|
||||
if (!text) {
|
||||
if (config.noValue) {
|
||||
text = config.noValue;
|
||||
} else {
|
||||
text = ''; // No data?
|
||||
}
|
||||
}
|
||||
return { text, numeric, prefix, suffix };
|
||||
};
|
||||
}
|
||||
|
||||
/** Will return any value as a number or NaN */
|
||||
@ -125,29 +122,6 @@ 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);
|
||||
}
|
||||
|
||||
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
|
||||
if (_.isNumber(decimalOverride)) {
|
||||
// It's important that scaledDecimals is null here
|
||||
|
@ -2,9 +2,9 @@ import merge from 'lodash/merge';
|
||||
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
||||
import { toDataFrame } from '../dataframe/processDataFrame';
|
||||
import { ReducerID } from '../transformations/fieldReducer';
|
||||
import { Threshold } from '../types/threshold';
|
||||
import { ThresholdsMode } from '../types/thresholds';
|
||||
import { GrafanaTheme } from '../types/theme';
|
||||
import { MappingType } from '../types';
|
||||
import { MappingType, FieldConfig } from '../types';
|
||||
import { setFieldConfigDefaults } from './fieldOverrides';
|
||||
|
||||
describe('FieldDisplay', () => {
|
||||
@ -63,34 +63,37 @@ describe('FieldDisplay', () => {
|
||||
});
|
||||
|
||||
it('should restore -Infinity value for base threshold', () => {
|
||||
const field = {
|
||||
thresholds: [
|
||||
({
|
||||
color: '#73BF69',
|
||||
value: null,
|
||||
} as unknown) as Threshold,
|
||||
{
|
||||
color: '#F2495C',
|
||||
value: 50,
|
||||
},
|
||||
],
|
||||
const config: FieldConfig = {
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{
|
||||
color: '#73BF69',
|
||||
value: (null as any) as number, // -Infinity becomes null in JSON
|
||||
},
|
||||
{
|
||||
color: '#F2495C',
|
||||
value: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setFieldConfigDefaults(field);
|
||||
expect(field.thresholds!.length).toEqual(2);
|
||||
expect(field.thresholds![0].value).toBe(-Infinity);
|
||||
setFieldConfigDefaults(config);
|
||||
expect(config.thresholds!.steps.length).toEqual(2);
|
||||
expect(config.thresholds!.steps[0].value).toBe(-Infinity);
|
||||
});
|
||||
|
||||
it('Should return field thresholds when there is no data', () => {
|
||||
const options = createEmptyDisplayOptions({
|
||||
fieldOptions: {
|
||||
defaults: {
|
||||
thresholds: [{ color: '#F2495C', value: 50 }],
|
||||
thresholds: { steps: [{ color: '#F2495C', value: 50 }] },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const display = getFieldDisplayValues(options);
|
||||
expect(display[0].field.thresholds!.length).toEqual(1);
|
||||
expect(display[0].field.thresholds!.steps!.length).toEqual(1);
|
||||
expect(display[0].display.numeric).toEqual(0);
|
||||
});
|
||||
|
||||
|
@ -124,9 +124,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
const display =
|
||||
field.display ??
|
||||
getDisplayProcessor({
|
||||
config,
|
||||
field,
|
||||
theme: options.theme,
|
||||
type: field.type,
|
||||
});
|
||||
|
||||
const title = config.title ? config.title : defaultTitle;
|
||||
@ -245,9 +244,11 @@ function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): Fiel
|
||||
const { defaults } = fieldOptions;
|
||||
|
||||
const displayProcessor = getDisplayProcessor({
|
||||
config: defaults,
|
||||
field: {
|
||||
type: FieldType.other,
|
||||
config: defaults,
|
||||
},
|
||||
theme: options.theme,
|
||||
type: FieldType.other,
|
||||
});
|
||||
|
||||
const display = displayProcessor(null);
|
||||
|
@ -8,12 +8,16 @@ import {
|
||||
Field,
|
||||
FieldType,
|
||||
FieldConfigSource,
|
||||
ThresholdsMode,
|
||||
FieldColorMode,
|
||||
ColorScheme,
|
||||
} from '../types';
|
||||
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||
import { FieldMatcher } from '../types/transformations';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import toNumber from 'lodash/toNumber';
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
import { guessFieldTypeForField } from '../dataframe';
|
||||
|
||||
interface OverrideProps {
|
||||
match: FieldMatcher;
|
||||
@ -112,6 +116,32 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
}
|
||||
}
|
||||
|
||||
// Try harder to set a real value that is not 'other'
|
||||
let type = field.type;
|
||||
if (!type || type === FieldType.other) {
|
||||
const t = guessFieldTypeForField(field);
|
||||
if (t) {
|
||||
type = t;
|
||||
}
|
||||
}
|
||||
|
||||
// Some units have an implied range
|
||||
if (config.unit === 'percent') {
|
||||
if (!isNumber(config.min)) {
|
||||
config.min = 0;
|
||||
}
|
||||
if (!isNumber(config.max)) {
|
||||
config.max = 100;
|
||||
}
|
||||
} else if (config.unit === 'percentunit') {
|
||||
if (!isNumber(config.min)) {
|
||||
config.min = 0;
|
||||
}
|
||||
if (!isNumber(config.max)) {
|
||||
config.max = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the Min/Max value automatically
|
||||
if (options.autoMinMax && field.type === FieldType.number) {
|
||||
if (!isNumber(config.min) || !isNumber(config.max)) {
|
||||
@ -127,19 +157,15 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Overwrite the configs
|
||||
const f: Field = {
|
||||
...field,
|
||||
|
||||
// Overwrite the configs
|
||||
config,
|
||||
|
||||
// Set the display processor
|
||||
display: getDisplayProcessor({
|
||||
type: field.type,
|
||||
config: config,
|
||||
theme: options.theme,
|
||||
}),
|
||||
type,
|
||||
};
|
||||
// and set the display processor using it
|
||||
f.display = getDisplayProcessor({ field: f, theme: options.theme });
|
||||
return f;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -206,9 +232,47 @@ export function setFieldConfigDefaults(config: FieldConfig, props?: FieldConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// First value is always -Infinity
|
||||
if (config.thresholds && config.thresholds.length) {
|
||||
config.thresholds[0].value = -Infinity;
|
||||
validateFieldConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* This checks that all options on FieldConfig make sense. It mutates any value that needs
|
||||
* fixed. In particular this makes sure that the first threshold value is -Infinity (not valid in JSON)
|
||||
*/
|
||||
export function validateFieldConfig(config: FieldConfig) {
|
||||
const { thresholds } = config;
|
||||
if (thresholds) {
|
||||
if (!thresholds.mode) {
|
||||
thresholds.mode = ThresholdsMode.Absolute;
|
||||
}
|
||||
if (!thresholds.steps) {
|
||||
thresholds.steps = [];
|
||||
} else if (thresholds.steps.length) {
|
||||
// First value is always -Infinity
|
||||
// JSON saves it as null
|
||||
thresholds.steps[0].value = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.color) {
|
||||
if (thresholds) {
|
||||
config.color = {
|
||||
mode: FieldColorMode.Thresholds,
|
||||
};
|
||||
}
|
||||
// No Color settings
|
||||
} else if (!config.color.mode) {
|
||||
// Without a mode, skip color altogether
|
||||
delete config.color;
|
||||
} else {
|
||||
const { color } = config;
|
||||
if (color.mode === FieldColorMode.Scheme) {
|
||||
if (!color.schemeName) {
|
||||
color.schemeName = ColorScheme.BrBG;
|
||||
}
|
||||
} else {
|
||||
delete color.schemeName;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that max > min (swap if necessary)
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './fieldDisplay';
|
||||
export * from './displayProcessor';
|
||||
export * from './scale';
|
||||
|
||||
export { applyFieldOverrides } from './fieldOverrides';
|
||||
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
|
||||
|
195
packages/grafana-data/src/field/scale.test.ts
Normal file
195
packages/grafana-data/src/field/scale.test.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { Field, FieldType, ColorScheme, ThresholdsConfig, ThresholdsMode, FieldColorMode, FieldConfig } from '../types';
|
||||
import { sortThresholds, getScaleCalculator, getActiveThreshold } from './scale';
|
||||
import { ArrayVector } from '../vector';
|
||||
import { validateFieldConfig } from './fieldOverrides';
|
||||
|
||||
describe('scale', () => {
|
||||
test('sort thresholds', () => {
|
||||
const thresholds: ThresholdsConfig = {
|
||||
steps: [
|
||||
{ color: 'TEN', value: 10 },
|
||||
{ color: 'HHH', value: 100 },
|
||||
{ color: 'ONE', value: 1 },
|
||||
],
|
||||
mode: ThresholdsMode.Absolute,
|
||||
};
|
||||
const sorted = sortThresholds(thresholds.steps).map(t => t.value);
|
||||
expect(sorted).toEqual([1, 10, 100]);
|
||||
const config: FieldConfig = { thresholds };
|
||||
|
||||
// Mutates and sorts the
|
||||
validateFieldConfig(config);
|
||||
expect(getActiveThreshold(10, thresholds.steps).color).toEqual('TEN');
|
||||
});
|
||||
|
||||
test('find active', () => {
|
||||
const thresholds: ThresholdsConfig = {
|
||||
steps: [
|
||||
{ color: 'ONE', value: 1 },
|
||||
{ color: 'TEN', value: 10 },
|
||||
{ color: 'HHH', value: 100 },
|
||||
],
|
||||
mode: ThresholdsMode.Absolute,
|
||||
};
|
||||
const config: FieldConfig = { thresholds };
|
||||
// Mutates and sets ONE to -Infinity
|
||||
validateFieldConfig(config);
|
||||
expect(getActiveThreshold(-1, thresholds.steps).color).toEqual('ONE');
|
||||
expect(getActiveThreshold(1, thresholds.steps).color).toEqual('ONE');
|
||||
expect(getActiveThreshold(5, thresholds.steps).color).toEqual('ONE');
|
||||
expect(getActiveThreshold(10, thresholds.steps).color).toEqual('TEN');
|
||||
expect(getActiveThreshold(11, thresholds.steps).color).toEqual('TEN');
|
||||
expect(getActiveThreshold(99, thresholds.steps).color).toEqual('TEN');
|
||||
expect(getActiveThreshold(100, thresholds.steps).color).toEqual('HHH');
|
||||
expect(getActiveThreshold(1000, thresholds.steps).color).toEqual('HHH');
|
||||
});
|
||||
|
||||
test('absolute thresholds', () => {
|
||||
const thresholds: ThresholdsConfig = {
|
||||
steps: [
|
||||
// Colors are ignored when 'scheme' exists
|
||||
{ color: '#F00', state: 'LowLow', value: -Infinity },
|
||||
{ color: '#F00', state: 'Low', value: -50 },
|
||||
{ color: '#F00', state: 'OK', value: 0 },
|
||||
{ color: '#F00', state: 'High', value: 50 },
|
||||
{ color: '#F00', state: 'HighHigh', value: 100 },
|
||||
],
|
||||
mode: ThresholdsMode.Absolute,
|
||||
};
|
||||
|
||||
const field: Field<number> = {
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
min: -100, // explicit range
|
||||
max: 100, // note less then range of actual data
|
||||
thresholds,
|
||||
color: {
|
||||
mode: FieldColorMode.Scheme,
|
||||
schemeName: ColorScheme.Greens,
|
||||
},
|
||||
},
|
||||
values: new ArrayVector([
|
||||
-1000,
|
||||
-100,
|
||||
-75,
|
||||
-50,
|
||||
-25,
|
||||
0, // middle
|
||||
25,
|
||||
50,
|
||||
75,
|
||||
100,
|
||||
1000,
|
||||
]),
|
||||
};
|
||||
validateFieldConfig(field.config);
|
||||
const calc = getScaleCalculator(field);
|
||||
const mapped = field.values.toArray().map(v => {
|
||||
return calc(v);
|
||||
});
|
||||
expect(mapped).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgb(247, 252, 245)",
|
||||
"percent": -4.5,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "LowLow",
|
||||
"value": -Infinity,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(247, 252, 245)",
|
||||
"percent": 0,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "LowLow",
|
||||
"value": -Infinity,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(227, 244, 222)",
|
||||
"percent": 0.125,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "LowLow",
|
||||
"value": -Infinity,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(198, 232, 191)",
|
||||
"percent": 0.25,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "Low",
|
||||
"value": -50,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(160, 216, 155)",
|
||||
"percent": 0.375,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "Low",
|
||||
"value": -50,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(115, 195, 120)",
|
||||
"percent": 0.5,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "OK",
|
||||
"value": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(69, 170, 93)",
|
||||
"percent": 0.625,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "OK",
|
||||
"value": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(34, 139, 69)",
|
||||
"percent": 0.75,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "High",
|
||||
"value": 50,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(6, 107, 45)",
|
||||
"percent": 0.875,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "High",
|
||||
"value": 50,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(0, 68, 27)",
|
||||
"percent": 1,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "HighHigh",
|
||||
"value": 100,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "rgb(0, 68, 27)",
|
||||
"percent": 5.5,
|
||||
"threshold": Object {
|
||||
"color": "#F00",
|
||||
"state": "HighHigh",
|
||||
"value": 100,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
122
packages/grafana-data/src/field/scale.ts
Normal file
122
packages/grafana-data/src/field/scale.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { Field, Threshold, GrafanaTheme, GrafanaThemeType, ThresholdsMode, FieldColorMode } from '../types';
|
||||
import { reduceField, ReducerID } from '../transformations';
|
||||
import { getColorFromHexRgbOrName } from '../utils/namedColorsPalette';
|
||||
import * as d3 from 'd3-scale-chromatic';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
|
||||
export interface ScaledValue {
|
||||
percent?: number; // 0-1
|
||||
threshold?: Threshold; // the selected step
|
||||
color?: string; // Selected color (may be range based on threshold)
|
||||
}
|
||||
|
||||
export type ScaleCalculator = (value: number) => ScaledValue;
|
||||
|
||||
/**
|
||||
* @param t Number in the range [0, 1].
|
||||
*/
|
||||
type colorInterpolator = (t: number) => string;
|
||||
|
||||
export function getScaleCalculator(field: Field, theme?: GrafanaTheme): ScaleCalculator {
|
||||
const themeType = theme ? theme.type : GrafanaThemeType.Dark;
|
||||
const config = field.config || {};
|
||||
const { thresholds, color } = config;
|
||||
|
||||
const fixedColor =
|
||||
color && color.mode === FieldColorMode.Fixed && color.fixedColor
|
||||
? getColorFromHexRgbOrName(color.fixedColor, themeType)
|
||||
: undefined;
|
||||
|
||||
// Should we calculate the percentage
|
||||
const percentThresholds = thresholds && thresholds.mode === ThresholdsMode.Percentage;
|
||||
const useColorScheme = color && color.mode === FieldColorMode.Scheme;
|
||||
if (percentThresholds || useColorScheme) {
|
||||
// Calculate min/max if required
|
||||
let min = config.min;
|
||||
let max = config.max;
|
||||
if (!isNumber(min) || !isNumber(max)) {
|
||||
if (field.values && field.values.length) {
|
||||
const stats = reduceField({ field, reducers: [ReducerID.min, ReducerID.max] });
|
||||
if (!isNumber(min)) {
|
||||
min = stats[ReducerID.min];
|
||||
}
|
||||
if (!isNumber(max)) {
|
||||
max = stats[ReducerID.max];
|
||||
}
|
||||
} else {
|
||||
min = 0;
|
||||
max = 100;
|
||||
}
|
||||
}
|
||||
const delta = max! - min!;
|
||||
|
||||
// Use a d3 color scale
|
||||
let interpolator: colorInterpolator | undefined;
|
||||
if (useColorScheme) {
|
||||
interpolator = (d3 as any)[`interpolate${color!.schemeName}`] as colorInterpolator;
|
||||
}
|
||||
|
||||
return (value: number) => {
|
||||
const percent = (value - min!) / delta;
|
||||
const threshold = thresholds
|
||||
? getActiveThreshold(percentThresholds ? percent * 100 : value, thresholds.steps)
|
||||
: undefined; // 0-100
|
||||
let color = fixedColor;
|
||||
if (interpolator) {
|
||||
color = interpolator(percent);
|
||||
} else if (threshold) {
|
||||
color = getColorFromHexRgbOrName(threshold!.color, themeType);
|
||||
}
|
||||
|
||||
return {
|
||||
percent,
|
||||
threshold,
|
||||
color,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (thresholds) {
|
||||
return (value: number) => {
|
||||
const threshold = getActiveThreshold(value, thresholds.steps);
|
||||
const color = fixedColor ?? (threshold ? getColorFromHexRgbOrName(threshold.color, themeType) : undefined);
|
||||
return {
|
||||
threshold,
|
||||
color,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Constant color
|
||||
if (fixedColor) {
|
||||
return (value: number) => {
|
||||
return { color: fixedColor };
|
||||
};
|
||||
}
|
||||
|
||||
// NO-OP
|
||||
return (value: number) => {
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
|
||||
let active = thresholds[0];
|
||||
for (const threshold of thresholds) {
|
||||
if (value >= threshold.value) {
|
||||
active = threshold;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the thresholds
|
||||
*/
|
||||
export function sortThresholds(thresholds: Threshold[]) {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t1.value - t2.value;
|
||||
});
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { Threshold } from './threshold';
|
||||
import { ThresholdsConfig } from './thresholds';
|
||||
import { ValueMapping } from './valueMapping';
|
||||
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||
import { DisplayProcessor } from './displayValue';
|
||||
import { DataLink } from './dataLink';
|
||||
import { Vector } from './vector';
|
||||
import { FieldCalcs } from '../transformations/fieldReducer';
|
||||
import { FieldColor } from './fieldColor';
|
||||
|
||||
export enum FieldType {
|
||||
time = 'time', // or date
|
||||
@ -32,8 +33,11 @@ export interface FieldConfig {
|
||||
// Convert input values into a display string
|
||||
mappings?: ValueMapping[];
|
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
thresholds?: Threshold[];
|
||||
// Map numeric values to states
|
||||
thresholds?: ThresholdsConfig;
|
||||
|
||||
// Map values to a display color
|
||||
color?: FieldColor;
|
||||
|
||||
// Used when reducing field values
|
||||
nullValueMode?: NullValueMode;
|
||||
@ -44,8 +48,7 @@ export interface FieldConfig {
|
||||
// Alternative to empty string
|
||||
noValue?: string;
|
||||
|
||||
color?: string;
|
||||
|
||||
// Panel Specific Values
|
||||
custom?: Record<string, any>;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ export type DisplayProcessor = (value: any) => DisplayValue;
|
||||
|
||||
export interface DisplayValue extends FormattedValue {
|
||||
numeric: number; // Use isNaN to check if it is a real number
|
||||
percent?: number; // 0-1 between min & max
|
||||
color?: string; // color based on configs or Threshold
|
||||
title?: string;
|
||||
}
|
||||
|
52
packages/grafana-data/src/types/fieldColor.ts
Normal file
52
packages/grafana-data/src/types/fieldColor.ts
Normal file
@ -0,0 +1,52 @@
|
||||
export enum FieldColorMode {
|
||||
Thresholds = 'thresholds',
|
||||
Scheme = 'scheme',
|
||||
Fixed = 'fixed',
|
||||
}
|
||||
|
||||
export interface FieldColor {
|
||||
mode: FieldColorMode;
|
||||
schemeName?: ColorScheme;
|
||||
fixedColor?: string;
|
||||
}
|
||||
|
||||
// https://github.com/d3/d3-scale-chromatic
|
||||
export enum ColorScheme {
|
||||
BrBG = 'BrBG',
|
||||
PRGn = 'PRGn',
|
||||
PiYG = 'PiYG',
|
||||
PuOr = 'PuOr',
|
||||
RdBu = 'RdBu',
|
||||
RdGy = 'RdGy',
|
||||
RdYlBu = 'RdYlBu',
|
||||
RdYlGn = 'RdYlGn',
|
||||
Spectral = 'Spectral',
|
||||
BuGn = 'BuGn',
|
||||
BuPu = 'BuPu',
|
||||
GnBu = 'GnBu',
|
||||
OrRd = 'OrRd',
|
||||
PuBuGn = 'PuBuGn',
|
||||
PuBu = 'PuBu',
|
||||
PuRd = 'PuRd',
|
||||
RdPu = 'RdPu',
|
||||
YlGnBu = 'YlGnBu',
|
||||
YlGn = 'YlGn',
|
||||
YlOrBr = 'YlOrBr',
|
||||
YlOrRd = 'YlOrRd',
|
||||
Blues = 'Blues',
|
||||
Greens = 'Greens',
|
||||
Greys = 'Greys',
|
||||
Purples = 'Purples',
|
||||
Reds = 'Reds',
|
||||
Oranges = 'Oranges',
|
||||
|
||||
// interpolateCubehelix
|
||||
// interpolateRainbow,
|
||||
// interpolateWarm
|
||||
// interpolateCool
|
||||
// interpolateSinebow
|
||||
// interpolateViridis
|
||||
// interpolateMagma
|
||||
// interpolateInferno
|
||||
// interpolatePlasma
|
||||
}
|
@ -5,7 +5,7 @@ export * from './logs';
|
||||
export * from './navModel';
|
||||
export * from './select';
|
||||
export * from './time';
|
||||
export * from './threshold';
|
||||
export * from './thresholds';
|
||||
export * from './utils';
|
||||
export * from './valueMapping';
|
||||
export * from './displayValue';
|
||||
@ -18,6 +18,8 @@ export * from './app';
|
||||
export * from './datasource';
|
||||
export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './thresholds';
|
||||
export * from './fieldColor';
|
||||
export * from './theme';
|
||||
export * from './orgs';
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
export interface Threshold {
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
17
packages/grafana-data/src/types/thresholds.ts
Normal file
17
packages/grafana-data/src/types/thresholds.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface Threshold {
|
||||
value: number;
|
||||
color: string;
|
||||
state?: string; // Warning, Error, LowLow, Low, OK, High, HighHigh etc
|
||||
}
|
||||
|
||||
export enum ThresholdsMode {
|
||||
Absolute = 'absolute',
|
||||
Percentage = 'percentage', // between 0 and 1 (based on min/max)
|
||||
}
|
||||
|
||||
export interface ThresholdsConfig {
|
||||
mode: ThresholdsMode;
|
||||
|
||||
// Must be sorted by 'value', first value is always -Infinity
|
||||
steps: Threshold[];
|
||||
}
|
@ -5,7 +5,6 @@ export * from './logs';
|
||||
export * from './labels';
|
||||
export * from './labels';
|
||||
export * from './object';
|
||||
export * from './thresholds';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './series';
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { Threshold } from '../types';
|
||||
|
||||
export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
|
||||
let active = thresholds[0];
|
||||
for (const threshold of thresholds) {
|
||||
if (value >= threshold.value) {
|
||||
active = threshold;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the thresholds
|
||||
*/
|
||||
export function sortThresholds(thresholds: Threshold[]) {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t1.value - t2.value;
|
||||
});
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number, text } from '@storybook/addon-knobs';
|
||||
import { BarGauge, Props, BarGaugeDisplayMode } from './BarGauge';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
import { VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
@ -35,6 +35,23 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
|
||||
threshold2Value,
|
||||
} = getKnobs();
|
||||
|
||||
const field: Partial<Field> = {
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: threshold1Value, color: threshold1Color },
|
||||
{ value: threshold2Value, color: threshold2Color },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
field.display = getDisplayProcessor({ field });
|
||||
|
||||
const props: Props = {
|
||||
theme: {} as any,
|
||||
width: 300,
|
||||
@ -44,15 +61,10 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
|
||||
title: title,
|
||||
numeric: value,
|
||||
},
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
orientation: VizOrientation.Vertical,
|
||||
displayMode: BarGaugeDisplayMode.Basic,
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: threshold1Value, color: threshold1Color },
|
||||
{ value: threshold2Value, color: threshold2Color },
|
||||
],
|
||||
field: field.config!,
|
||||
display: field.display!,
|
||||
};
|
||||
|
||||
Object.assign(props, overrides);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DisplayValue } from '@grafana/data';
|
||||
import { DisplayValue, VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
|
||||
import {
|
||||
BarGauge,
|
||||
Props,
|
||||
@ -11,29 +11,38 @@ import {
|
||||
getValuePercent,
|
||||
BarGaugeDisplayMode,
|
||||
} from './BarGauge';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
const green = '#73BF69';
|
||||
const orange = '#FF9830';
|
||||
|
||||
function getProps(propOverrides?: Partial<Props>): Props {
|
||||
const field: Partial<Field> = {
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 70, color: 'orange' },
|
||||
{ value: 90, color: 'red' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const theme = getTheme();
|
||||
field.display = getDisplayProcessor({ field, theme });
|
||||
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
displayMode: BarGaugeDisplayMode.Basic,
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 70, color: 'orange' },
|
||||
{ value: 90, color: 'red' },
|
||||
],
|
||||
field: field.config!,
|
||||
display: field.display!,
|
||||
height: 300,
|
||||
width: 300,
|
||||
value: {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
},
|
||||
theme: getTheme(),
|
||||
value: field.display(25),
|
||||
theme,
|
||||
orientation: VizOrientation.Horizontal,
|
||||
};
|
||||
|
||||
@ -59,11 +68,13 @@ function getValue(value: number, title?: string): DisplayValue {
|
||||
describe('BarGauge', () => {
|
||||
describe('Get value color', () => {
|
||||
it('should get the threshold color if value is same as a threshold', () => {
|
||||
const props = getProps({ value: getValue(70) });
|
||||
const props = getProps();
|
||||
props.value = props.display(70);
|
||||
expect(getValueColor(props)).toEqual(orange);
|
||||
});
|
||||
it('should get the base threshold', () => {
|
||||
const props = getProps({ value: getValue(-10) });
|
||||
const props = getProps();
|
||||
props.value = props.display(-10);
|
||||
expect(getValueColor(props)).toEqual(green);
|
||||
});
|
||||
});
|
||||
|
@ -1,14 +1,17 @@
|
||||
// Library
|
||||
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as d3 from 'd3-scale-chromatic';
|
||||
import {
|
||||
Threshold,
|
||||
TimeSeriesValue,
|
||||
getActiveThreshold,
|
||||
DisplayValue,
|
||||
formattedValueToString,
|
||||
FormattedValue,
|
||||
DisplayValueAlignmentFactors,
|
||||
ThresholdsMode,
|
||||
DisplayProcessor,
|
||||
FieldConfig,
|
||||
FieldColorMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
// Compontents
|
||||
@ -33,10 +36,9 @@ const VALUE_LEFT_PADDING = 10;
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
width: number;
|
||||
thresholds: Threshold[];
|
||||
field: FieldConfig;
|
||||
display: DisplayProcessor;
|
||||
value: DisplayValue;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
orientation: VizOrientation;
|
||||
itemSpacing?: number;
|
||||
lcdCellWidth?: number;
|
||||
@ -55,8 +57,6 @@ export enum BarGaugeDisplayMode {
|
||||
|
||||
export class BarGauge extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
lcdCellWidth: 12,
|
||||
value: {
|
||||
text: '100',
|
||||
@ -64,7 +64,14 @@ export class BarGauge extends PureComponent<Props> {
|
||||
},
|
||||
displayMode: BarGaugeDisplayMode.Gradient,
|
||||
orientation: VizOrientation.Horizontal,
|
||||
thresholds: [],
|
||||
field: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [],
|
||||
},
|
||||
},
|
||||
itemSpacing: 10,
|
||||
showUnfilled: true,
|
||||
};
|
||||
@ -116,7 +123,7 @@ export class BarGauge extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
getCellColor(positionValue: TimeSeriesValue): CellColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
const { value, display } = this.props;
|
||||
if (positionValue === null) {
|
||||
return {
|
||||
background: 'gray',
|
||||
@ -124,10 +131,8 @@ export class BarGauge extends PureComponent<Props> {
|
||||
};
|
||||
}
|
||||
|
||||
const activeThreshold = getActiveThreshold(positionValue, thresholds);
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
const color = display(positionValue).color;
|
||||
if (color) {
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return {
|
||||
@ -160,7 +165,7 @@ export class BarGauge extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
renderRetroBars(): ReactNode {
|
||||
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
|
||||
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
|
||||
const {
|
||||
valueHeight,
|
||||
valueWidth,
|
||||
@ -169,6 +174,8 @@ export class BarGauge extends PureComponent<Props> {
|
||||
wrapperWidth,
|
||||
wrapperHeight,
|
||||
} = calculateBarAndValueDimensions(this.props);
|
||||
const minValue = field.min!;
|
||||
const maxValue = field.max!;
|
||||
|
||||
const isVert = isVertical(orientation);
|
||||
const valueRange = maxValue - minValue;
|
||||
@ -402,10 +409,10 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe
|
||||
* Only exported to for unit test
|
||||
*/
|
||||
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
|
||||
const { displayMode, maxValue, minValue, value, alignmentFactors, orientation, theme } = props;
|
||||
const { displayMode, field, value, alignmentFactors, orientation, theme } = props;
|
||||
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
|
||||
|
||||
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
|
||||
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
|
||||
const valueColor = getValueColor(props);
|
||||
|
||||
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
|
||||
@ -495,26 +502,56 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
|
||||
* Only exported to for unit test
|
||||
*/
|
||||
export function getBarGradient(props: Props, maxSize: number): string {
|
||||
const { minValue, maxValue, thresholds, value, orientation } = props;
|
||||
const { field, value, orientation } = props;
|
||||
const cssDirection = isVertical(orientation) ? '0deg' : '90deg';
|
||||
const minValue = field.min!;
|
||||
const maxValue = field.max!;
|
||||
|
||||
let gradient = '';
|
||||
let lastpos = 0;
|
||||
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
const threshold = thresholds[i];
|
||||
const color = getColorFromHexRgbOrName(threshold.color);
|
||||
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
|
||||
const pos = valuePercent * maxSize;
|
||||
const offset = Math.round(pos - (pos - lastpos) / 2);
|
||||
|
||||
if (gradient === '') {
|
||||
if (field.color && field.color.mode === FieldColorMode.Scheme) {
|
||||
const schemeSet = (d3 as any)[`scheme${field.color.schemeName}`] as any[];
|
||||
if (!schemeSet) {
|
||||
// Error: unknown scheme
|
||||
const color = '#F00';
|
||||
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
|
||||
} else if (value.numeric < threshold.value) {
|
||||
break;
|
||||
} else {
|
||||
lastpos = pos;
|
||||
gradient += ` ${offset}px, ${color}`;
|
||||
gradient += ` ${maxSize}px, ${color}`;
|
||||
return gradient + ')';
|
||||
}
|
||||
// Get the scheme with as many steps as possible
|
||||
const scheme = schemeSet[schemeSet.length - 1] as string[];
|
||||
for (let i = 0; i < scheme.length; i++) {
|
||||
const color = scheme[i];
|
||||
const valuePercent = i / (scheme.length - 1);
|
||||
const pos = valuePercent * maxSize;
|
||||
const offset = Math.round(pos - (pos - lastpos) / 2);
|
||||
|
||||
if (gradient === '') {
|
||||
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
|
||||
} else {
|
||||
lastpos = pos;
|
||||
gradient += ` ${offset}px, ${color}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const thresholds = field.thresholds!;
|
||||
|
||||
for (let i = 0; i < thresholds.steps.length; i++) {
|
||||
const threshold = thresholds.steps[i];
|
||||
const color = getColorFromHexRgbOrName(threshold.color);
|
||||
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
|
||||
const pos = valuePercent * maxSize;
|
||||
const offset = Math.round(pos - (pos - lastpos) / 2);
|
||||
|
||||
if (gradient === '') {
|
||||
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
|
||||
} else if (value.numeric < threshold.value) {
|
||||
break;
|
||||
} else {
|
||||
lastpos = pos;
|
||||
gradient += ` ${offset}px, ${color}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,14 +562,10 @@ export function getBarGradient(props: Props, maxSize: number): string {
|
||||
* Only exported to for unit test
|
||||
*/
|
||||
export function getValueColor(props: Props): string {
|
||||
const { thresholds, theme, value } = props;
|
||||
|
||||
const activeThreshold = getActiveThreshold(value.numeric, thresholds);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
const { theme, value } = props;
|
||||
if (value.color) {
|
||||
return value.color;
|
||||
}
|
||||
|
||||
return getColorFromHexRgbOrName('gray', theme.type);
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,15 @@ exports[`BarGauge Render with basic options should render 1`] = `
|
||||
}
|
||||
value={
|
||||
Object {
|
||||
"color": "#73BF69",
|
||||
"numeric": 25,
|
||||
"prefix": undefined,
|
||||
"suffix": undefined,
|
||||
"text": "25",
|
||||
"threshold": Object {
|
||||
"color": "green",
|
||||
"value": -Infinity,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -3,20 +3,29 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { getTheme } from '../../themes';
|
||||
import { ThresholdsMode, FieldConfig } from '@grafana/data';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const setup = (propOverrides?: FieldConfig) => {
|
||||
const field: FieldConfig = {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [{ value: -Infinity, color: '#7EB26D' }],
|
||||
},
|
||||
};
|
||||
Object.assign(field, propOverrides);
|
||||
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
thresholds: [{ value: -Infinity, color: '#7EB26D' }],
|
||||
height: 300,
|
||||
field,
|
||||
width: 300,
|
||||
height: 300,
|
||||
value: {
|
||||
text: '25',
|
||||
numeric: 25,
|
||||
@ -24,8 +33,6 @@ const setup = (propOverrides?: object) => {
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<Gauge {...props} />);
|
||||
const instance = wrapper.instance() as Gauge;
|
||||
|
||||
@ -37,7 +44,9 @@ const setup = (propOverrides?: object) => {
|
||||
|
||||
describe('Get thresholds formatted', () => {
|
||||
it('should return first thresholds color for min and max', () => {
|
||||
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
|
||||
const { instance } = setup({
|
||||
thresholds: { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: '#7EB26D' }] },
|
||||
});
|
||||
|
||||
expect(instance.getFormattedThresholds()).toEqual([
|
||||
{ value: 0, color: '#7EB26D' },
|
||||
@ -47,11 +56,14 @@ describe('Get thresholds formatted', () => {
|
||||
|
||||
it('should get the correct formatted values when thresholds are added', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(instance.getFormattedThresholds()).toEqual([
|
||||
|
@ -1,14 +1,20 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { Threshold, DisplayValue, getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data';
|
||||
import {
|
||||
DisplayValue,
|
||||
getColorFromHexRgbOrName,
|
||||
formattedValueToString,
|
||||
FieldConfig,
|
||||
ThresholdsMode,
|
||||
getActiveThreshold,
|
||||
Threshold,
|
||||
} from '@grafana/data';
|
||||
import { Themeable } from '../../types';
|
||||
import { selectThemeVariant } from '../../themes';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
height: number;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
thresholds: Threshold[];
|
||||
field: FieldConfig;
|
||||
showThresholdMarkers: boolean;
|
||||
showThresholdLabels: boolean;
|
||||
width: number;
|
||||
@ -23,11 +29,19 @@ export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
thresholds: [],
|
||||
field: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 80, color: 'red' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -38,22 +52,38 @@ export class Gauge extends PureComponent<Props> {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
getFormattedThresholds() {
|
||||
const { maxValue, minValue, thresholds, theme } = this.props;
|
||||
getFormattedThresholds(): Threshold[] {
|
||||
const { field, theme } = this.props;
|
||||
const isPercent = field.thresholds?.mode === ThresholdsMode.Percentage;
|
||||
const steps = field.thresholds!.steps;
|
||||
let min = field.min!;
|
||||
let max = field.max!;
|
||||
if (isPercent) {
|
||||
min = 0;
|
||||
max = 100;
|
||||
}
|
||||
|
||||
const lastThreshold = thresholds[thresholds.length - 1];
|
||||
|
||||
return [
|
||||
...thresholds.map((threshold, index) => {
|
||||
if (index === 0) {
|
||||
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
|
||||
const first = getActiveThreshold(min, steps);
|
||||
const last = getActiveThreshold(max, steps);
|
||||
const formatted: Threshold[] = [];
|
||||
formatted.push({ value: min, color: getColorFromHexRgbOrName(first.color, theme.type) });
|
||||
let skip = true;
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
if (skip) {
|
||||
if (first === step) {
|
||||
skip = false;
|
||||
}
|
||||
|
||||
const previousThreshold = thresholds[index - 1];
|
||||
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
|
||||
}),
|
||||
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
|
||||
];
|
||||
continue;
|
||||
}
|
||||
const prev = steps[i - 1];
|
||||
formatted.push({ value: step.value, color: getColorFromHexRgbOrName(prev!.color, theme.type) });
|
||||
if (step === last) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
formatted.push({ value: max, color: getColorFromHexRgbOrName(last.color, theme.type) });
|
||||
return formatted;
|
||||
}
|
||||
|
||||
getFontScale(length: number): number {
|
||||
@ -64,7 +94,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
draw() {
|
||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
||||
const { field, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
||||
|
||||
const autoProps = calculateGaugeAutoProps(width, height, value.title);
|
||||
const dimension = Math.min(width, autoProps.gaugeHeight);
|
||||
@ -85,12 +115,25 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
let min = field.min!;
|
||||
let max = field.max!;
|
||||
let numeric = value.numeric;
|
||||
if (field.thresholds?.mode === ThresholdsMode.Percentage) {
|
||||
min = 0;
|
||||
max = 100;
|
||||
if (value.percent === undefined) {
|
||||
numeric = ((numeric - min) / (max - min)) * 100;
|
||||
} else {
|
||||
numeric = value.percent! * 100;
|
||||
}
|
||||
}
|
||||
|
||||
const options: any = {
|
||||
series: {
|
||||
gauges: {
|
||||
gauge: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
min,
|
||||
max,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
@ -123,7 +166,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
const plotSeries = {
|
||||
data: [[0, value.numeric]],
|
||||
data: [[0, numeric]],
|
||||
label: value.title,
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Graph } from './Graph';
|
||||
import Chart from '../Chart';
|
||||
import { dateTime, ArrayVector, FieldType, GraphSeriesXY } from '@grafana/data';
|
||||
import { dateTime, ArrayVector, FieldType, GraphSeriesXY, FieldColorMode } from '@grafana/data';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TooltipContentProps } from '../Chart/Tooltip';
|
||||
@ -47,7 +47,12 @@ const series: GraphSeriesXY[] = [
|
||||
type: FieldType.number,
|
||||
name: 'a-series',
|
||||
values: new ArrayVector([10, 20, 10]),
|
||||
config: { color: 'red' },
|
||||
config: {
|
||||
color: {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
@ -76,7 +81,12 @@ const series: GraphSeriesXY[] = [
|
||||
name:
|
||||
"B-series with an ultra wide label that is probably going go make the tooltip overflow window. This situation happens, so let's better make sure it behaves nicely :)",
|
||||
values: new ArrayVector([20, 30, 40]),
|
||||
config: { color: 'blue' },
|
||||
config: {
|
||||
color: {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Graph from './Graph';
|
||||
import Chart from '../Chart';
|
||||
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
|
||||
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
|
||||
|
||||
const series: GraphSeriesXY[] = [
|
||||
{
|
||||
@ -25,7 +25,7 @@ const series: GraphSeriesXY[] = [
|
||||
type: FieldType.number,
|
||||
name: 'a-series',
|
||||
values: new ArrayVector([10, 20, 10]),
|
||||
config: { color: 'red' },
|
||||
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
@ -52,7 +52,7 @@ const series: GraphSeriesXY[] = [
|
||||
type: FieldType.number,
|
||||
name: 'b-series',
|
||||
values: new ArrayVector([20, 30, 40]),
|
||||
config: { color: 'blue' },
|
||||
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
|
@ -1,5 +1,10 @@
|
||||
import React from 'react';
|
||||
import { getValueFromDimension, getColumnFromDimension, formattedValueToString } from '@grafana/data';
|
||||
import {
|
||||
getValueFromDimension,
|
||||
getColumnFromDimension,
|
||||
formattedValueToString,
|
||||
getDisplayProcessor,
|
||||
} from '@grafana/data';
|
||||
import { SeriesTable } from './SeriesTable';
|
||||
import { GraphTooltipContentProps } from './types';
|
||||
|
||||
@ -19,11 +24,18 @@ export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dim
|
||||
|
||||
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
|
||||
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
|
||||
const processedValue = valueField.display ? formattedValueToString(valueField.display(value)) : value;
|
||||
const display = valueField.display ?? getDisplayProcessor({ field: valueField });
|
||||
const disp = display(value);
|
||||
|
||||
return (
|
||||
<SeriesTable
|
||||
series={[{ color: valueField.config.color, label: valueField.name, value: processedValue }]}
|
||||
series={[
|
||||
{
|
||||
color: disp.color,
|
||||
label: valueField.name,
|
||||
value: formattedValueToString(disp),
|
||||
},
|
||||
]}
|
||||
timestamp={processedTime}
|
||||
/>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import { withHorizontallyCenteredStory } from '../../utils/storybook/withCentere
|
||||
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
|
||||
|
||||
import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
|
||||
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
|
||||
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
|
||||
const GraphWithLegendStories = storiesOf('Visualizations/Graph/GraphWithLegend', module);
|
||||
GraphWithLegendStories.addDecorator(withHorizontallyCenteredStory);
|
||||
|
||||
@ -31,7 +31,12 @@ const series: GraphSeriesXY[] = [
|
||||
type: FieldType.number,
|
||||
name: 'a-series',
|
||||
values: new ArrayVector([10, 20, 10]),
|
||||
config: { color: 'red' },
|
||||
config: {
|
||||
color: {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
@ -58,7 +63,12 @@ const series: GraphSeriesXY[] = [
|
||||
type: FieldType.number,
|
||||
name: 'b-series',
|
||||
values: new ArrayVector([20, 30, 40]),
|
||||
config: { color: 'blue' },
|
||||
config: {
|
||||
color: {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
timeStep: 3600000,
|
||||
yAxis: {
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { GraphSeriesValue, toDataFrame, FieldType, FieldCache } from '@grafana/data';
|
||||
import {
|
||||
GraphSeriesValue,
|
||||
toDataFrame,
|
||||
FieldType,
|
||||
FieldCache,
|
||||
FieldColorMode,
|
||||
getColorFromHexRgbOrName,
|
||||
GrafanaThemeType,
|
||||
Field,
|
||||
} from '@grafana/data';
|
||||
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData } from './utils';
|
||||
|
||||
const mockResult = (
|
||||
value: GraphSeriesValue,
|
||||
value: string,
|
||||
datapointIndex: number,
|
||||
seriesIndex: number,
|
||||
color?: string,
|
||||
@ -21,23 +30,42 @@ const mockResult = (
|
||||
const aSeries = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||
{ name: 'value', type: FieldType.number, values: [10, 20, 10], config: { color: 'red' } },
|
||||
{
|
||||
name: 'value',
|
||||
type: FieldType.number,
|
||||
values: [10, 20, 10],
|
||||
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
const bSeries = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||
{ name: 'value', type: FieldType.number, values: [30, 60, 30], config: { color: 'blue' } },
|
||||
{
|
||||
name: 'value',
|
||||
type: FieldType.number,
|
||||
values: [30, 60, 30],
|
||||
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
// C-series has the same x-axis range as A and B but is missing the middle point
|
||||
const cSeries = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [100, 300] },
|
||||
{ name: 'value', type: FieldType.number, values: [30, 30], config: { color: 'yellow' } },
|
||||
{
|
||||
name: 'value',
|
||||
type: FieldType.number,
|
||||
values: [30, 30],
|
||||
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'yellow' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
function getFixedThemedColor(field: Field): string {
|
||||
return getColorFromHexRgbOrName(field.config.color!.fixedColor!, GrafanaThemeType.Dark);
|
||||
}
|
||||
|
||||
describe('Graph utils', () => {
|
||||
describe('getMultiSeriesGraphHoverInfo', () => {
|
||||
describe('when series datapoints are x-axis aligned', () => {
|
||||
@ -51,8 +79,12 @@ describe('Graph utils', () => {
|
||||
|
||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0);
|
||||
expect(result.time).toBe(100);
|
||||
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
|
||||
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
|
||||
expect(result.results[0]).toEqual(
|
||||
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
|
||||
);
|
||||
expect(result.results[1]).toEqual(
|
||||
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
|
||||
);
|
||||
});
|
||||
|
||||
describe('returns the closest datapoints before the hover position', () => {
|
||||
@ -67,8 +99,12 @@ describe('Graph utils', () => {
|
||||
// hovering right before middle point
|
||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 199);
|
||||
expect(result.time).toBe(100);
|
||||
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
|
||||
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
|
||||
expect(result.results[0]).toEqual(
|
||||
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
|
||||
);
|
||||
expect(result.results[1]).toEqual(
|
||||
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
|
||||
);
|
||||
});
|
||||
|
||||
it('when hovering right after a datapoint', () => {
|
||||
@ -82,8 +118,12 @@ describe('Graph utils', () => {
|
||||
// hovering right after middle point
|
||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 201);
|
||||
expect(result.time).toBe(200);
|
||||
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
|
||||
expect(result.results[1]).toEqual(mockResult(60, 1, 1, bValueField!.config.color, bValueField!.name, 200));
|
||||
expect(result.results[0]).toEqual(
|
||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
|
||||
);
|
||||
expect(result.results[1]).toEqual(
|
||||
mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, 200)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -106,9 +146,13 @@ describe('Graph utils', () => {
|
||||
// we expect a time of the hovered point
|
||||
expect(result.time).toBe(200);
|
||||
// we expect middle point from aSeries (the one we are hovering over)
|
||||
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
|
||||
expect(result.results[0]).toEqual(
|
||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
|
||||
);
|
||||
// we expect closest point before hovered point from cSeries (1st point)
|
||||
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
|
||||
expect(result.results[1]).toEqual(
|
||||
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
|
||||
);
|
||||
});
|
||||
|
||||
it('hovering right after over the middle point', () => {
|
||||
@ -125,9 +169,13 @@ describe('Graph utils', () => {
|
||||
// we expect the time of the closest point before hover
|
||||
expect(result.time).toBe(200);
|
||||
// we expect the closest datapoint before hover from aSeries
|
||||
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
|
||||
expect(result.results[0]).toEqual(
|
||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
|
||||
);
|
||||
// we expect the closest datapoint before hover from cSeries (1st point)
|
||||
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
|
||||
expect(result.results[1]).toEqual(
|
||||
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GraphSeriesValue, Field, formattedValueToString } from '@grafana/data';
|
||||
import { GraphSeriesValue, Field, formattedValueToString, getDisplayProcessor } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Returns index of the closest datapoint BEFORE hover position
|
||||
@ -53,14 +53,14 @@ export const getMultiSeriesGraphHoverInfo = (
|
||||
results: MultiSeriesHoverInfo[];
|
||||
time?: GraphSeriesValue;
|
||||
} => {
|
||||
let value, i, series, hoverIndex, hoverDistance, pointTime;
|
||||
let i, field, hoverIndex, hoverDistance, pointTime;
|
||||
|
||||
const results: MultiSeriesHoverInfo[] = [];
|
||||
|
||||
let minDistance, minTime;
|
||||
|
||||
for (i = 0; i < yAxisDimensions.length; i++) {
|
||||
series = yAxisDimensions[i];
|
||||
field = yAxisDimensions[i];
|
||||
const time = xAxisDimensions[i];
|
||||
hoverIndex = findHoverIndexFromData(time, xAxisPosition);
|
||||
hoverDistance = xAxisPosition - time.values.get(hoverIndex);
|
||||
@ -75,14 +75,15 @@ export const getMultiSeriesGraphHoverInfo = (
|
||||
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
|
||||
}
|
||||
|
||||
value = series.values.get(hoverIndex);
|
||||
const display = field.display ?? getDisplayProcessor({ field });
|
||||
const disp = display(field.values.get(hoverIndex));
|
||||
|
||||
results.push({
|
||||
value: series.display ? formattedValueToString(series.display(value)) : value,
|
||||
value: formattedValueToString(disp),
|
||||
datapointIndex: hoverIndex,
|
||||
seriesIndex: i,
|
||||
color: series.config.color,
|
||||
label: series.name,
|
||||
color: disp.color,
|
||||
label: field.name,
|
||||
time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime,
|
||||
});
|
||||
}
|
||||
|
@ -37,6 +37,36 @@ describe('sharedSingleStatMigrationHandler', () => {
|
||||
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('move thresholds to scale', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
fieldOptions: {
|
||||
defaults: {
|
||||
thresholds: [
|
||||
{
|
||||
color: 'green',
|
||||
index: 0,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
index: 1,
|
||||
value: 40,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
index: 2,
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Remove unused `overrides` option', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
|
@ -13,6 +13,10 @@ import {
|
||||
PanelModel,
|
||||
FieldDisplayOptions,
|
||||
ConfigOverrideRule,
|
||||
ThresholdsMode,
|
||||
ThresholdsConfig,
|
||||
validateFieldConfig,
|
||||
FieldColorMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface SingleStatBaseOptions {
|
||||
@ -70,7 +74,10 @@ export function sharedSingleStatPanelChangedHandler(
|
||||
thresholds.push({ value: -Infinity, color });
|
||||
}
|
||||
}
|
||||
defaults.thresholds = thresholds;
|
||||
defaults.thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: thresholds,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert value mappings
|
||||
@ -112,8 +119,10 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
|
||||
}
|
||||
|
||||
if (previousVersion < 6.6) {
|
||||
const { fieldOptions } = options;
|
||||
|
||||
// discard the old `override` options and enter an empty array
|
||||
if (options.fieldOptions && options.fieldOptions.override) {
|
||||
if (fieldOptions && fieldOptions.override) {
|
||||
const { override, ...rest } = options.fieldOptions;
|
||||
options = {
|
||||
...options,
|
||||
@ -123,6 +132,34 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Move thresholds to steps
|
||||
let thresholds = fieldOptions?.defaults?.thresholds;
|
||||
if (thresholds) {
|
||||
delete fieldOptions.defaults.thresholds;
|
||||
} else {
|
||||
thresholds = fieldOptions?.thresholds;
|
||||
delete fieldOptions.thresholds;
|
||||
}
|
||||
|
||||
if (thresholds) {
|
||||
fieldOptions.defaults.thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: thresholds,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrate color from simple string to a mode
|
||||
const { defaults } = fieldOptions;
|
||||
if (defaults.color) {
|
||||
const old = defaults.color;
|
||||
defaults.color = {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: old,
|
||||
};
|
||||
}
|
||||
|
||||
validateFieldConfig(defaults);
|
||||
}
|
||||
|
||||
return options as SingleStatBaseOptions;
|
||||
@ -135,7 +172,15 @@ export function moveThresholdsAndMappingsToField(old: any) {
|
||||
return old;
|
||||
}
|
||||
|
||||
const { mappings, thresholds, ...rest } = old.fieldOptions;
|
||||
const { mappings, ...rest } = old.fieldOptions;
|
||||
|
||||
let thresholds: ThresholdsConfig | undefined = undefined;
|
||||
if (old.thresholds) {
|
||||
thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: migrateOldThresholds(old.thresholds)!,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...old,
|
||||
@ -144,7 +189,7 @@ export function moveThresholdsAndMappingsToField(old: any) {
|
||||
defaults: {
|
||||
...fieldOptions.defaults,
|
||||
mappings,
|
||||
thresholds: migrateOldThresholds(thresholds),
|
||||
thresholds,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -24,6 +24,9 @@ Object {
|
||||
"last",
|
||||
],
|
||||
"defaults": Object {
|
||||
"color": Object {
|
||||
"mode": "thresholds",
|
||||
},
|
||||
"decimals": 5,
|
||||
"mappings": Array [
|
||||
Object {
|
||||
@ -34,22 +37,39 @@ Object {
|
||||
],
|
||||
"max": 100,
|
||||
"min": 10,
|
||||
"thresholds": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
"unit": "watt",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sharedSingleStatMigrationHandler move thresholds to scale 1`] = `
|
||||
Object {
|
||||
"fieldOptions": Object {
|
||||
"defaults": Object {
|
||||
"mappings": undefined,
|
||||
"thresholds": undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
@ -1,18 +1,21 @@
|
||||
import React, { FC } from 'react';
|
||||
import { ReactTableCellProps, TableCellDisplayMode } from './types';
|
||||
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
|
||||
import { VizOrientation } from '@grafana/data';
|
||||
import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data';
|
||||
|
||||
const defaultThresholds = [
|
||||
{
|
||||
color: 'blue',
|
||||
value: -Infinity,
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
value: 20,
|
||||
},
|
||||
];
|
||||
const defaultScale: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{
|
||||
color: 'blue',
|
||||
value: -Infinity,
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
value: 20,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
|
||||
const { column, tableStyles, cell } = props;
|
||||
@ -22,6 +25,14 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { config } = field;
|
||||
if (!config.thresholds) {
|
||||
config = {
|
||||
...config,
|
||||
thresholds: defaultScale,
|
||||
};
|
||||
}
|
||||
|
||||
const displayValue = field.display(cell.value);
|
||||
let barGaugeMode = BarGaugeDisplayMode.Gradient;
|
||||
|
||||
@ -34,10 +45,8 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
|
||||
<BarGauge
|
||||
width={column.width - tableStyles.cellPadding * 2}
|
||||
height={tableStyles.cellHeightInner}
|
||||
thresholds={field.config.thresholds || defaultThresholds}
|
||||
field={config}
|
||||
value={displayValue}
|
||||
maxValue={field.config.max || 100}
|
||||
minValue={field.config.min || 0}
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={tableStyles.theme}
|
||||
itemSpacing={1}
|
||||
|
@ -3,17 +3,24 @@ import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ThresholdsEditor } from './ThresholdsEditor';
|
||||
import { getTheme } from '../../themes';
|
||||
import { ThresholdsMode, ThresholdsConfig } from '@grafana/data';
|
||||
|
||||
const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module);
|
||||
const thresholds = [
|
||||
{ index: 0, value: -Infinity, color: 'green' },
|
||||
{ index: 1, value: 50, color: 'red' },
|
||||
];
|
||||
const thresholds: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 50, color: 'red' },
|
||||
],
|
||||
};
|
||||
|
||||
ThresholdsEditorStories.add('default', () => {
|
||||
return <ThresholdsEditor thresholds={[]} onChange={action('Thresholds changed')} />;
|
||||
return (
|
||||
<ThresholdsEditor theme={getTheme()} thresholds={{} as ThresholdsConfig} onChange={action('Thresholds changed')} />
|
||||
);
|
||||
});
|
||||
|
||||
ThresholdsEditorStories.add('with thresholds', () => {
|
||||
return <ThresholdsEditor thresholds={thresholds} onChange={action('Thresholds changed')} />;
|
||||
return <ThresholdsEditor theme={getTheme()} thresholds={thresholds} onChange={action('Thresholds changed')} />;
|
||||
});
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { GrafanaThemeType } from '@grafana/data';
|
||||
import { GrafanaThemeType, GrafanaTheme, ThresholdsMode } from '@grafana/data';
|
||||
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
|
||||
import { colors } from '../../utils';
|
||||
import { mockThemeContext } from '../../themes/ThemeContext';
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
theme: { type: GrafanaThemeType.Dark, isDark: true, isLight: false } as GrafanaTheme,
|
||||
onChange: jest.fn(),
|
||||
thresholds: [],
|
||||
thresholds: { mode: ThresholdsMode.Absolute, steps: [] },
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -23,7 +24,7 @@ const setup = (propOverrides?: Partial<Props>) => {
|
||||
};
|
||||
|
||||
function getCurrentThresholds(editor: ThresholdsEditor) {
|
||||
return thresholdsWithoutKey(editor.state.thresholds);
|
||||
return thresholdsWithoutKey(editor.props.thresholds, editor.state.steps);
|
||||
}
|
||||
|
||||
describe('Render', () => {
|
||||
@ -38,14 +39,14 @@ describe('Render', () => {
|
||||
|
||||
it('should render with base threshold', () => {
|
||||
const { wrapper } = setup();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('.thresholds')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should add a base threshold if missing', () => {
|
||||
const { instance } = setup();
|
||||
expect(getCurrentThresholds(instance)).toEqual([{ value: -Infinity, color: 'green' }]);
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([{ value: -Infinity, color: 'green' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -53,9 +54,9 @@ describe('Add threshold', () => {
|
||||
it('should add threshold', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onAddThresholdAfter(instance.state.thresholds[0]);
|
||||
instance.onAddThresholdAfter(instance.state.steps[0]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: 'green' }, // 0
|
||||
{ value: 50, color: colors[1] }, // 1
|
||||
]);
|
||||
@ -63,15 +64,18 @@ describe('Add threshold', () => {
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: colors[0] }, // 0
|
||||
{ value: 50, color: colors[2] }, // 1
|
||||
],
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: colors[0] }, // 0
|
||||
{ value: 50, color: colors[2] }, // 1
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
instance.onAddThresholdAfter(instance.state.thresholds[1]);
|
||||
instance.onAddThresholdAfter(instance.state.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: colors[0] }, // 0
|
||||
{ value: 50, color: colors[2] }, // 1
|
||||
{ value: 75, color: colors[3] }, // 2
|
||||
@ -80,16 +84,19 @@ describe('Add threshold', () => {
|
||||
|
||||
it('should add another threshold between first and second index', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: colors[0] },
|
||||
{ value: 50, color: colors[2] },
|
||||
{ value: 75, color: colors[3] },
|
||||
],
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: colors[0] },
|
||||
{ value: 50, color: colors[2] },
|
||||
{ value: 75, color: colors[3] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
instance.onAddThresholdAfter(instance.state.thresholds[1]);
|
||||
instance.onAddThresholdAfter(instance.state.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: colors[0] },
|
||||
{ value: 50, color: colors[2] },
|
||||
{ value: 62.5, color: colors[4] },
|
||||
@ -100,29 +107,35 @@ describe('Add threshold', () => {
|
||||
|
||||
describe('Remove threshold', () => {
|
||||
it('should not remove threshold at index 0', () => {
|
||||
const thresholds = [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
};
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
instance.onRemoveThreshold(instance.state.thresholds[0]);
|
||||
instance.onRemoveThreshold(instance.state.steps[0]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should remove threshold', () => {
|
||||
const thresholds = [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
};
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
instance.onRemoveThreshold(instance.state.thresholds[1]);
|
||||
instance.onRemoveThreshold(instance.state.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
]);
|
||||
@ -131,37 +144,43 @@ describe('Remove threshold', () => {
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should not change threshold at index 0', () => {
|
||||
const thresholds = [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
];
|
||||
const thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 50, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
],
|
||||
};
|
||||
const { instance } = setup({ thresholds });
|
||||
|
||||
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, instance.state.thresholds[0]);
|
||||
instance.onChangeThresholdValue(mockEvent, instance.state.steps[0]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual(thresholds);
|
||||
});
|
||||
|
||||
it('should update value', () => {
|
||||
const { instance } = setup();
|
||||
const thresholds = [
|
||||
{ value: -Infinity, color: '#7EB26D', key: 1 },
|
||||
{ value: 50, color: '#EAB839', key: 2 },
|
||||
{ value: 75, color: '#6ED0E0', key: 3 },
|
||||
];
|
||||
const thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D', key: 1 },
|
||||
{ value: 50, color: '#EAB839', key: 2 },
|
||||
{ value: 75, color: '#6ED0E0', key: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
instance.state = {
|
||||
thresholds,
|
||||
steps: thresholds.steps,
|
||||
};
|
||||
|
||||
const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
|
||||
instance.onChangeThresholdValue(mockEvent, thresholds.steps[1]);
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 78, color: '#EAB839' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
@ -172,19 +191,22 @@ describe('change threshold value', () => {
|
||||
describe('on blur threshold value', () => {
|
||||
it('should resort rows and update indexes', () => {
|
||||
const { instance } = setup();
|
||||
const thresholds = [
|
||||
{ value: -Infinity, color: '#7EB26D', key: 1 },
|
||||
{ value: 78, color: '#EAB839', key: 2 },
|
||||
{ value: 75, color: '#6ED0E0', key: 3 },
|
||||
];
|
||||
const thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: '#7EB26D', key: 1 },
|
||||
{ value: 78, color: '#EAB839', key: 2 },
|
||||
{ value: 75, color: '#6ED0E0', key: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
instance.setState({
|
||||
thresholds,
|
||||
steps: thresholds.steps,
|
||||
});
|
||||
|
||||
instance.onBlur();
|
||||
|
||||
expect(getCurrentThresholds(instance)).toEqual([
|
||||
expect(getCurrentThresholds(instance).steps).toEqual([
|
||||
{ value: -Infinity, color: '#7EB26D' },
|
||||
{ value: 75, color: '#6ED0E0' },
|
||||
{ value: 78, color: '#EAB839' },
|
||||
|
@ -1,19 +1,31 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { Threshold, sortThresholds } from '@grafana/data';
|
||||
import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, SelectableValue } from '@grafana/data';
|
||||
import { colors } from '../../utils';
|
||||
import { ThemeContext } from '../../themes';
|
||||
import { getColorFromHexRgbOrName } from '@grafana/data';
|
||||
import { Input } from '../Input/Input';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { Themeable } from '../../types';
|
||||
import { css } from 'emotion';
|
||||
import Select from '../Select/Select';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
export interface Props {
|
||||
thresholds?: Threshold[];
|
||||
onChange: (thresholds: Threshold[]) => void;
|
||||
const modes: Array<SelectableValue<ThresholdsMode>> = [
|
||||
{ value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' },
|
||||
{
|
||||
value: ThresholdsMode.Percentage,
|
||||
label: 'Percentage',
|
||||
description: 'Pick threshold based on the percent between min/max',
|
||||
},
|
||||
];
|
||||
|
||||
export interface Props extends Themeable {
|
||||
showAlphaUI?: boolean;
|
||||
thresholds: ThresholdsConfig;
|
||||
onChange: (thresholds: ThresholdsConfig) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
thresholds: ThresholdWithKey[];
|
||||
steps: ThresholdWithKey[];
|
||||
}
|
||||
|
||||
interface ThresholdWithKey extends Threshold {
|
||||
@ -22,12 +34,12 @@ interface ThresholdWithKey extends Threshold {
|
||||
|
||||
let counter = 100;
|
||||
|
||||
function toThresholdsWithKey(thresholds?: Threshold[]): ThresholdWithKey[] {
|
||||
if (!thresholds || thresholds.length === 0) {
|
||||
thresholds = [{ value: -Infinity, color: 'green' }];
|
||||
function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] {
|
||||
if (!steps || steps.length === 0) {
|
||||
steps = [{ value: -Infinity, color: 'green' }];
|
||||
}
|
||||
|
||||
return thresholds.map(t => {
|
||||
return steps.map(t => {
|
||||
return {
|
||||
color: t.color,
|
||||
value: t.value === null ? -Infinity : t.value,
|
||||
@ -40,21 +52,21 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const thresholds = toThresholdsWithKey(props.thresholds);
|
||||
thresholds[0].value = -Infinity;
|
||||
const steps = toThresholdsWithKey(props.thresholds!.steps);
|
||||
steps[0].value = -Infinity;
|
||||
|
||||
this.state = { thresholds };
|
||||
this.state = { steps };
|
||||
}
|
||||
|
||||
onAddThresholdAfter = (threshold: ThresholdWithKey) => {
|
||||
const { thresholds } = this.state;
|
||||
const { steps } = this.state;
|
||||
|
||||
const maxValue = 100;
|
||||
const minValue = 0;
|
||||
|
||||
let prev: ThresholdWithKey | undefined = undefined;
|
||||
let next: ThresholdWithKey | undefined = undefined;
|
||||
for (const t of thresholds) {
|
||||
for (const t of steps) {
|
||||
if (prev && prev.key === threshold.key) {
|
||||
next = t;
|
||||
break;
|
||||
@ -65,35 +77,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
|
||||
const nextValue = next && isFinite(next.value) ? next.value : maxValue;
|
||||
|
||||
const color = colors.filter(c => !thresholds.some(t => t.color === c))[1];
|
||||
const color = colors.filter(c => !steps.some(t => t.color === c))[1];
|
||||
const add = {
|
||||
value: prevValue + (nextValue - prevValue) / 2.0,
|
||||
color: color,
|
||||
key: counter++,
|
||||
};
|
||||
const newThresholds = [...thresholds, add];
|
||||
const newThresholds = [...steps, add];
|
||||
sortThresholds(newThresholds);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
steps: newThresholds,
|
||||
},
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
onRemoveThreshold = (threshold: ThresholdWithKey) => {
|
||||
const { thresholds } = this.state;
|
||||
if (!thresholds.length) {
|
||||
const { steps } = this.state;
|
||||
if (!steps.length) {
|
||||
return;
|
||||
}
|
||||
// Don't remove index 0
|
||||
if (threshold.key === thresholds[0].key) {
|
||||
if (threshold.key === steps[0].key) {
|
||||
return;
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
thresholds: thresholds.filter(t => t.key !== threshold.key),
|
||||
steps: steps.filter(t => t.key !== threshold.key),
|
||||
},
|
||||
() => this.onChange()
|
||||
);
|
||||
@ -104,22 +116,22 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
const parsedValue = parseFloat(cleanValue);
|
||||
const value = isNaN(parsedValue) ? '' : parsedValue;
|
||||
|
||||
const thresholds = this.state.thresholds.map(t => {
|
||||
const steps = this.state.steps.map(t => {
|
||||
if (t.key === threshold.key) {
|
||||
t = { ...t, value: value as number };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
if (thresholds.length) {
|
||||
thresholds[0].value = -Infinity;
|
||||
if (steps.length) {
|
||||
steps[0].value = -Infinity;
|
||||
}
|
||||
this.setState({ thresholds });
|
||||
this.setState({ steps });
|
||||
};
|
||||
|
||||
onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
|
||||
const { thresholds } = this.state;
|
||||
const { steps } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
const newThresholds = steps.map(t => {
|
||||
if (t.key === threshold.key) {
|
||||
t = { ...t, color: color };
|
||||
}
|
||||
@ -129,29 +141,38 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
steps: newThresholds,
|
||||
},
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
const thresholds = [...this.state.thresholds];
|
||||
sortThresholds(thresholds);
|
||||
const steps = [...this.state.steps];
|
||||
sortThresholds(steps);
|
||||
this.setState(
|
||||
{
|
||||
thresholds,
|
||||
steps,
|
||||
},
|
||||
() => this.onChange()
|
||||
);
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
const { thresholds } = this.state;
|
||||
this.props.onChange(thresholdsWithoutKey(thresholds));
|
||||
this.props.onChange(thresholdsWithoutKey(this.props.thresholds, this.state.steps));
|
||||
};
|
||||
|
||||
onModeChanged = (item: SelectableValue<ThresholdsMode>) => {
|
||||
if (item.value) {
|
||||
this.props.onChange({
|
||||
...this.props.thresholds,
|
||||
mode: item.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderInput = (threshold: ThresholdWithKey) => {
|
||||
const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage;
|
||||
return (
|
||||
<div className="thresholds-row-input-inner">
|
||||
<span className="thresholds-row-input-inner-arrow" />
|
||||
@ -181,6 +202,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
{isPercent && (
|
||||
<div className={css(`margin-left:-20px; margin-top:5px;`)}>
|
||||
<i className="fa fa-percent" />
|
||||
</div>
|
||||
)}
|
||||
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
@ -191,42 +217,50 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
const { steps } = this.state;
|
||||
const { theme } = this.props;
|
||||
const t = this.props.thresholds;
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(threshold => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.key}`}>
|
||||
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<>
|
||||
<div className="thresholds">
|
||||
{steps
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(threshold => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.key}`}>
|
||||
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{this.props.showAlphaUI && (
|
||||
<div>
|
||||
<Select options={modes} value={modes.filter(m => m.value === t.mode)} onChange={this.onModeChanged} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function thresholdsWithoutKey(thresholds: ThresholdWithKey[]): Threshold[] {
|
||||
return thresholds.map(t => {
|
||||
const { key, ...rest } = t;
|
||||
return rest; // everything except key
|
||||
});
|
||||
export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: ThresholdWithKey[]): ThresholdsConfig {
|
||||
const mode = thresholds.mode ?? ThresholdsMode.Absolute;
|
||||
return {
|
||||
mode,
|
||||
steps: steps.map(t => {
|
||||
const { key, ...rest } = t;
|
||||
return rest; // everything except key
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -1,182 +1,156 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render with base threshold 1`] = `
|
||||
<ThresholdsEditor
|
||||
onChange={[MockFunction]}
|
||||
thresholds={Array []}
|
||||
<div
|
||||
className="thresholds"
|
||||
>
|
||||
<Component
|
||||
title="Thresholds"
|
||||
<div
|
||||
className="thresholds-row"
|
||||
key="100"
|
||||
>
|
||||
<div
|
||||
className="panel-options-group"
|
||||
className="thresholds-row-add-button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#73BF69",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="thresholds-row-input"
|
||||
>
|
||||
<div
|
||||
className="panel-options-group__header"
|
||||
className="thresholds-row-input-inner"
|
||||
>
|
||||
<span
|
||||
className="panel-options-group__title"
|
||||
>
|
||||
Thresholds
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="panel-options-group__body"
|
||||
>
|
||||
className="thresholds-row-input-inner-arrow"
|
||||
/>
|
||||
<div
|
||||
className="thresholds"
|
||||
className="thresholds-row-input-inner-color"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row"
|
||||
key="100"
|
||||
className="thresholds-row-input-inner-color-colorpicker"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={[Function]}
|
||||
<WithTheme(ColorPicker)
|
||||
color="green"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#73BF69",
|
||||
<ColorPicker
|
||||
color="green"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"type": "dark",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="thresholds-row-input"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-input-inner"
|
||||
>
|
||||
<span
|
||||
className="thresholds-row-input-inner-arrow"
|
||||
/>
|
||||
<div
|
||||
className="thresholds-row-input-inner-color"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-input-inner-color-colorpicker"
|
||||
>
|
||||
<WithTheme(ColorPicker)
|
||||
<PopoverController
|
||||
content={
|
||||
<ColorPickerPopover
|
||||
color="green"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
>
|
||||
<ColorPicker
|
||||
color="green"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"type": "dark",
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"type": "dark",
|
||||
}
|
||||
>
|
||||
<PopoverController
|
||||
content={
|
||||
<ColorPickerPopover
|
||||
color="green"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"type": "dark",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
hideAfter={300}
|
||||
>
|
||||
<ForwardRef(ColorPickerTrigger)
|
||||
color="#73BF69"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "inherit",
|
||||
"border": "none",
|
||||
"borderRadius": 10,
|
||||
"color": "inherit",
|
||||
"cursor": "pointer",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
|
||||
"border": "none",
|
||||
"float": "left",
|
||||
"height": 15,
|
||||
"margin": 0,
|
||||
"position": "relative",
|
||||
"width": 15,
|
||||
"zIndex": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#73BF69",
|
||||
"bottom": 0,
|
||||
"display": "block",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ForwardRef(ColorPickerTrigger)>
|
||||
</PopoverController>
|
||||
</ColorPicker>
|
||||
</WithTheme(ColorPicker)>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-input-inner-value"
|
||||
}
|
||||
/>
|
||||
}
|
||||
hideAfter={300}
|
||||
>
|
||||
<Input
|
||||
className=""
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="Base"
|
||||
<ForwardRef(ColorPickerTrigger)
|
||||
color="#73BF69"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
"background": "inherit",
|
||||
"border": "none",
|
||||
"borderRadius": 10,
|
||||
"color": "inherit",
|
||||
"cursor": "pointer",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="Base"
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
|
||||
"border": "none",
|
||||
"float": "left",
|
||||
"height": 15,
|
||||
"margin": 0,
|
||||
"position": "relative",
|
||||
"width": 15,
|
||||
"zIndex": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#73BF69",
|
||||
"bottom": 0,
|
||||
"display": "block",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ForwardRef(ColorPickerTrigger)>
|
||||
</PopoverController>
|
||||
</ColorPicker>
|
||||
</WithTheme(ColorPicker)>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-input-inner-value"
|
||||
>
|
||||
<Input
|
||||
className=""
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="Base"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="Base"
|
||||
/>
|
||||
</div>
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
</ThresholdsEditor>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -150,8 +150,7 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number, timeZ
|
||||
|
||||
const timeField = data.fields[1];
|
||||
timeField.display = getDisplayProcessor({
|
||||
config: timeField.config,
|
||||
type: timeField.type,
|
||||
field: timeField,
|
||||
isUtc: timeZone === 'utc',
|
||||
});
|
||||
|
||||
|
@ -89,7 +89,7 @@ export class ResultProcessor {
|
||||
// set display processor
|
||||
for (const field of data.fields) {
|
||||
field.display = getDisplayProcessor({
|
||||
config: field.config,
|
||||
field,
|
||||
theme: config.theme,
|
||||
});
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
alignmentFactors: DisplayValueAlignmentFactors
|
||||
): JSX.Element => {
|
||||
const { options } = this.props;
|
||||
const { field, display } = value;
|
||||
const { field, display, view, colIndex } = value;
|
||||
const f = view.dataFrame.fields[colIndex];
|
||||
|
||||
return (
|
||||
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||
@ -34,12 +35,11 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
width={width}
|
||||
height={height}
|
||||
orientation={options.orientation}
|
||||
thresholds={field.thresholds}
|
||||
field={field}
|
||||
display={f.display!}
|
||||
theme={config.theme}
|
||||
itemSpacing={this.getItemSpacing()}
|
||||
displayMode={options.displayMode}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
alignmentFactors={alignmentFactors}
|
||||
|
@ -13,18 +13,24 @@ import {
|
||||
DataLinksEditor,
|
||||
Switch,
|
||||
} from '@grafana/ui';
|
||||
import { FieldDisplayOptions, FieldConfig, DataLink, PanelEditorProps } from '@grafana/data';
|
||||
|
||||
import { Threshold, ValueMapping } from '@grafana/data';
|
||||
import {
|
||||
ThresholdsConfig,
|
||||
ValueMapping,
|
||||
FieldDisplayOptions,
|
||||
FieldConfig,
|
||||
DataLink,
|
||||
PanelEditorProps,
|
||||
} from '@grafana/data';
|
||||
import { BarGaugeOptions, displayModes } from './types';
|
||||
import { orientationOptions } from '../gauge/types';
|
||||
import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) => {
|
||||
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
|
||||
const current = this.props.options.fieldOptions.defaults;
|
||||
this.onDefaultsChange({
|
||||
...current,
|
||||
@ -118,7 +124,12 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
theme={config.theme}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -8,27 +8,53 @@ Object {
|
||||
"mean",
|
||||
],
|
||||
"defaults": Object {
|
||||
"color": Object {
|
||||
"mode": "thresholds",
|
||||
},
|
||||
"decimals": null,
|
||||
"mappings": Array [],
|
||||
"max": -22,
|
||||
"min": 33,
|
||||
"thresholds": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
"max": 33,
|
||||
"min": -22,
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
"unit": "watt",
|
||||
},
|
||||
"overrides": Array [],
|
||||
"thresholds": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "orange",
|
||||
"index": 1,
|
||||
"value": 40,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"index": 2,
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
"values": false,
|
||||
},
|
||||
"orientation": "vertical",
|
||||
|
@ -26,11 +26,9 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
value={display}
|
||||
width={width}
|
||||
height={height}
|
||||
thresholds={field.thresholds}
|
||||
field={field}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
theme={config.theme}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
|
@ -10,13 +10,21 @@ import {
|
||||
PanelOptionsGroup,
|
||||
DataLinksEditor,
|
||||
} from '@grafana/ui';
|
||||
import { PanelEditorProps, FieldDisplayOptions, Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
|
||||
import {
|
||||
PanelEditorProps,
|
||||
FieldDisplayOptions,
|
||||
ThresholdsConfig,
|
||||
ValueMapping,
|
||||
FieldConfig,
|
||||
DataLink,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { GaugeOptions } from './types';
|
||||
import {
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
getDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
labelWidth = 6;
|
||||
@ -30,7 +38,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
||||
showThresholdMarkers: !this.props.options.showThresholdMarkers,
|
||||
});
|
||||
|
||||
onThresholdsChanged = (thresholds: Threshold[]) => {
|
||||
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
|
||||
const current = this.props.options.fieldOptions.defaults;
|
||||
this.onDefaultsChange({
|
||||
...current,
|
||||
@ -122,7 +130,12 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
theme={config.theme}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -7,6 +7,9 @@ Object {
|
||||
"last",
|
||||
],
|
||||
"defaults": Object {
|
||||
"color": Object {
|
||||
"mode": "thresholds",
|
||||
},
|
||||
"decimals": 3,
|
||||
"mappings": Array [
|
||||
Object {
|
||||
@ -21,24 +24,31 @@ Object {
|
||||
],
|
||||
"max": "50",
|
||||
"min": "-50",
|
||||
"thresholds": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "#EAB839",
|
||||
"value": -25,
|
||||
},
|
||||
Object {
|
||||
"color": "#6ED0E0",
|
||||
"value": 0,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"value": 25,
|
||||
},
|
||||
],
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "green",
|
||||
"index": 0,
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "#EAB839",
|
||||
"index": 1,
|
||||
"value": -25,
|
||||
},
|
||||
Object {
|
||||
"color": "#6ED0E0",
|
||||
"index": 2,
|
||||
"value": 0,
|
||||
},
|
||||
Object {
|
||||
"color": "red",
|
||||
"index": 3,
|
||||
"value": 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
"unit": "accMS2",
|
||||
},
|
||||
},
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
getFlotPairsConstant,
|
||||
PanelEvents,
|
||||
formattedValueToString,
|
||||
FieldType,
|
||||
} from '@grafana/data';
|
||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
@ -264,7 +265,7 @@ class GraphElement {
|
||||
links,
|
||||
};
|
||||
const fieldDisplay = getDisplayProcessor({
|
||||
config: fieldConfig,
|
||||
field: { config: fieldConfig, type: FieldType.number },
|
||||
theme: getCurrentTheme(),
|
||||
})(field.values.get(item.dataIndex));
|
||||
linksSupplier = links.length
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { colors } from '@grafana/ui';
|
||||
import {
|
||||
getFlotPairs,
|
||||
getColorFromHexRgbOrName,
|
||||
getDisplayProcessor,
|
||||
NullValueMode,
|
||||
reduceField,
|
||||
@ -16,6 +15,8 @@ import {
|
||||
hasMsResolution,
|
||||
MS_DATE_TIME_FORMAT,
|
||||
DEFAULT_DATE_TIME_FORMAT,
|
||||
FieldColor,
|
||||
FieldColorMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { SeriesOptions, GraphOptions } from './types';
|
||||
@ -32,9 +33,11 @@ export const getGraphSeriesModel = (
|
||||
const graphs: GraphSeriesXY[] = [];
|
||||
|
||||
const displayProcessor = getDisplayProcessor({
|
||||
config: {
|
||||
unit: fieldOptions?.defaults?.unit,
|
||||
decimals: legendOptions.decimals,
|
||||
field: {
|
||||
config: {
|
||||
unit: fieldOptions?.defaults?.unit,
|
||||
decimals: legendOptions.decimals,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -74,15 +77,21 @@ export const getGraphSeriesModel = (
|
||||
});
|
||||
}
|
||||
|
||||
let seriesColor;
|
||||
let color: FieldColor;
|
||||
if (seriesOptions[field.name] && seriesOptions[field.name].color) {
|
||||
// Case when panel has settings provided via SeriesOptions, i.e. graph panel
|
||||
seriesColor = getColorFromHexRgbOrName(seriesOptions[field.name].color);
|
||||
color = {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: seriesOptions[field.name].color,
|
||||
};
|
||||
} else if (field.config && field.config.color) {
|
||||
// Case when color settings are set on field, i.e. Explore logs histogram (see makeSeriesForLogs)
|
||||
seriesColor = field.config.color;
|
||||
color = field.config.color;
|
||||
} else {
|
||||
seriesColor = colors[graphs.length % colors.length];
|
||||
color = {
|
||||
mode: FieldColorMode.Fixed,
|
||||
fixedColor: colors[graphs.length % colors.length],
|
||||
};
|
||||
}
|
||||
|
||||
field.config = fieldOptions
|
||||
@ -90,28 +99,31 @@ export const getGraphSeriesModel = (
|
||||
...field.config,
|
||||
unit: fieldOptions.defaults.unit,
|
||||
decimals: fieldOptions.defaults.decimals,
|
||||
color: seriesColor,
|
||||
color,
|
||||
}
|
||||
: { ...field.config, color: seriesColor };
|
||||
: { ...field.config, color };
|
||||
|
||||
field.display = getDisplayProcessor({ config: { ...field.config }, type: field.type });
|
||||
field.display = getDisplayProcessor({ field });
|
||||
|
||||
// Time step is used to determine bars width when graph is rendered as bar chart
|
||||
const timeStep = getSeriesTimeStep(timeField);
|
||||
const useMsDateFormat = hasMsResolution(timeField);
|
||||
|
||||
timeField.display = getDisplayProcessor({
|
||||
type: timeField.type,
|
||||
isUtc: timeZone === 'utc',
|
||||
config: {
|
||||
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
|
||||
field: {
|
||||
...timeField,
|
||||
type: timeField.type,
|
||||
config: {
|
||||
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graphs.push({
|
||||
label: field.name,
|
||||
data: points,
|
||||
color: seriesColor,
|
||||
color: field.config.color?.fixedColor,
|
||||
info: statsDisplayValues,
|
||||
isVisible: true,
|
||||
yAxis: {
|
||||
|
@ -183,9 +183,11 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
|
||||
if (!fieldInfo) {
|
||||
const processor = getDisplayProcessor({
|
||||
config: {
|
||||
mappings: convertOldAngularValueMapping(this.panel),
|
||||
noValue: 'No Data',
|
||||
field: {
|
||||
config: {
|
||||
mappings: convertOldAngularValueMapping(this.panel),
|
||||
noValue: 'No Data',
|
||||
},
|
||||
},
|
||||
theme: config.theme,
|
||||
});
|
||||
@ -242,11 +244,14 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
const processor = getDisplayProcessor({
|
||||
config: {
|
||||
...fieldInfo.field.config,
|
||||
unit: panel.format,
|
||||
decimals: panel.decimals,
|
||||
mappings: convertOldAngularValueMapping(panel),
|
||||
field: {
|
||||
...fieldInfo.field,
|
||||
config: {
|
||||
...fieldInfo.field.config,
|
||||
unit: panel.format,
|
||||
decimals: panel.decimals,
|
||||
mappings: convertOldAngularValueMapping(panel),
|
||||
},
|
||||
},
|
||||
theme: config.theme,
|
||||
isUtc: dashboard.isTimezoneUtc && dashboard.isTimezoneUtc(),
|
||||
|
@ -13,7 +13,14 @@ import {
|
||||
Select,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { Threshold, ValueMapping, FieldConfig, DataLink, PanelEditorProps, FieldDisplayOptions } from '@grafana/data';
|
||||
import {
|
||||
ThresholdsConfig,
|
||||
ValueMapping,
|
||||
FieldConfig,
|
||||
DataLink,
|
||||
PanelEditorProps,
|
||||
FieldDisplayOptions,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { StatPanelOptions, colorModes, graphModes, justifyModes } from './types';
|
||||
import { orientationOptions } from '../gauge/types';
|
||||
@ -22,9 +29,10 @@ import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) => {
|
||||
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
|
||||
const current = this.props.options.fieldOptions.defaults;
|
||||
this.onDefaultsChange({
|
||||
...current,
|
||||
@ -129,7 +137,12 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
theme={config.theme}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SingleStatBaseOptions, BigValueColorMode, BigValueGraphMode, BigValueJustifyMode } from '@grafana/ui';
|
||||
import { VizOrientation, ReducerID, FieldDisplayOptions, SelectableValue } from '@grafana/data';
|
||||
import { VizOrientation, ReducerID, FieldDisplayOptions, SelectableValue, ThresholdsMode } from '@grafana/data';
|
||||
|
||||
// Structure copied from angular
|
||||
export interface StatPanelOptions extends SingleStatBaseOptions {
|
||||
@ -27,10 +27,13 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
|
||||
values: false,
|
||||
calcs: [ReducerID.mean],
|
||||
defaults: {
|
||||
thresholds: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 80, color: 'red' }, // 80%
|
||||
],
|
||||
thresholds: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [
|
||||
{ value: -Infinity, color: 'green' },
|
||||
{ value: 80, color: 'red' }, // 80%
|
||||
],
|
||||
},
|
||||
mappings: [],
|
||||
},
|
||||
overrides: [],
|
||||
|
@ -18163,7 +18163,7 @@ react-syntax-highlighter@^8.0.1:
|
||||
prismjs "^1.8.4"
|
||||
refractor "^2.4.1"
|
||||
|
||||
react-table@latest:
|
||||
react-table@7.0.0-rc.15:
|
||||
version "7.0.0-rc.15"
|
||||
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a"
|
||||
integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==
|
||||
|
Loading…
Reference in New Issue
Block a user