diff --git a/src/plugins/vis_type_timeseries/common/constants.ts b/src/plugins/vis_type_timeseries/common/constants.ts index 1debfaf951e999..bddbf095e895eb 100644 --- a/src/plugins/vis_type_timeseries/common/constants.ts +++ b/src/plugins/vis_type_timeseries/common/constants.ts @@ -14,3 +14,4 @@ export const ROUTES = { FIELDS: '/api/metrics/fields', }; export const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes'; +export const TSVB_DEFAULT_COLOR = '#68BC00'; diff --git a/src/plugins/vis_type_timeseries/common/types/panel_model.ts b/src/plugins/vis_type_timeseries/common/types/panel_model.ts index 7eea4e64e7c6f7..2ac9125534ac79 100644 --- a/src/plugins/vis_type_timeseries/common/types/panel_model.ts +++ b/src/plugins/vis_type_timeseries/common/types/panel_model.ts @@ -24,6 +24,7 @@ interface Percentile { shade?: number | string; value?: number | string; percentile?: string; + color?: string; } export interface Metric { @@ -52,6 +53,7 @@ export interface Metric { type: string; value?: string; values?: string[]; + colors?: string[]; size?: string | number; agg_with?: string; order?: string; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js index c536856327f283..c4a49a393acd68 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js @@ -27,7 +27,7 @@ const runTest = (aggType, name, test, additionalProps = {}) => { ...additionalProps, }; const series = { ...SERIES, metrics: [metric] }; - const panel = { ...PANEL, series }; + const panel = PANEL; it(name, () => { const wrapper = mountWithIntl( diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index 45bb5387c5cd3d..94adb37de156b2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -102,7 +102,13 @@ export function PercentileAgg(props) { /> } > - + diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx new file mode 100644 index 00000000000000..7b08715ba1a934 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { MultiValueRow } from './multi_value_row'; +import { ColorPicker } from '../../color_picker'; + +describe('MultiValueRow', () => { + const model = { + id: 95, + value: '95', + color: '#00028', + }; + const props = { + model, + enableColorPicker: true, + onChange: jest.fn(), + onDelete: jest.fn(), + onAdd: jest.fn(), + disableAdd: false, + disableDelete: false, + }; + + const wrapper = shallowWithIntl(); + + it('displays a color picker if the enableColorPicker prop is true', () => { + expect(wrapper.find(ColorPicker).length).toEqual(1); + }); + + it('not displays a color picker if the enableColorPicker prop is false', () => { + const newWrapper = shallowWithIntl(); + expect(newWrapper.find(ColorPicker).length).toEqual(0); + }); + + it('sets the picker color to the model color', () => { + expect(wrapper.find(ColorPicker).prop('value')).toEqual('#00028'); + }); + + it('should have called the onChange function on color change', () => { + wrapper.find(ColorPicker).simulate('change'); + expect(props.onChange).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx index 8fa65e6ce40db5..d1174ce95367c3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx @@ -10,17 +10,21 @@ import React, { ChangeEvent } from 'react'; import { get } from 'lodash'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { TSVB_DEFAULT_COLOR } from '../../../../../common/constants'; import { AddDeleteButtons } from '../../add_delete_buttons'; +import { ColorPicker, ColorProps } from '../../color_picker'; interface MultiValueRowProps { model: { id: number; value: string; + color: string; }; disableAdd: boolean; disableDelete: boolean; - onChange: ({ value, id }: { id: number; value: string }) => void; + enableColorPicker: boolean; + onChange: ({ value, id, color }: { id: number; value: string; color: string }) => void; onDelete: (model: { id: number; value: string }) => void; onAdd: () => void; } @@ -32,6 +36,7 @@ export const MultiValueRow = ({ onAdd, disableAdd, disableDelete, + enableColorPicker, }: MultiValueRowProps) => { const onFieldNumberChange = (event: ChangeEvent) => onChange({ @@ -39,9 +44,25 @@ export const MultiValueRow = ({ value: get(event, 'target.value'), }); + const onColorPickerChange = (props: ColorProps) => + onChange({ + ...model, + color: props?.color || TSVB_DEFAULT_COLOR, + }); + return ( + {enableColorPicker && ( + + + + )} { const { panel, fields, indexPattern } = props; - const defaults = { values: [''] }; + const defaults = { values: [''], colors: [TSVB_DEFAULT_COLOR] }; const model = { ...defaults, ...props.model }; const htmlId = htmlIdGenerator(); @@ -56,11 +59,17 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); const handleNumberChange = createNumberHandler(handleChange); + const percentileRankSeries = + panel.series.find((s) => s.id === props.series.id) || panel.series[0]; + // If the series is grouped by, then these colors are not respected, no need to display the color picker */ + const isGroupedBy = panel.series.length > 0 && percentileRankSeries.split_mode !== 'everything'; + const enableColorPicker = !isGroupedBy && !['table', 'metric', 'markdown'].includes(panel.type); - const handlePercentileRankValuesChange = (values: Metric['values']) => { + const handlePercentileRankValuesChange = (values: Metric['values'], colors: Metric['colors']) => { handleChange({ ...model, values, + colors, }); }; return ( @@ -119,8 +128,10 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { disableAdd={isTablePanel} disableDelete={isTablePanel} showOnlyLastRow={isTablePanel} - model={model.values!} + values={model.values!} + colors={model.colors!} onChange={handlePercentileRankValuesChange} + enableColorPicker={enableColorPicker} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx index 2441611b87d316..f3eb290f77a08c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx @@ -10,35 +10,43 @@ import React from 'react'; import { last } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { TSVB_DEFAULT_COLOR } from '../../../../../common/constants'; import { MultiValueRow } from './multi_value_row'; interface PercentileRankValuesProps { - model: Array; + values: string[]; + colors: string[]; disableDelete: boolean; disableAdd: boolean; showOnlyLastRow: boolean; - onChange: (values: any[]) => void; + enableColorPicker: boolean; + onChange: (values: string[], colors: string[]) => void; } export const PercentileRankValues = (props: PercentileRankValuesProps) => { - const model = props.model || []; - const { onChange, disableAdd, disableDelete, showOnlyLastRow } = props; + const values = props.values || []; + const colors = props.colors || []; + const { onChange, disableAdd, disableDelete, showOnlyLastRow, enableColorPicker } = props; - const onChangeValue = ({ value, id }: { value: string; id: number }) => { - model[id] = value; + const onChangeValue = ({ value, id, color }: { value: string; id: number; color: string }) => { + values[id] = value; + colors[id] = color; - onChange(model); + onChange(values, colors); }; const onDeleteValue = ({ id }: { id: number }) => - onChange(model.filter((item, currentIndex) => id !== currentIndex)); - const onAddValue = () => onChange([...model, '']); + onChange( + values.filter((item, currentIndex) => id !== currentIndex), + colors.filter((item, currentIndex) => id !== currentIndex) + ); + const onAddValue = () => onChange([...values, ''], [...colors, TSVB_DEFAULT_COLOR]); const renderRow = ({ rowModel, disableDeleteRow, disableAddRow, }: { - rowModel: { id: number; value: string }; + rowModel: { id: number; value: string; color: string }; disableDeleteRow: boolean; disableAddRow: boolean; }) => ( @@ -50,6 +58,7 @@ export const PercentileRankValues = (props: PercentileRankValuesProps) => { disableDelete={disableDeleteRow} disableAdd={disableAddRow} model={rowModel} + enableColorPicker={enableColorPicker} /> ); @@ -59,19 +68,21 @@ export const PercentileRankValues = (props: PercentileRankValuesProps) => { {showOnlyLastRow && renderRow({ rowModel: { - id: model.length - 1, - value: last(model) || '', + id: values.length - 1, + value: last(values) || '', + color: last(colors) || TSVB_DEFAULT_COLOR, }, disableAddRow: true, disableDeleteRow: true, })} {!showOnlyLastRow && - model.map((value, id, array) => + values.map((value, id, array) => renderRow({ rowModel: { id, value: value || '', + color: colors[id] || TSVB_DEFAULT_COLOR, }, disableAddRow: disableAdd, disableDeleteRow: disableDelete || array.length < 2, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js index 5b8b56849fcdad..bfd41b9cdfafca 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; +import { TSVB_DEFAULT_COLOR } from '../../../../common/constants'; import { collectionActions } from '../lib/collection_actions'; import { AddDeleteButtons } from '../add_delete_buttons'; import uuid from 'uuid'; @@ -23,10 +24,11 @@ import { EuiFlexGrid, EuiPanel, } from '@elastic/eui'; +import { ColorPicker } from '../color_picker'; import { FormattedMessage } from '@kbn/i18n/react'; export const newPercentile = (opts) => { - return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts); + return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2, color: TSVB_DEFAULT_COLOR }, opts); }; export class Percentiles extends Component { @@ -39,11 +41,20 @@ export class Percentiles extends Component { }; } + handleColorChange(item) { + return (val) => { + const handleChange = collectionActions.handleChange.bind(null, this.props); + handleChange(_.assign({}, item, val)); + }; + } + renderRow = (row, i, items) => { - const defaults = { value: '', percentile: '', shade: '' }; + const defaults = { value: '', percentile: '', shade: '', color: TSVB_DEFAULT_COLOR }; const model = { ...defaults, ...row }; - const { panel } = this.props; + const { panel, seriesId } = this.props; const flexItemStyle = { minWidth: 100 }; + const percentileSeries = panel.series.find((s) => s.id === seriesId) || panel.series[0]; + const isGroupedBy = panel.series.length > 0 && percentileSeries.split_mode !== 'everything'; const percentileFieldNumber = ( @@ -106,7 +117,19 @@ export class Percentiles extends Component { - + + {/* If the series is grouped by, then these colors are not respected, + no need to display the color picker */} + {!isGroupedBy && !['table', 'metric', 'markdown'].includes(panel.type) && ( + + + + )} {percentileFieldNumber} { + const props = { + name: 'percentiles', + model: { + values: ['100', '200'], + colors: ['#00028', 'rgba(96,146,192,1)'], + percentiles: [ + { + id: 'ece1c4b0-fb4b-11eb-a845-3de627f78e15', + mode: 'line', + shade: 0.2, + color: '#00028', + value: 50, + }, + ], + }, + panel: { + time_range_mode: 'entire_time_range', + series: [ + { + axis_position: 'right', + chart_type: 'line', + color: '#68BC00', + fill: 0.5, + formatter: 'number', + id: '64e4b07a-206e-4a0d-87e1-d6f5864f4acb', + label: '', + line_width: 1, + metrics: [ + { + values: ['100', '200'], + colors: ['#68BC00', 'rgba(96,146,192,1)'], + field: 'AvgTicketPrice', + id: 'a64ed16c-c642-4705-8045-350206595530', + type: 'percentile', + percentiles: [ + { + id: 'ece1c4b0-fb4b-11eb-a845-3de627f78e15', + mode: 'line', + shade: 0.2, + color: '#68BC00', + value: 50, + }, + ], + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + point_size: 1, + separate_axis: 0, + split_mode: 'everything', + stacked: 'none', + type: 'timeseries', + }, + ], + show_grid: 1, + show_legend: 1, + time_field: '', + tooltip_mode: 'show_all', + type: 'timeseries', + use_kibana_indexes: true, + }, + seriesId: '64e4b07a-206e-4a0d-87e1-d6f5864f4acb', + id: 'iecdd7ef1-fb4b-11eb-8db9-69be3a5b3be0', + onBlur: jest.fn(), + onChange: jest.fn(), + onFocus: jest.fn(), + }; + + const wrapper = shallowWithIntl(); + + it('displays a color picker if is not grouped by', () => { + expect(wrapper.find(ColorPicker).length).toEqual(1); + }); + + it('sets the picker color to the model color', () => { + expect(wrapper.find(ColorPicker).prop('value')).toEqual('#00028'); + }); + + it('should have called the onChange function on color change', () => { + wrapper.find(ColorPicker).simulate('change'); + expect(props.onChange).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx index 280e4eda338998..fbfec011210365 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; const COMMAS_NUMS_ONLY_RE = /[^0-9,]/g; -interface ColorProps { +export interface ColorProps { [key: string]: string | null; } diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index a2efe39b2c7f0e..364de9c6b42458 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -11,6 +11,7 @@ import uuid from 'uuid/v4'; import { TSVB_EDITOR_NAME } from './application/editor_controller'; import { PANEL_TYPES, TOOLTIP_MODES } from '../common/enums'; import { isStringTypeIndexPattern } from '../common/index_patterns_utils'; +import { TSVB_DEFAULT_COLOR } from '../common/constants'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; import { getDataStart } from './services'; @@ -30,7 +31,7 @@ export const metricsVisDefinition = { series: [ { id: uuid(), - color: '#68BC00', + color: TSVB_DEFAULT_COLOR, split_mode: 'everything', palette: { type: 'palette', diff --git a/src/plugins/vis_type_timeseries/public/test_utils/index.ts b/src/plugins/vis_type_timeseries/public/test_utils/index.ts index d5121237cd2a76..b88c765baf3a35 100644 --- a/src/plugins/vis_type_timeseries/public/test_utils/index.ts +++ b/src/plugins/vis_type_timeseries/public/test_utils/index.ts @@ -35,5 +35,5 @@ export const SERIES = { export const PANEL = { type: 'timeseries', index_pattern: INDEX_PATTERN, - series: SERIES, + series: [SERIES], }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js index 5eec0f8f2c6f6b..b7e0026132af3b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.js @@ -38,7 +38,10 @@ export function percentile(resp, panel, series, meta, extractFields) { if (percentile.mode === 'band') { results.push({ id, - color: split.color, + color: + series.split_mode === 'everything' && percentile.color + ? percentile.color + : split.color, label: split.label, data, lines: { @@ -60,8 +63,11 @@ export function percentile(resp, panel, series, meta, extractFields) { const decoration = getDefaultDecoration(series); results.push({ id, - color: split.color, - label: `${split.label} (${percentileValue})`, + color: + series.split_mode === 'everything' && percentile.color + ? percentile.color + : split.color, + label: `(${percentileValue}) ${split.label}`, data, ...decoration, }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js index 9174876c768c51..de304913d6c694 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile.test.js @@ -31,7 +31,7 @@ describe('percentile(resp, panel, series)', () => { type: 'percentile', field: 'cpu', percentiles: [ - { id: '10-90', mode: 'band', value: 10, percentile: 90, shade: 0.2 }, + { id: '10-90', mode: 'band', value: 10, percentile: 90, shade: 0.2, color: '#000028' }, { id: '50', mode: 'line', value: 50 }, ], }, @@ -84,7 +84,7 @@ describe('percentile(resp, panel, series)', () => { expect(results).toHaveLength(2); expect(results[0]).toHaveProperty('id', 'test:10-90'); - expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)'); + expect(results[0]).toHaveProperty('color', '#000028'); expect(results[0]).toHaveProperty('label', 'Percentile of cpu'); expect(results[0]).toHaveProperty('lines'); expect(results[0].lines).toEqual({ @@ -102,7 +102,7 @@ describe('percentile(resp, panel, series)', () => { expect(results[1]).toHaveProperty('id', 'test:50'); expect(results[1]).toHaveProperty('color', 'rgb(255, 0, 0)'); - expect(results[1]).toHaveProperty('label', 'Percentile of cpu (50)'); + expect(results[1]).toHaveProperty('label', '(50) Percentile of cpu'); expect(results[1]).toHaveProperty('stack', false); expect(results[1]).toHaveProperty('lines'); expect(results[1].lines).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js index 96b004d4b539e2..7203be4d2feb6e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js @@ -34,8 +34,11 @@ export function percentileRank(resp, panel, series, meta, extractFields) { results.push({ data, id: `${split.id}:${percentileRank}:${index}`, - label: `${split.label} (${percentileRank || 0})`, - color: split.color, + label: `(${percentileRank || 0}) ${split.label}`, + color: + series.split_mode === 'everything' && metric.colors + ? metric.colors[index] + : split.color, ...getDefaultDecoration(series), }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts new file mode 100644 index 00000000000000..c1e5bd006ef688 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.test.ts @@ -0,0 +1,94 @@ +/* + * 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. + */ + +// @ts-expect-error no typed yet +import { percentileRank } from './percentile_rank'; +import type { Panel, Series } from '../../../../../common/types'; + +describe('percentile_rank(resp, panel, series, meta, extractFields)', () => { + let panel: Panel; + let series: Series; + let resp: unknown; + beforeEach(() => { + panel = { + time_field: 'timestamp', + } as Panel; + series = ({ + chart_type: 'line', + stacked: 'stacked', + line_width: 1, + point_size: 1, + fill: 0, + color: 'rgb(255, 0, 0)', + id: 'test', + split_mode: 'everything', + metrics: [ + { + id: 'pct_rank', + type: 'percentile_rank', + field: 'cpu', + values: ['1000', '500'], + colors: ['#000028', '#0000FF'], + }, + ], + } as unknown) as Series; + resp = { + aggregations: { + test: { + timeseries: { + buckets: [ + { + key: 1, + pct_rank: { + values: { '500.0': 1, '1000.0': 2 }, + }, + }, + { + key: 2, + pct_rank: { + values: { '500.0': 3, '1000.0': 1 }, + }, + }, + ], + }, + }, + }, + }; + }); + + test('calls next when finished', async () => { + const next = jest.fn(); + + await percentileRank(resp, panel, series, {})(next)([]); + + expect(next.mock.calls.length).toEqual(1); + }); + + test('creates a series', async () => { + const next = (results: unknown) => results; + const results = await percentileRank(resp, panel, series, {})(next)([]); + + expect(results).toHaveLength(2); + + expect(results[0]).toHaveProperty('id', 'test:1000:0'); + expect(results[0]).toHaveProperty('color', '#000028'); + expect(results[0]).toHaveProperty('label', '(1000) Percentile Rank of cpu'); + expect(results[0].data).toEqual([ + [1, 2], + [2, 1], + ]); + + expect(results[1]).toHaveProperty('id', 'test:500:1'); + expect(results[1]).toHaveProperty('color', '#0000FF'); + expect(results[1]).toHaveProperty('label', '(500) Percentile Rank of cpu'); + expect(results[1].data).toEqual([ + [1, 1], + [2, 3], + ]); + }); +});