diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.test.js b/src/plugins/vis_type_timeseries/common/get_last_value.test.js
deleted file mode 100644
index 794bbe17a1e7..000000000000
--- a/src/plugins/vis_type_timeseries/common/get_last_value.test.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { getLastValue } from './get_last_value';
-
-describe('getLastValue(data)', () => {
- test('should returns data if data is not array', () => {
- expect(getLastValue('foo')).toBe('foo');
- });
-
- test('should returns 0 as a value when not an array', () => {
- expect(getLastValue(0)).toBe(0);
- });
-
- test('should returns the last value', () => {
- expect(getLastValue([[1, 2]])).toBe(2);
- });
-
- test('should return 0 as a valid value', () => {
- expect(getLastValue([[0, 0]])).toBe(0);
- });
-
- test('should returns the default value ', () => {
- expect(getLastValue()).toBe('-');
- });
-
- test('should returns 0 if second to last is not defined (default)', () => {
- expect(
- getLastValue([
- [1, null],
- [2, null],
- ])
- ).toBe('-');
- });
-});
diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts b/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts
new file mode 100644
index 000000000000..34e1265b9a6a
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getLastValue, isEmptyValue, EMPTY_VALUE } from './last_value_utils';
+import { clone } from 'lodash';
+
+describe('getLastValue(data)', () => {
+ test('should return data, if data is not an array', () => {
+ const data = 'foo';
+ expect(getLastValue(data)).toBe(data);
+ });
+
+ test('should return 0 as a value, when data is not an array', () => {
+ expect(getLastValue(0)).toBe(0);
+ });
+
+ test('should return the last value', () => {
+ const lastVal = 2;
+ expect(getLastValue([[1, lastVal]])).toBe(lastVal);
+ });
+
+ test('should return 0 as a valid value', () => {
+ expect(getLastValue([[0, 0]])).toBe(0);
+ });
+
+ test("should return empty value (null), if second array is empty or it's last element is null/undefined (default)", () => {
+ expect(
+ getLastValue([
+ [1, null],
+ [2, null],
+ ])
+ ).toBe(EMPTY_VALUE);
+
+ expect(
+ getLastValue([
+ [1, null],
+ [2, undefined],
+ ])
+ ).toBe(EMPTY_VALUE);
+ });
+});
+
+describe('isEmptyValue(value)', () => {
+ test('should return true if is equal to the empty value', () => {
+ // if empty value will change, no need to rewrite test for passing it.
+ const emptyValue =
+ typeof EMPTY_VALUE === 'object' && EMPTY_VALUE != null ? clone(EMPTY_VALUE) : EMPTY_VALUE;
+ expect(isEmptyValue(emptyValue)).toBe(true);
+ });
+
+ test('should return the last value', () => {
+ const notEmptyValue = [...Array(10).keys()];
+ expect(isEmptyValue(notEmptyValue)).toBe(false);
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.js b/src/plugins/vis_type_timeseries/common/last_value_utils.ts
similarity index 50%
rename from src/plugins/vis_type_timeseries/common/get_last_value.js
rename to src/plugins/vis_type_timeseries/common/last_value_utils.ts
index 80adf7098f24..a51a04962a89 100644
--- a/src/plugins/vis_type_timeseries/common/get_last_value.js
+++ b/src/plugins/vis_type_timeseries/common/last_value_utils.ts
@@ -6,16 +6,19 @@
* Side Public License, v 1.
*/
-import { isArray, last } from 'lodash';
+import { isArray, last, isEqual } from 'lodash';
-export const DEFAULT_VALUE = '-';
+export const EMPTY_VALUE = null;
+export const DISPLAY_EMPTY_VALUE = '-';
-const extractValue = (data) => (data && data[1]) ?? null;
+const extractValue = (data: unknown[] | void) => (data && data[1]) ?? EMPTY_VALUE;
-export const getLastValue = (data) => {
+export const getLastValue = (data: unknown) => {
if (!isArray(data)) {
- return data ?? DEFAULT_VALUE;
+ return data;
}
- return extractValue(last(data)) ?? DEFAULT_VALUE;
+ return extractValue(last(data));
};
+
+export const isEmptyValue = (value: unknown) => isEqual(value, EMPTY_VALUE);
diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.test.ts b/src/plugins/vis_type_timeseries/common/operators_utils.test.ts
new file mode 100644
index 000000000000..ad66f058a491
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/common/operators_utils.test.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getOperator, shouldOperate, Rule, Operator } from './operators_utils';
+
+describe('getOperator(operator)', () => {
+ test('should return operator function', () => {
+ const operatorName = Operator.Gte;
+ const operator = getOperator(operatorName);
+ expect(typeof operator).toBe('function');
+ });
+});
+
+describe('shouldOperate(rule, value)', () => {
+ test('should operate, if value is not null and rule value is not null', () => {
+ const rule: Rule = {
+ value: 1,
+ operator: Operator.Gte,
+ };
+ const value = 2;
+
+ expect(shouldOperate(rule, value)).toBeTruthy();
+ });
+
+ test('should operate, if value is null and operator allows null value', () => {
+ const rule: Rule = {
+ operator: Operator.Empty,
+ value: null,
+ };
+ const value = null;
+
+ expect(shouldOperate(rule, value)).toBeTruthy();
+ });
+
+ test("should not operate, if value is null and operator doesn't allow null values", () => {
+ const rule: Rule = {
+ operator: Operator.Gte,
+ value: 2,
+ };
+ const value = null;
+
+ expect(shouldOperate(rule, value)).toBeFalsy();
+ });
+
+ test("should not operate, if rule value is null and operator doesn't allow null values", () => {
+ const rule: Rule = {
+ operator: Operator.Gte,
+ value: null,
+ };
+ const value = 3;
+
+ expect(shouldOperate(rule, value)).toBeFalsy();
+ });
+});
diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.ts b/src/plugins/vis_type_timeseries/common/operators_utils.ts
new file mode 100644
index 000000000000..603e63159b22
--- /dev/null
+++ b/src/plugins/vis_type_timeseries/common/operators_utils.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { gt, gte, lt, lte, isNull } from 'lodash';
+
+export enum Operator {
+ Gte = 'gte',
+ Lte = 'lte',
+ Gt = 'gt',
+ Lt = 'lt',
+ Empty = 'empty',
+}
+
+export interface Rule {
+ operator: Operator;
+ value: unknown;
+}
+
+type OperatorsAllowNullType = {
+ [name in Operator]?: boolean;
+};
+
+const OPERATORS = {
+ [Operator.Gte]: gte,
+ [Operator.Lte]: lte,
+ [Operator.Gt]: gt,
+ [Operator.Lt]: lt,
+ [Operator.Empty]: isNull,
+};
+
+const OPERATORS_ALLOW_NULL: OperatorsAllowNullType = {
+ [Operator.Empty]: true,
+};
+
+export const getOperator = (operator: Operator) => {
+ return OPERATORS[operator];
+};
+
+// This check is necessary for preventing from comparing null values with numeric rules.
+export const shouldOperate = (rule: Rule, value: unknown) =>
+ (isNull(rule.value) && OPERATORS_ALLOW_NULL[rule.operator]) ||
+ (!isNull(rule.value) && !isNull(value));
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx
index 9ea8898636ce..3b1356d57174 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx
@@ -12,22 +12,51 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from '@kbn/test/jest';
import { collectionActions } from './lib/collection_actions';
-import { ColorRules, ColorRulesProps } from './color_rules';
+import {
+ ColorRules,
+ ColorRulesProps,
+ colorRulesOperatorsList,
+ ColorRulesOperator,
+} from './color_rules';
+import { Operator } from '../../../common/operators_utils';
describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js', () => {
- const defaultProps = ({
+ const emptyRule: ColorRulesOperator = colorRulesOperatorsList.filter(
+ (operator) => operator.method === Operator.Empty
+ )[0];
+ const notEmptyRule: ColorRulesOperator = colorRulesOperatorsList.filter(
+ (operator) => operator.method !== Operator.Empty
+ )[0];
+
+ const getColorRulesProps = (gaugeColorRules: unknown = []) => ({
name: 'gauge_color_rules',
- model: {
- gauge_color_rules: [
- {
- gauge: null,
- value: 0,
- id: 'unique value',
- },
- ],
- },
+ model: { gauge_color_rules: gaugeColorRules },
onChange: jest.fn(),
- } as unknown) as ColorRulesProps;
+ });
+
+ const defaultProps = (getColorRulesProps([
+ {
+ gauge: null,
+ value: 0,
+ id: 'unique value',
+ },
+ ]) as unknown) as ColorRulesProps;
+
+ const emptyColorRuleProps = (getColorRulesProps([
+ {
+ operator: emptyRule?.method,
+ value: emptyRule?.value,
+ id: 'unique value',
+ },
+ ]) as unknown) as ColorRulesProps;
+
+ const notEmptyColorRuleProps = (getColorRulesProps([
+ {
+ operator: notEmptyRule?.method,
+ value: notEmptyRule?.value,
+ id: 'unique value',
+ },
+ ]) as unknown) as ColorRulesProps;
describe('ColorRules', () => {
it('should render empty
node', () => {
@@ -47,6 +76,7 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js'
expect(isNode).toBeTruthy();
});
+
it('should handle change of operator and value correctly', () => {
collectionActions.handleChange = jest.fn();
const wrapper = mountWithIntl();
@@ -57,8 +87,23 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js'
expect((collectionActions.handleChange as jest.Mock).mock.calls[0][1].operator).toEqual('gt');
const numberInput = findTestSubject(wrapper, 'colorRuleValue');
+
numberInput.simulate('change', { target: { value: '123' } });
expect((collectionActions.handleChange as jest.Mock).mock.calls[1][1].value).toEqual(123);
});
+
+ it('should handle render of value field if empty value oparetor is selected by default', () => {
+ collectionActions.handleChange = jest.fn();
+ const wrapper = mountWithIntl();
+ const numberInput = findTestSubject(wrapper, 'colorRuleValue');
+ expect(numberInput.exists()).toBeFalsy();
+ });
+
+ it('should handle render of value field if not empty operator is selected by default', () => {
+ collectionActions.handleChange = jest.fn();
+ const wrapper = mountWithIntl();
+ const numberInput = findTestSubject(wrapper, 'colorRuleValue');
+ expect(numberInput.exists()).toBeTruthy();
+ });
});
});
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx
index 7aea5f934ee9..0cc64528ae3f 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx
@@ -23,6 +23,7 @@ import { AddDeleteButtons } from './add_delete_buttons';
import { collectionActions } from './lib/collection_actions';
import { ColorPicker, ColorPickerProps } from './color_picker';
import { TimeseriesVisParams } from '../../types';
+import { Operator } from '../../../common/operators_utils';
export interface ColorRulesProps {
name: keyof TimeseriesVisParams;
@@ -40,10 +41,17 @@ interface ColorRule {
id: string;
background_color?: string;
color?: string;
- operator?: string;
+ operator?: Operator;
text?: string;
}
+export interface ColorRulesOperator {
+ label: string;
+ method: Operator;
+ value?: unknown;
+ hideValueSelector?: boolean;
+}
+
const defaultSecondaryName = i18n.translate(
'visTypeTimeseries.colorRules.defaultSecondaryNameLabel',
{
@@ -54,33 +62,45 @@ const defaultPrimaryName = i18n.translate('visTypeTimeseries.colorRules.defaultP
defaultMessage: 'background',
});
-const operatorOptions = [
+export const colorRulesOperatorsList: ColorRulesOperator[] = [
{
label: i18n.translate('visTypeTimeseries.colorRules.greaterThanLabel', {
defaultMessage: '> greater than',
}),
- value: 'gt',
+ method: Operator.Gt,
},
{
label: i18n.translate('visTypeTimeseries.colorRules.greaterThanOrEqualLabel', {
defaultMessage: '>= greater than or equal',
}),
- value: 'gte',
+ method: Operator.Gte,
},
{
label: i18n.translate('visTypeTimeseries.colorRules.lessThanLabel', {
defaultMessage: '< less than',
}),
- value: 'lt',
+ method: Operator.Lt,
},
{
label: i18n.translate('visTypeTimeseries.colorRules.lessThanOrEqualLabel', {
defaultMessage: '<= less than or equal',
}),
- value: 'lte',
+ method: Operator.Lte,
+ },
+ {
+ label: i18n.translate('visTypeTimeseries.colorRules.emptyLabel', {
+ defaultMessage: 'empty',
+ }),
+ method: Operator.Empty,
+ hideValueSelector: true,
},
];
+const operatorOptions = colorRulesOperatorsList.map((operator) => ({
+ label: operator.label,
+ value: operator.method,
+}));
+
export class ColorRules extends Component {
constructor(props: ColorRulesProps) {
super(props);
@@ -100,9 +120,14 @@ export class ColorRules extends Component {
handleOperatorChange = (item: ColorRule) => {
return (options: Array>) => {
+ const selectedOperator = colorRulesOperatorsList.find(
+ (operator) => options[0]?.value === operator.method
+ );
+ const value = selectedOperator?.value ?? null;
collectionActions.handleChange(this.props, {
...item,
- operator: options[0].value,
+ operator: options[0]?.value,
+ value,
});
};
};
@@ -119,7 +144,11 @@ export class ColorRules extends Component {
const selectedOperatorOption = operatorOptions.find(
(option) => model.operator === option.value
);
+ const selectedOperator = colorRulesOperatorsList.find(
+ (operator) => model.operator === operator.method
+ );
+ const hideValueSelectorField = selectedOperator?.hideValueSelector ?? false;
const labelStyle = { marginBottom: 0 };
let secondary;
@@ -203,19 +232,19 @@ export class ColorRules extends Component {
fullWidth
/>
-
-
-
-
-
+ {!hideValueSelectorField && (
+
+
+
+ )}
{
- let value;
-
- if (val === DEFAULT_VALUE) {
- return val;
+ if (isEmptyValue(val)) {
+ return DISPLAY_EMPTY_VALUE;
}
+ let value;
+
if (!isNumber(val)) {
value = val;
} else {
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
index a464771b01af..6140726975cb 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js
@@ -10,9 +10,10 @@ import PropTypes from 'prop-types';
import React from 'react';
import { visWithSplits } from '../../vis_with_splits';
import { createTickFormatter } from '../../lib/tick_formatter';
-import _, { get, isUndefined, assign, includes } from 'lodash';
+import { get, isUndefined, assign, includes } from 'lodash';
import { Gauge } from '../../../visualizations/views/gauge';
-import { getLastValue } from '../../../../../common/get_last_value';
+import { getLastValue } from '../../../../../common/last_value_utils';
+import { getOperator, shouldOperate } from '../../../../../common/operators_utils';
function getColors(props) {
const { model, visData } = props;
@@ -21,9 +22,9 @@ function getColors(props) {
let gauge;
if (model.gauge_color_rules) {
model.gauge_color_rules.forEach((rule) => {
- if (rule.operator && rule.value != null) {
- const value = (series[0] && getLastValue(series[0].data)) || 0;
- if (_[rule.operator](value, rule.value)) {
+ if (rule.operator) {
+ const value = getLastValue(series[0]?.data);
+ if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) {
gauge = rule.gauge;
text = rule.text;
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
index 3029bba04b45..b35ee977d3e4 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js
@@ -10,10 +10,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import { visWithSplits } from '../../vis_with_splits';
import { createTickFormatter } from '../../lib/tick_formatter';
-import _, { get, isUndefined, assign, includes, pick } from 'lodash';
+import { get, isUndefined, assign, includes, pick } from 'lodash';
import { Metric } from '../../../visualizations/views/metric';
-import { getLastValue } from '../../../../../common/get_last_value';
+import { getLastValue } from '../../../../../common/last_value_utils';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
+import { getOperator, shouldOperate } from '../../../../../common/operators_utils';
function getColors(props) {
const { model, visData } = props;
@@ -22,9 +23,9 @@ function getColors(props) {
let background;
if (model.background_color_rules) {
model.background_color_rules.forEach((rule) => {
- if (rule.operator && rule.value != null) {
- const value = (series[0] && getLastValue(series[0].data)) || 0;
- if (_[rule.operator](value, rule.value)) {
+ if (rule.operator) {
+ const value = getLastValue(series[0]?.data);
+ if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) {
background = rule.background_color;
color = rule.color;
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
index 41e6236cbc39..0b3a24615c0e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js
@@ -9,13 +9,13 @@
import { getCoreStart } from '../../../../services';
import { createTickFormatter } from '../../lib/tick_formatter';
import { TopN } from '../../../visualizations/views/top_n';
-import { getLastValue } from '../../../../../common/get_last_value';
+import { getLastValue } from '../../../../../common/last_value_utils';
import { isBackgroundInverted } from '../../../lib/set_is_reversed';
import { replaceVars } from '../../lib/replace_vars';
import PropTypes from 'prop-types';
import React from 'react';
-import { sortBy, first, get, gt, gte, lt, lte } from 'lodash';
-const OPERATORS = { gt, gte, lt, lte };
+import { sortBy, first, get } from 'lodash';
+import { getOperator, shouldOperate } from '../../../../../common/operators_utils';
function sortByDirection(data, direction, fn) {
if (direction === 'desc') {
@@ -53,8 +53,8 @@ function TopNVisualization(props) {
let color = item.color || seriesConfig.color;
if (model.bar_color_rules) {
model.bar_color_rules.forEach((rule) => {
- if (rule.operator && rule.value != null && rule.bar_color) {
- if (OPERATORS[rule.operator](value, rule.value)) {
+ if (shouldOperate(rule, value) && rule.operator && rule.bar_color) {
+ if (getOperator(rule.operator)(value, rule.value)) {
color = rule.bar_color;
}
}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
index 31ea3412972e..000701c3a076 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js
@@ -11,7 +11,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed';
-import { getLastValue } from '../../../../common/get_last_value';
+import { getLastValue } from '../../../../common/last_value_utils';
import { getValueBy } from '../lib/get_value_by';
import { GaugeVis } from './gauge_vis';
import reactcss from 'reactcss';
@@ -61,7 +61,7 @@ export class Gauge extends Component {
render() {
const { metric, type } = this.props;
const { scale, translateX, translateY } = this.state;
- const value = metric && getLastValue(metric.data);
+ const value = getLastValue(metric?.data);
const max = (metric && getValueBy('max', metric.data)) || 1;
const formatter =
(metric && (metric.tickFormatter || metric.formatter)) ||
@@ -76,16 +76,13 @@ export class Gauge extends Component {
left: this.state.left || 0,
transform: `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`,
},
- },
- valueColor: {
- value: {
+ valueColor: {
color: this.props.valueColor,
},
},
},
this.props
);
-
const gaugeProps = {
value,
reversed: isBackgroundDark(this.props.backgroundColor),
@@ -114,7 +111,7 @@ export class Gauge extends Component {
{title}
-
+
{formatter(value)}
{additionalLabel}
@@ -127,7 +124,7 @@ export class Gauge extends Component {
ref={(el) => (this.inner = el)}
style={styles.inner}
>
-
+
{formatter(value)}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js
index c8789f98969f..30b7844a90fd 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js
@@ -12,6 +12,7 @@ import _ from 'lodash';
import reactcss from 'reactcss';
import { calculateCoordinates } from '../lib/calculate_coordinates';
import { COLORS } from '../constants/chart';
+import { isEmptyValue } from '../../../../common/last_value_utils';
export class GaugeVis extends Component {
constructor(props) {
@@ -55,10 +56,14 @@ export class GaugeVis extends Component {
render() {
const { type, value, max, color } = this.props;
+
+ // if value is empty array, no metrics to display.
+ const formattedValue = isEmptyValue(value) ? 1 : value;
+
const { scale, translateX, translateY } = this.state;
const size = 2 * Math.PI * 50;
const sliceSize = type === 'half' ? 0.6 : 1;
- const percent = value < max ? value / max : 1;
+ const percent = formattedValue < max ? formattedValue / max : 1;
const styles = reactcss(
{
default: {
@@ -161,6 +166,6 @@ GaugeVis.propTypes = {
max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
metric: PropTypes.object,
reversed: PropTypes.bool,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
type: PropTypes.oneOf(['half', 'circle']),
};
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
index 17cadb94457b..bc4230d0a15e 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js
@@ -11,7 +11,7 @@ import React, { Component } from 'react';
import _ from 'lodash';
import reactcss from 'reactcss';
-import { getLastValue } from '../../../../common/get_last_value';
+import { getLastValue } from '../../../../common/last_value_utils';
import { calculateCoordinates } from '../lib/calculate_coordinates';
export class Metric extends Component {
@@ -58,7 +58,8 @@ export class Metric extends Component {
const { metric, secondary } = this.props;
const { scale, translateX, translateY } = this.state;
const primaryFormatter = (metric && (metric.tickFormatter || metric.formatter)) || ((n) => n);
- const primaryValue = primaryFormatter(getLastValue(metric && metric.data));
+ const primaryValue = primaryFormatter(getLastValue(metric?.data));
+
const styles = reactcss(
{
default: {
@@ -120,7 +121,6 @@ export class Metric extends Component {
if (this.props.reversed) {
className += ' tvbVisMetric--reversed';
}
-
return (
(this.resize = el)} className="tvbVisMetric__resize">
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
index 2559ed543e54..0c43ab157fbb 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js
@@ -8,7 +8,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { getLastValue } from '../../../../common/get_last_value';
+import { getLastValue, isEmptyValue } from '../../../../common/last_value_utils';
import { labelDateFormatter } from '../../components/lib/label_date_formatter';
import { emptyLabel } from '../../../../common/empty_label';
import reactcss from 'reactcss';
@@ -97,15 +97,16 @@ export class TopN extends Component {
const renderMode = TopN.getRenderMode(min, max);
const key = `${item.id || item.label}`;
const lastValue = getLastValue(item.data);
+ // if result is empty, all bar need to be colored.
+ const lastValueFormatted = isEmptyValue(lastValue) ? 1 : lastValue;
const formatter = item.tickFormatter || this.props.tickFormatter;
- const isPositiveValue = lastValue >= 0;
+ const isPositiveValue = lastValueFormatted >= 0;
const intervalLength = TopN.calcDomain(renderMode, min, max);
// if both are 0, the division returns NaN causing unexpected behavior.
// For this it defaults to 0
- const width = 100 * (Math.abs(lastValue) / intervalLength) || 0;
+ const width = 100 * (Math.abs(lastValueFormatted) / intervalLength) || 0;
const label = item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label;
-
const styles = reactcss(
{
default: {
@@ -150,7 +151,7 @@ export class TopN extends Component {
const intervalSettings = this.props.series.reduce(
(acc, series, index) => {
- const value = getLastValue(series.data);
+ const value = getLastValue(series.data) ?? 1;
return {
min: !index || value < acc.min ? value : acc.min,
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js
index 9e3a2ac71ed0..88b06d7f7ffa 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js
@@ -8,7 +8,7 @@
import { buildProcessorFunction } from '../build_processor_function';
import { processors } from '../response_processors/table';
-import { getLastValue } from '../../../../common/get_last_value';
+import { getLastValue } from '../../../../common/last_value_utils';
import { first, get } from 'lodash';
import { overwrite } from '../helpers';
import { getActiveSeries } from '../helpers/get_active_series';
diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
index 6f214745e129..212c033a65c2 100644
--- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
+++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
@@ -13,6 +13,7 @@ import {
commonAddSupportOfDualIndexSelectionModeInTSVB,
commonHideTSVBLastValueIndicator,
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
+ commonAddEmptyValueColorRule,
} from '../migrations/visualization_common_migrations';
const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => {
@@ -36,6 +37,13 @@ const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: Serial
};
};
+const byValueAddEmptyValueColorRule = (state: SerializableState) => {
+ return {
+ ...state,
+ savedVis: commonAddEmptyValueColorRule(state.savedVis),
+ };
+};
+
export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
return {
id: 'visualization',
@@ -47,6 +55,7 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
byValueHideTSVBLastValueIndicator,
byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel
)(state),
+ '7.14.0': (state) => flow(byValueAddEmptyValueColorRule)(state),
},
};
};
diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
index 3f09f19d9ac6..13b8d8c4a0f9 100644
--- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
@@ -6,6 +6,9 @@
* Side Public License, v 1.
*/
+import { get, last } from 'lodash';
+import uuid from 'uuid';
+
export const commonAddSupportOfDualIndexSelectionModeInTSVB = (visState: any) => {
if (visState && visState.type === 'metrics') {
const { params } = visState;
@@ -42,3 +45,49 @@ export const commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (visStat
return visState;
};
+
+export const commonAddEmptyValueColorRule = (visState: any) => {
+ if (visState && visState.type === 'metrics') {
+ const params: any = get(visState, 'params') || {};
+
+ const getRuleWithComparingToZero = (rules: any[] = []) => {
+ const compareWithEqualMethods = ['gte', 'lte'];
+ return last(
+ rules.filter((rule) => compareWithEqualMethods.includes(rule.operator) && rule.value === 0)
+ );
+ };
+
+ const convertRuleToEmpty = (rule: any = {}) => ({
+ ...rule,
+ id: uuid.v4(),
+ operator: 'empty',
+ value: null,
+ });
+
+ const addEmptyRuleToListIfNecessary = (rules: any[]) => {
+ const rule = getRuleWithComparingToZero(rules);
+
+ if (rule) {
+ return [...rules, convertRuleToEmpty(rule)];
+ }
+
+ return rules;
+ };
+
+ const colorRules = {
+ bar_color_rules: addEmptyRuleToListIfNecessary(params.bar_color_rules),
+ background_color_rules: addEmptyRuleToListIfNecessary(params.background_color_rules),
+ gauge_color_rules: addEmptyRuleToListIfNecessary(params.gauge_color_rules),
+ };
+
+ return {
+ ...visState,
+ params: {
+ ...params,
+ ...colorRules,
+ },
+ };
+ }
+
+ return visState;
+};
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
index dbe5482c442b..36e1635ad473 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
@@ -2017,4 +2017,101 @@ describe('migration visualization', () => {
expect(params.use_kibana_indexes).toBeFalsy();
});
});
+
+ describe('7.14.0 tsvb - add empty value rule to savedObjects with less and greater then zero rules', () => {
+ const migrate = (doc: any) =>
+ visualizationSavedObjectTypeMigrations['7.14.0'](
+ doc as Parameters
[0],
+ savedObjectMigrationContext
+ );
+
+ const rule1 = { value: 0, operator: 'lte', color: 'rgb(145, 112, 184)' };
+ const rule2 = { value: 0, operator: 'gte', color: 'rgb(96, 146, 192)' };
+ const rule3 = { value: 0, operator: 'gt', color: 'rgb(84, 179, 153)' };
+ const rule4 = { value: 0, operator: 'lt', color: 'rgb(84, 179, 153)' };
+
+ const createTestDocWithType = (params: any) => ({
+ attributes: {
+ title: 'My Vis',
+ description: 'This is my super cool vis.',
+ visState: `{
+ "type":"metrics",
+ "params": ${JSON.stringify(params)}
+ }`,
+ },
+ });
+
+ const checkEmptyRuleIsAddedToArray = (
+ rulesArrayProperty: string,
+ prevParams: any,
+ migratedParams: any,
+ rule: any
+ ) => {
+ expect(migratedParams).toHaveProperty(rulesArrayProperty);
+ expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy();
+ expect(migratedParams[rulesArrayProperty].length).toBe(
+ prevParams[rulesArrayProperty].length + 1
+ );
+
+ const lastElementIndex = migratedParams[rulesArrayProperty].length - 1;
+ expect(migratedParams[rulesArrayProperty][lastElementIndex]).toHaveProperty('operator');
+ expect(migratedParams[rulesArrayProperty][lastElementIndex].operator).toEqual('empty');
+ expect(migratedParams[rulesArrayProperty][lastElementIndex].color).toEqual(rule.color);
+ };
+
+ const checkRuleIsNotAddedToArray = (
+ rulesArrayProperty: string,
+ prevParams: any,
+ migratedParams: any,
+ rule: any
+ ) => {
+ expect(migratedParams).toHaveProperty(rulesArrayProperty);
+ expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy();
+ expect(migratedParams[rulesArrayProperty].length).toBe(prevParams[rulesArrayProperty].length);
+ // expects, that array contains one element...
+ expect(migratedParams[rulesArrayProperty][0].operator).toBe(rule.operator);
+ };
+
+ it('should add empty rule if operator = lte and value = 0', () => {
+ const params = {
+ bar_color_rules: [rule1],
+ background_color_rules: [rule1],
+ gauge_color_rules: [rule1],
+ };
+ const migratedTestDoc = migrate(createTestDocWithType(params));
+ const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState);
+
+ checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule1);
+ checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule1);
+ checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule1);
+ });
+
+ it('should add empty rule if operator = gte and value = 0', () => {
+ const params = {
+ bar_color_rules: [rule2],
+ background_color_rules: [rule2],
+ gauge_color_rules: [rule2],
+ };
+ const migratedTestDoc = migrate(createTestDocWithType(params));
+ const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState);
+
+ checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule2);
+ checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule2);
+ checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule2);
+ });
+
+ it('should not add empty rule if operator = gt or lt and value = any', () => {
+ const params = {
+ bar_color_rules: [rule3],
+ background_color_rules: [rule3],
+ gauge_color_rules: [rule4],
+ };
+ const migratedTestDoc = migrate(createTestDocWithType(params));
+ const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState);
+
+ checkRuleIsNotAddedToArray('bar_color_rules', params, migratedParams, rule3);
+ checkRuleIsNotAddedToArray('background_color_rules', params, migratedParams, rule3);
+ checkRuleIsNotAddedToArray('gauge_color_rules', params, migratedParams, rule4);
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
index b9885588b6f7..c5050b4a6940 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
@@ -15,6 +15,7 @@ import {
commonAddSupportOfDualIndexSelectionModeInTSVB,
commonHideTSVBLastValueIndicator,
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
+ commonAddEmptyValueColorRule,
} from './visualization_common_migrations';
const migrateIndexPattern: SavedObjectMigrationFn = (doc) => {
@@ -966,6 +967,29 @@ const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn
};
};
+const addEmptyValueColorRule: SavedObjectMigrationFn = (doc) => {
+ const visStateJSON = get(doc, 'attributes.visState');
+ let visState;
+
+ if (visStateJSON) {
+ try {
+ visState = JSON.parse(visStateJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ }
+ const newVisState = commonAddEmptyValueColorRule(visState);
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ visState: JSON.stringify(newVisState),
+ },
+ };
+ }
+ return doc;
+};
+
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@@ -1012,4 +1036,5 @@ export const visualizationSavedObjectTypeMigrations = {
hideTSVBLastValueIndicator,
removeDefaultIndexPatternAndTimeFieldFromTSVBModel
),
+ '7.14.0': flow(addEmptyValueColorRule),
};