diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.test.ts new file mode 100644 index 0000000000000..cbc899981717b --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.test.ts @@ -0,0 +1,171 @@ +/* + * 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 { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { TSVB_METRIC_TYPES } from '../../../common/enums'; +import { Metric } from '../../../common/types'; +import { convertToLens } from '.'; +import { createPanel, createSeries } from '../lib/__mocks__'; +import { AvgColumn } from '../lib/convert'; + +const mockGetMetricsColumns = jest.fn(); +const mockGetBucketsColumns = jest.fn(); +const mockGetConfigurationForGauge = jest.fn(); +const mockIsValidMetrics = jest.fn(); +const mockGetDatasourceValue = jest + .fn() + .mockImplementation(() => Promise.resolve(stubLogstashDataView)); +const mockGetDataSourceInfo = jest.fn(); +const mockGetSeriesAgg = jest.fn(); + +jest.mock('../../services', () => ({ + getDataViewsStart: jest.fn(() => mockGetDatasourceValue), +})); + +jest.mock('../lib/series', () => ({ + getMetricsColumns: jest.fn(() => mockGetMetricsColumns()), + getBucketsColumns: jest.fn(() => mockGetBucketsColumns()), + getSeriesAgg: jest.fn(() => mockGetSeriesAgg()), +})); + +jest.mock('../lib/configurations/metric', () => ({ + getConfigurationForGauge: jest.fn(() => mockGetConfigurationForGauge()), +})); + +jest.mock('../lib/metrics', () => { + const actual = jest.requireActual('../lib/metrics'); + return { + isValidMetrics: jest.fn(() => mockIsValidMetrics()), + getReducedTimeRange: jest.fn().mockReturnValue('10'), + SUPPORTED_METRICS: actual.SUPPORTED_METRICS, + getFormulaFromMetric: actual.getFormulaFromMetric, + }; +}); + +jest.mock('../lib/datasource', () => ({ + getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()), +})); + +describe('convertToLens', () => { + const metric = { id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }; + const model = createPanel({ + series: [createSeries({ metrics: [metric] })], + }); + + const metricColumn: AvgColumn = { + columnId: 'col-id', + dataType: 'number', + isSplit: false, + isBucketed: false, + meta: { metricId: metric.id }, + operationType: 'average', + sourceField: metric.field, + params: {}, + reducedTimeRange: '10m', + }; + + beforeEach(() => { + mockIsValidMetrics.mockReturnValue(true); + mockGetDataSourceInfo.mockReturnValue({ + indexPatternId: 'test-index-pattern', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern' }, + }); + mockGetMetricsColumns.mockReturnValue([{}]); + mockGetBucketsColumns.mockReturnValue([{}]); + mockGetConfigurationForGauge.mockReturnValue({}); + mockGetSeriesAgg.mockReturnValue({ metrics: [] }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null for invalid metrics', async () => { + mockIsValidMetrics.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockIsValidMetrics).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported metrics', async () => { + mockGetMetricsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetMetricsColumns).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported buckets', async () => { + mockGetBucketsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + + test('should return null if metric is staticValue', async () => { + const result = await convertToLens({ + ...model, + series: [ + { + ...model.series[0], + metrics: [...model.series[0].metrics, { type: TSVB_METRIC_TYPES.STATIC } as Metric], + }, + ], + }); + expect(result).toBeNull(); + expect(mockGetDataSourceInfo).toBeCalledTimes(0); + }); + + test('should return null if only series agg is specified', async () => { + const result = await convertToLens({ + ...model, + series: [ + { + ...model.series[0], + metrics: [ + { type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min', id: 'some-id' } as Metric, + ], + }, + ], + }); + expect(result).toBeNull(); + }); + + test('should return null configuration is not valid', async () => { + mockGetMetricsColumns.mockReturnValue([metricColumn]); + mockGetSeriesAgg.mockReturnValue({ metrics: [metric] }); + mockGetConfigurationForGauge.mockReturnValue(null); + + const result = await convertToLens(model); + expect(result).toBeNull(); + }); + + test('should return state', async () => { + mockGetMetricsColumns.mockReturnValue([metricColumn]); + mockGetSeriesAgg.mockReturnValue({ metrics: [metric] }); + mockGetConfigurationForGauge.mockReturnValue({}); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.ts new file mode 100644 index 0000000000000..b97f8d59e9537 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/gauge/index.ts @@ -0,0 +1,139 @@ +/* + * 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 uuid from 'uuid'; +import { parseTimeShift } from '@kbn/data-plugin/common'; +import { + FormulaColumn, + getIndexPatternIds, + StaticValueColumn, +} from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { PANEL_TYPES, TSVB_METRIC_TYPES } from '../../../common/enums'; +import { Metric } from '../../../common/types'; +import { getDataViewsStart } from '../../services'; +import { getDataSourceInfo } from '../lib/datasource'; +import { getMetricsColumns, getBucketsColumns } from '../lib/series'; +import { getConfigurationForGauge as getConfiguration } from '../lib/configurations/metric'; +import { + getFormulaFromMetric, + getReducedTimeRange, + isValidMetrics, + SUPPORTED_METRICS, +} from '../lib/metrics'; +import { ConvertTsvbToLensVisualization } from '../types'; +import { + Column, + createFormulaColumnWithoutMeta, + createStaticValueColumn, + Layer as ExtendedLayer, +} from '../lib/convert'; +import { excludeMetaFromLayers, findMetricColumn, getMetricWithCollapseFn } from '../utils'; + +const getMaxFormula = (metric: Metric, column?: Column) => { + const baseFormula = `overall_max`; + if (column && column.operationType === 'formula') { + return `${baseFormula}(${column.params.formula})`; + } + + return `${baseFormula}(${getFormulaFromMetric(SUPPORTED_METRICS[metric.type]!)}(${ + metric.field ?? '' + }))`; +}; + +export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { + const dataViews = getDataViewsStart(); + + const series = model.series[0]; + // not valid time shift + if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') { + return null; + } + + if (!isValidMetrics(series.metrics, PANEL_TYPES.GAUGE, series.time_range_mode)) { + return null; + } + + if (series.metrics[series.metrics.length - 1].type === TSVB_METRIC_TYPES.STATIC) { + return null; + } + + const reducedTimeRange = getReducedTimeRange(model, series, timeRange); + const datasourceInfo = await getDataSourceInfo( + model.index_pattern, + model.time_field, + Boolean(series.override_index_pattern), + series.series_index_pattern, + series.series_time_field, + dataViews + ); + + if (!datasourceInfo) { + return null; + } + + const { indexPatternId, indexPattern } = datasourceInfo; + + // handle multiple metrics + const metricsColumns = getMetricsColumns(series, indexPattern!, model.series.length, { + reducedTimeRange, + }); + if (metricsColumns === null) { + return null; + } + + const bucketsColumns = getBucketsColumns(model, series, metricsColumns, indexPattern!, false); + + if (bucketsColumns === null) { + return null; + } + + const [bucket] = bucketsColumns; + + const extendedLayer: ExtendedLayer = { + indexPatternId, + layerId: uuid(), + columns: [...metricsColumns, ...(bucket ? [bucket] : [])], + columnOrder: [], + }; + + const primarySeries = model.series[0]; + const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries); + + if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) { + return null; + } + + const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, extendedLayer.columns); + if (!primaryColumn) { + return null; + } + + let gaugeMaxColumn: StaticValueColumn | FormulaColumn | null = createFormulaColumnWithoutMeta( + getMaxFormula(primaryMetricWithCollapseFn.metric, primaryColumn) + ); + if (model.gauge_max !== undefined && model.gauge_max !== '') { + gaugeMaxColumn = createStaticValueColumn(model.gauge_max); + } + + const layer = { + ...extendedLayer, + columns: [...extendedLayer.columns, gaugeMaxColumn], + }; + const configuration = getConfiguration(model, layer, bucket, gaugeMaxColumn ?? undefined); + if (!configuration) { + return null; + } + + const layers = Object.values(excludeMetaFromLayers({ 0: layer })); + return { + type: 'lnsMetric', + layers, + configuration, + indexPatternIds: getIndexPatternIds(layers), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts index a64118a1cb507..a3d08e89e91a2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts @@ -25,6 +25,10 @@ const getConvertFnByType = (type: PANEL_TYPES) => { const { convertToLens } = await import('./metric'); return convertToLens; }, + [PANEL_TYPES.GAUGE]: async () => { + const { convertToLens } = await import('./gauge'); + return convertToLens; + }, }; return convertionFns[type]?.(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.test.ts new file mode 100644 index 0000000000000..9cb0238f9d265 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.test.ts @@ -0,0 +1,344 @@ +/* + * 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 { METRIC_TYPES } from '@kbn/data-plugin/common'; +import { TSVB_METRIC_TYPES } from '../../../../../common/enums'; +import { Column, FormulaColumn, Layer } from '../../convert'; +import { createPanel, createSeries } from '../../__mocks__'; +import { getConfigurationForMetric, getConfigurationForGauge } from '.'; + +const mockGetPalette = jest.fn(); + +jest.mock('./palette', () => ({ + getPalette: jest.fn(() => mockGetPalette()), +})); + +describe('getConfigurationForMetric', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetPalette.mockReturnValue(undefined); + }); + + const metricId = 'some-id'; + const metric = { id: metricId, type: METRIC_TYPES.COUNT }; + test('should return null if no series was provided', () => { + const layerId = 'layer-id-1'; + const model = createPanel({ series: [] }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer); + + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return null if only series agg', () => { + const layerId = 'layer-id-1'; + const metric1 = { id: 'metric-id-2', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min' }; + const model = createPanel({ + series: [createSeries({ metrics: [metric1] })], + }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer); + + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return null if multiple series aggs', () => { + const layerId = 'layer-id-1'; + const metric1 = { id: 'metric-id-1', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'sum' }; + const metric2 = { id: 'metric-id-2', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min' }; + const model = createPanel({ + series: [ + createSeries({ metrics: [metric, metric1] }), + createSeries({ metrics: [metric, metric2] }), + ], + }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer); + + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return config if only one series agg is specified', () => { + const layerId = 'layer-id-1'; + const metricId1 = 'metric-id-1'; + + const metricId2 = 'metric-id-2'; + const metric1 = { id: metricId1, type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'sum' }; + const metric2 = { ...metric, id: metricId2 }; + const columnId1 = 'col-id-1'; + const columnId2 = 'col-id-2'; + + const model = createPanel({ + series: [createSeries({ metrics: [metric, metric1] }), createSeries({ metrics: [metric2] })], + }); + const layer: Layer = { + columns: [ + { + columnId: columnId1, + meta: { metricId }, + }, + { + columnId: columnId2, + meta: { metricId: metricId2 }, + }, + ] as Column[], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer); + + expect(config).toEqual({ + breakdownByAccessor: undefined, + collapseFn: 'sum', + layerId, + layerType: 'data', + metricAccessor: columnId1, + palette: undefined, + secondaryMetricAccessor: columnId2, + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); + + test('should return config for single metric', () => { + const layerId = 'layer-id-1'; + const columnId = 'col-id-1'; + const bucketColumnId = 'col-id-2'; + const model = createPanel({ + series: [createSeries({ metrics: [metric] })], + }); + const bucket = { columnId: bucketColumnId } as Column; + const layer: Layer = { + columns: [ + { + columnId, + operationType: 'count', + dataType: 'number', + params: {}, + sourceField: 'document', + isBucketed: false, + isSplit: false, + meta: { metricId }, + }, + ], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer, bucket); + + expect(config).toEqual({ + layerId, + layerType: 'data', + metricAccessor: columnId, + breakdownByAccessor: bucketColumnId, + collapseFn: undefined, + palette: undefined, + secondaryMetricAccessor: undefined, + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); + + test('should return null if palette is invalid', () => { + mockGetPalette.mockReturnValue(null); + const layerId = 'layer-id-1'; + const columnId = 'col-id-1'; + const model = createPanel({ + series: [createSeries({ metrics: [metric] })], + }); + const layer: Layer = { + columns: [ + { + columnId, + operationType: 'count', + dataType: 'number', + params: {}, + sourceField: 'document', + isBucketed: false, + isSplit: false, + meta: { metricId }, + }, + ], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForMetric(model, layer); + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(1); + }); +}); + +describe('getConfigurationForGauge', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetPalette.mockReturnValue(undefined); + }); + + const metricId = 'some-id'; + const maxColumnId = 'col-id-1'; + const metric = { id: metricId, type: METRIC_TYPES.COUNT }; + const gaugeMaxColumn: FormulaColumn = { + references: [], + columnId: maxColumnId, + operationType: 'formula', + isBucketed: false, + isSplit: false, + dataType: 'number', + params: { formula: '100' }, + meta: { metricId }, + }; + + test('should return null if no series was provided', () => { + const layerId = 'layer-id-1'; + const model = createPanel({ series: [] }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForGauge(model, layer, undefined, gaugeMaxColumn); + + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return null if only series agg', () => { + const layerId = 'layer-id-1'; + const metric1 = { id: 'metric-id-2', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min' }; + const model = createPanel({ + series: [createSeries({ metrics: [metric1] })], + }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForGauge(model, layer, undefined, gaugeMaxColumn); + + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return null if palette is invalid', () => { + mockGetPalette.mockReturnValueOnce(null); + const layerId = 'layer-id-1'; + const columnId = 'col-id-1'; + const model = createPanel({ + series: [createSeries({ metrics: [metric] })], + }); + const layer: Layer = { + columns: [ + { + columnId, + operationType: 'count', + dataType: 'number', + params: {}, + sourceField: 'document', + isBucketed: false, + isSplit: false, + meta: { metricId }, + }, + ], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForGauge(model, layer, undefined, gaugeMaxColumn); + expect(config).toBeNull(); + expect(mockGetPalette).toBeCalledTimes(1); + }); + + test('should return config with color if palette is not valid', () => { + const layerId = 'layer-id-1'; + const metric1 = { id: 'metric-id-1', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'sum' }; + const color = '#fff'; + const model = createPanel({ series: [createSeries({ metrics: [metric, metric1], color })] }); + const layer: Layer = { + columns: [], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForGauge(model, layer, undefined, gaugeMaxColumn); + + expect(config).toEqual({ + breakdownByAccessor: undefined, + collapseFn: 'sum', + layerId: 'layer-id-1', + layerType: 'data', + metricAccessor: undefined, + palette: undefined, + maxAccessor: maxColumnId, + color: '#FFFFFF', + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); + + test('should return config with palette', () => { + const palette = { type: 'custom', name: 'default', params: {} }; + mockGetPalette.mockReturnValue(palette); + const layerId = 'layer-id-1'; + const columnId1 = 'col-id-1'; + + const metric1 = { id: 'metric-id-1', type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'sum' }; + const color = '#fff'; + const model = createPanel({ series: [createSeries({ metrics: [metric, metric1], color })] }); + const bucketColumnId = 'bucket-column-id-1'; + const bucket = { columnId: bucketColumnId } as Column; + const layer: Layer = { + columns: [ + { + columnId: columnId1, + operationType: 'count', + dataType: 'number', + params: {}, + sourceField: 'document', + isBucketed: false, + isSplit: false, + meta: { metricId }, + }, + ], + columnOrder: [], + indexPatternId: 'some-index-pattern', + layerId, + }; + const config = getConfigurationForGauge(model, layer, bucket, gaugeMaxColumn); + + expect(config).toEqual({ + breakdownByAccessor: bucket.columnId, + collapseFn: 'sum', + layerId, + layerType: 'data', + metricAccessor: columnId1, + palette, + maxAccessor: maxColumnId, + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts index d1f24485d7646..7b49d604b2343 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts @@ -6,28 +6,12 @@ * Side Public License, v 1. */ +import color from 'color'; import { MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; -import { Metric, Panel, Series } from '../../../../../common/types'; +import { Panel } from '../../../../../common/types'; import { Column, Layer } from '../../convert'; -import { getSeriesAgg } from '../../series'; import { getPalette } from './palette'; - -const getMetricWithCollapseFn = (series: Series | undefined) => { - if (!series) { - return; - } - const { metrics, seriesAgg } = getSeriesAgg(series.metrics); - const visibleMetric = metrics[metrics.length - 1]; - return { metric: visibleMetric, collapseFn: seriesAgg }; -}; - -const findMetricColumn = (metric: Metric | undefined, columns: Column[]) => { - if (!metric) { - return; - } - - return columns.find((column) => 'meta' in column && column.meta.metricId === metric.id); -}; +import { findMetricColumn, getMetricWithCollapseFn } from '../../../utils'; export const getConfigurationForMetric = ( model: Panel, @@ -37,7 +21,6 @@ export const getConfigurationForMetric = ( const [primarySeries, secondarySeries] = model.series.filter(({ hidden }) => !hidden); const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries); - if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) { return null; } @@ -45,7 +28,6 @@ export const getConfigurationForMetric = ( const secondaryMetricWithCollapseFn = getMetricWithCollapseFn(secondarySeries); const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, layer.columns); const secondaryColumn = findMetricColumn(secondaryMetricWithCollapseFn?.metric, layer.columns); - if (primaryMetricWithCollapseFn.collapseFn && secondaryMetricWithCollapseFn?.collapseFn) { return null; } @@ -65,3 +47,35 @@ export const getConfigurationForMetric = ( collapseFn: primaryMetricWithCollapseFn.collapseFn ?? secondaryMetricWithCollapseFn?.collapseFn, }; }; + +export const getConfigurationForGauge = ( + model: Panel, + layer: Layer, + bucket: Column | undefined, + gaugeMaxColumn: Column +): MetricVisConfiguration | null => { + const primarySeries = model.series[0]; + const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries); + if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) { + return null; + } + + const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, layer.columns); + const primaryColor = primarySeries.color ? color(primarySeries.color).hex() : undefined; + + const gaugePalette = getPalette(model.gauge_color_rules ?? [], primaryColor); + if (gaugePalette === null) { + return null; + } + + return { + layerId: layer.layerId, + layerType: 'data', + metricAccessor: primaryColumn?.columnId, + breakdownByAccessor: bucket?.columnId, + maxAccessor: gaugeMaxColumn.columnId, + palette: gaugePalette, + collapseFn: primaryMetricWithCollapseFn.collapseFn, + ...(gaugePalette ? {} : { color: primaryColor }), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts index 827dc15ff171b..b7356f094f91a 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts @@ -9,6 +9,7 @@ import { getPalette } from './palette'; describe('getPalette', () => { + const baseColor = '#fff'; const invalidRules = [ { id: 'some-id-0' }, { id: 'some-id-1', value: 10 }, @@ -16,162 +17,346 @@ describe('getPalette', () => { { id: 'some-id-3', color: '#000' }, { id: 'some-id-4', background_color: '#000' }, ]; - test('should return undefined if no filled rules was provided', () => { - expect(getPalette([])).toBeUndefined(); - expect(getPalette(invalidRules)).toBeUndefined(); - }); - test('should return undefined if only one valid rule is provided and it is not lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'gt', value: 100, background_color: '#000' }, - ]) - ).toBeUndefined(); - }); + describe('Metric', () => { + test('should return undefined if no filled rules was provided', () => { + expect(getPalette([])).toBeUndefined(); + expect(getPalette(invalidRules)).toBeUndefined(); + }); + + test('should return undefined if only one valid rule is provided and it is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gt', value: 100, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); - test('should return custom palette if only one valid rule is provided and it is lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, - ]) - ).toEqual({ - name: 'custom', - params: { - colorStops: [{ color: '#000000', stop: 100 }], - continuity: 'below', - maxSteps: 5, + test('should return custom palette if only one valid rule is provided and it is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + ]) + ).toEqual({ name: 'custom', - progression: 'fixed', - rangeMax: 100, - rangeMin: -Infinity, - rangeType: 'number', - reverse: false, - steps: 1, - stops: [{ color: '#000000', stop: 100 }], - }, - type: 'palette', + params: { + colorStops: [{ color: '#000000', stop: 100 }], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 100, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 1, + stops: [{ color: '#000000', stop: 100 }], + }, + type: 'palette', + }); }); - }); - test('should return undefined if more than two types of rules', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, - { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, - { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, - ]) - ).toBeUndefined(); - }); + test('should return undefined if more than two types of rules', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); - test('should return undefined if two types of rules and last rule is not lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, - { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, - { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, - ]) - ).toBeUndefined(); - }); + test('should return undefined if two types of rules and last rule is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); - test('should return undefined if all rules are lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, - { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, - { id: 'some-id-6', operator: 'lte', value: 150, background_color: '#000' }, - ]) - ).toBeUndefined(); - }); + test('should return undefined if all rules are lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); - test('should return undefined if two types of rules and all except last one are lt and last one is not lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, - { id: 'some-id-7', operator: 'gte', value: 200, background_color: '#000' }, - { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, - ]) - ).toBeUndefined(); - }); + test('should return undefined if two types of rules and all except last one are lt and last one is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'gte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); - test('should return custom palette if two types of rules and all except last one is lt and last one is lte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, - { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, - { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, - ]) - ).toEqual({ - name: 'custom', - params: { - colorStops: [ - { color: '#000000', stop: -Infinity }, - { color: '#000000', stop: 100 }, - { color: '#000000', stop: 150 }, - ], - continuity: 'below', - maxSteps: 5, + test('should return custom palette if two types of rules and all except last one is lt and last one is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toEqual({ name: 'custom', - progression: 'fixed', - rangeMax: 200, - rangeMin: -Infinity, - rangeType: 'number', - reverse: false, - steps: 4, - stops: [ - { color: '#000000', stop: 100 }, - { color: '#000000', stop: 150 }, - { color: '#000000', stop: 200 }, - ], - }, - type: 'palette', + params: { + colorStops: [ + { color: '#000000', stop: -Infinity }, + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 4, + stops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); + + test('should return custom palette if last one is lte and all previous are gte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'none', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: 100, + rangeType: 'number', + reverse: false, + steps: 2, + stops: [ + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); }); }); - test('should return custom palette if last one is lte and all previous are gte', () => { - expect(getPalette([])).toBeUndefined(); - expect( - getPalette([ - ...invalidRules, - { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, - { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, - { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, - ]) - ).toEqual({ - name: 'custom', - params: { - colorStops: [ - { color: '#000000', stop: 100 }, - { color: '#000000', stop: 150 }, - ], - continuity: 'none', - maxSteps: 5, + describe('Gauge', () => { + test('should return undefined if no filled rules was provided', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect(getPalette(invalidRules, baseColor)).toBeUndefined(); + }); + + test('should return undefined if only one valid rule is provided and it is not lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [...invalidRules, { id: 'some-id-5', operator: 'gt', value: 100, gauge: '#000' }], + baseColor + ) + ).toBeUndefined(); + }); + + test('should return custom palette if only one valid rule is provided and it is lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [...invalidRules, { id: 'some-id-5', operator: 'lte', value: 100, gauge: '#000' }], + baseColor + ) + ).toEqual({ + name: 'custom', + params: { + colorStops: [{ color: '#000000', stop: 100 }], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 100, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 1, + stops: [{ color: '#000000', stop: 100 }], + }, + type: 'palette', + }); + }); + + test('should return undefined if more than two types of rules', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, gauge: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, gauge: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, gauge: '#000' }, + ], + baseColor + ) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and last rule is not lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, gauge: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, gauge: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, gauge: '#000' }, + ], + baseColor + ) + ).toBeUndefined(); + }); + + test('should return undefined if all rules are lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, gauge: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, gauge: '#000' }, + { id: 'some-id-6', operator: 'lte', value: 150, gauge: '#000' }, + ], + baseColor + ) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and all except last one are lt and last one is not lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, gauge: '#000' }, + { id: 'some-id-7', operator: 'gte', value: 200, gauge: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, gauge: '#000' }, + ], + baseColor + ) + ).toBeUndefined(); + }); + + test('should return custom palette if two types of rules and all except last one is lt and last one is lte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, gauge: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, gauge: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, gauge: '#000' }, + ], + baseColor + ) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: -Infinity }, + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 4, + stops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); + + test('should return custom palette if last one is lte and all previous are gte', () => { + expect(getPalette([], baseColor)).toBeUndefined(); + expect( + getPalette( + [ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, gauge: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, gauge: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, gauge: '#000' }, + ], + baseColor + ) + ).toEqual({ name: 'custom', - progression: 'fixed', - rangeMax: 200, - rangeMin: 100, - rangeType: 'number', - reverse: false, - steps: 2, - stops: [ - { color: '#000000', stop: 150 }, - { color: '#000000', stop: 200 }, - ], - }, - type: 'palette', + params: { + colorStops: [ + { color: baseColor, stop: -Infinity }, + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 3, + stops: [ + { color: baseColor, stop: 100 }, + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); }); }); }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts index 55741c57595e7..4079ffb396647 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts @@ -22,15 +22,61 @@ type ColorStopsWithMinMax = Pick< 'colorStops' | 'stops' | 'steps' | 'rangeMax' | 'rangeMin' | 'continuity' >; +type MetricColorRules = Exclude; +type GaugeColorRules = Exclude; + +type MetricColorRule = MetricColorRules[number]; +type GaugeColorRule = GaugeColorRules[number]; + +type ValidMetricColorRule = Omit & + ( + | { + background_color: Exclude; + color: MetricColorRule['color']; + } + | { + background_color: MetricColorRule['background_color']; + color: Exclude; + } + ); + +type ValidGaugeColorRule = Omit & { + gauge: Exclude; +}; + +const isValidColorRule = ( + rule: MetricColorRule | GaugeColorRule +): rule is ValidMetricColorRule | ValidGaugeColorRule => { + const { background_color: bColor, color: textColor } = rule as MetricColorRule; + const { gauge } = rule as GaugeColorRule; + + return rule.operator && (bColor ?? textColor ?? gauge) && rule.value !== undefined ? true : false; +}; + +const isMetricColorRule = ( + rule: ValidMetricColorRule | ValidGaugeColorRule +): rule is ValidMetricColorRule => { + const metricRule = rule as ValidMetricColorRule; + return metricRule.background_color ?? metricRule.color ? true : false; +}; + +const getColor = (rule: ValidMetricColorRule | ValidGaugeColorRule) => { + if (isMetricColorRule(rule)) { + return rule.background_color ?? rule.color; + } + return rule.gauge; +}; + const getColorStopsWithMinMaxForAllGteOrWithLte = ( - rules: Exclude, - tailOperator: string + rules: Array, + tailOperator: string, + baseColor?: string ): ColorStopsWithMinMax => { const lastRule = rules[rules.length - 1]; - const lastRuleColor = (lastRule.background_color ?? lastRule.color)!; - + const lastRuleColor = getColor(lastRule); + const initRules = baseColor ? [{ stop: -Infinity, color: baseColor }] : []; const colorStops = rules.reduce((colors, rule, index, rulesArr) => { - const rgbColor = (rule.background_color ?? rule.color)!; + const rgbColor = getColor(rule); if (index === rulesArr.length - 1 && tailOperator === Operators.LTE) { return colors; } @@ -51,7 +97,7 @@ const getColorStopsWithMinMaxForAllGteOrWithLte = ( stop: rule.value!, }, ]; - }, []); + }, initRules); const stops = colorStops.reduce((prevStops, colorStop, index, colorStopsArr) => { if (index === colorStopsArr.length - 1) { @@ -68,24 +114,25 @@ const getColorStopsWithMinMaxForAllGteOrWithLte = ( const [rule] = rules; return { - rangeMin: rule.value, + rangeMin: baseColor ? -Infinity : rule.value, rangeMax: tailOperator === Operators.LTE ? lastRule.value : Infinity, colorStops, stops, steps: colorStops.length, - continuity: tailOperator === Operators.LTE ? 'none' : 'above', + continuity: + tailOperator === Operators.LTE ? (baseColor ? 'below' : 'none') : baseColor ? 'all' : 'above', }; }; const getColorStopsWithMinMaxForLtWithLte = ( - rules: Exclude + rules: Array ): ColorStopsWithMinMax => { const lastRule = rules[rules.length - 1]; const colorStops = rules.reduce((colors, rule, index, rulesArr) => { if (index === 0) { - return [{ color: color((rule.background_color ?? rule.color)!).hex(), stop: -Infinity }]; + return [{ color: color(getColor(rule)).hex(), stop: -Infinity }]; } - const rgbColor = (rule.background_color ?? rule.color)!; + const rgbColor = getColor(rule); return [ ...colors, { @@ -119,10 +166,10 @@ const getColorStopsWithMinMaxForLtWithLte = ( }; const getColorStopWithMinMaxForLte = ( - rule: Exclude[number] + rule: ValidMetricColorRule | ValidGaugeColorRule ): ColorStopsWithMinMax => { const colorStop = { - color: color((rule.background_color ?? rule.color)!).hex(), + color: color(getColor(rule)).hex(), stop: rule.value!, }; return { @@ -135,6 +182,27 @@ const getColorStopWithMinMaxForLte = ( }; }; +const getColorStopWithMinMaxForGte = ( + rule: ValidMetricColorRule | ValidGaugeColorRule, + baseColor?: string +): ColorStopsWithMinMax => { + const colorStop = { + color: color(getColor(rule)).hex(), + stop: rule.value!, + }; + return { + colorStops: [...(baseColor ? [{ color: baseColor, stop: -Infinity }] : []), colorStop], + continuity: baseColor ? 'all' : 'above', + rangeMax: Infinity, + rangeMin: baseColor ? -Infinity : colorStop.stop, + steps: 2, + stops: [ + ...(baseColor ? [{ color: baseColor, stop: colorStop.stop }] : []), + { color: colorStop.color, stop: Infinity }, + ], + }; +}; + const getCustomPalette = ( colorStopsWithMinMax: ColorStopsWithMinMax ): PaletteOutput => { @@ -156,13 +224,12 @@ const getCustomPalette = ( }; export const getPalette = ( - rules: Exclude + rules: MetricColorRules | GaugeColorRules, + baseColor?: string ): PaletteOutput | null | undefined => { - const validRules = - rules.filter( - ({ operator, color: textColor, value, background_color: bColor }) => - operator && (bColor ?? textColor) && value !== undefined - ) ?? []; + const validRules = (rules as Array).filter< + ValidMetricColorRule | ValidGaugeColorRule + >((rule): rule is ValidMetricColorRule | ValidGaugeColorRule => isValidColorRule(rule)); validRules.sort((rule1, rule2) => { return rule1.value! - rule2.value!; @@ -176,10 +243,14 @@ export const getPalette = ( // lnsMetric is supporting lte only, if one rule is defined if (validRules.length === 1) { - if (validRules[0].operator !== Operators.LTE) { - return; + if (validRules[0].operator === Operators.LTE) { + return getCustomPalette(getColorStopWithMinMaxForLte(validRules[0])); } - return getCustomPalette(getColorStopWithMinMaxForLte(validRules[0])); + + if (validRules[0].operator === Operators.GTE) { + return getCustomPalette(getColorStopWithMinMaxForGte(validRules[0], baseColor)); + } + return; } const headRules = validRules.slice(0, -1); @@ -208,7 +279,7 @@ export const getPalette = ( if (rule.operator === Operators.GTE) { return getCustomPalette( - getColorStopsWithMinMaxForAllGteOrWithLte(validRules, tailRule.operator!) + getColorStopsWithMinMaxForAllGteOrWithLte(validRules, tailRule.operator!, baseColor) ); } }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts index 46e9d9e1fae2a..3bb4b743c2588 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts @@ -110,7 +110,6 @@ describe('getLayers', () => { params: { value: '100', }, - meta: { metricId: 'metric-1' }, }, ], columnOrder: [], diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts index 730e770160ecd..15b35fade92c2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts @@ -6,8 +6,12 @@ * Side Public License, v 1. */ +import uuid from 'uuid'; import { METRIC_TYPES } from '@kbn/data-plugin/public'; -import { FormulaParams } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { + FormulaParams, + FormulaColumn as BaseFormulaColumn, +} from '@kbn/visualizations-plugin/common/convert_to_lens'; import { CommonColumnConverterArgs, CommonColumnsConverterArgs, FormulaColumn } from './types'; import { TSVB_METRIC_TYPES } from '../../../../common/enums'; import type { Metric } from '../../../../common/types'; @@ -39,6 +43,19 @@ export const createFormulaColumn = ( }; }; +export const createFormulaColumnWithoutMeta = (formula: string): BaseFormulaColumn => { + const params = convertToFormulaParams(formula); + return { + columnId: uuid(), + operationType: 'formula', + references: [], + dataType: 'string', + isSplit: false, + isBucketed: false, + params: { ...params }, + }; +}; + const convertFormulaScriptForPercentileAggs = ( mathScript: string, variables: Exclude, diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts index e03701e6ea153..b1b15339b37a6 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts @@ -9,7 +9,11 @@ export { isColumnWithMeta, excludeMetaFromColumn } from './column'; export { convertToPercentileColumns, isPercentileColumnWithMeta } from './percentile'; export { convertToPercentileRankColumns, isPercentileRanksColumnWithMeta } from './percentile_rank'; -export { convertMathToFormulaColumn, convertOtherAggsToFormulaColumn } from './formula'; +export { + convertMathToFormulaColumn, + convertOtherAggsToFormulaColumn, + createFormulaColumnWithoutMeta, +} from './formula'; export { convertParentPipelineAggToColumns, convertMetricAggregationColumnWithoutSpecialParams, @@ -17,7 +21,11 @@ export { export { convertToCumulativeSumColumns } from './cumulative_sum'; export { convertFilterRatioToFormulaColumn } from './filter_ratio'; export { convertToLastValueColumn } from './last_value'; -export { convertToStaticValueColumn, convertStaticValueToFormulaColumn } from './static_value'; +export { + convertToStaticValueColumn, + createStaticValueColumn, + convertStaticValueToFormulaColumn, +} from './static_value'; export { convertToFiltersColumn } from './filters'; export { convertToDateHistogramColumn } from './date_histogram'; export { convertToTermsColumn } from './terms'; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts index e03a9d7821364..d3e6aef09b1cf 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { StaticValueParams } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import uuid from 'uuid'; +import { + StaticValueParams, + StaticValueColumn as BaseStaticValueColumn, +} from '@kbn/visualizations-plugin/common/convert_to_lens'; import { CommonColumnsConverterArgs, FormulaColumn, StaticValueColumn } from './types'; import type { Metric } from '../../../../common/types'; import { createColumn, getFormat } from './column'; @@ -39,6 +43,19 @@ export const convertToStaticValueColumn = ( }; }; +export const createStaticValueColumn = (staticValue: number): BaseStaticValueColumn => ({ + columnId: uuid(), + operationType: 'static_value', + references: [], + dataType: 'number', + isStaticValue: true, + isBucketed: false, + isSplit: false, + params: { + value: staticValue.toString(), + }, +}); + export const convertStaticValueToFormulaColumn = ( { series, metrics, dataView }: CommonColumnsConverterArgs, { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts index e5b862a0fe70f..4b3b6c582f915 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts @@ -85,7 +85,13 @@ export type MovingAverageColumn = GenericColumnWithMeta; export type StaticValueColumn = GenericColumnWithMeta; -export type ColumnsWithoutMeta = FiltersColumn | TermsColumn | DateHistogramColumn; +export type ColumnsWithoutMeta = + | FiltersColumn + | TermsColumn + | DateHistogramColumn + | BaseStaticValueColumn + | BaseFormulaColumn; + export type AnyColumnWithReferences = GenericColumnWithMeta; type CommonColumns = Exclude; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts index 76d15793f4516..8be4b444be099 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts @@ -66,6 +66,7 @@ const supportedPanelTypes: readonly PANEL_TYPES[] = [ PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N, PANEL_TYPES.METRIC, + PANEL_TYPES.GAUGE, ]; const supportedTimeRangeModes: readonly TIME_RANGE_DATA_MODES[] = [ diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts index 25f55b5a1c44c..149acc513b9ff 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts @@ -17,7 +17,7 @@ import { getConfigurationForMetric as getConfiguration } from '../lib/configurat import { getReducedTimeRange, isValidMetrics } from '../lib/metrics'; import { ConvertTsvbToLensVisualization } from '../types'; import { ColumnsWithoutMeta, Layer as ExtendedLayer } from '../lib/convert'; -import { excludeMetaFromLayers, getUniqueBuckets } from './utils'; +import { excludeMetaFromLayers, getUniqueBuckets } from '../utils'; const MAX_SERIES = 2; const MAX_BUCKETS = 2; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/utils.test.ts similarity index 97% rename from src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts rename to src/plugins/vis_types/timeseries/public/convert_to_lens/utils.test.ts index 8f880dfe95c5e..c60370871294a 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Column, DateHistogramColumn, TermsColumn } from '../lib/convert'; +import { Column, DateHistogramColumn, TermsColumn } from './lib/convert'; import { getUniqueBuckets } from './utils'; describe('getUniqueBuckets', () => { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/utils.ts similarity index 75% rename from src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts rename to src/plugins/vis_types/timeseries/public/convert_to_lens/utils.ts index 8df1b0f40f8be..93fb56e39012c 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/utils.ts @@ -9,7 +9,14 @@ import { uniqWith } from 'lodash'; import deepEqual from 'react-fast-compare'; import { Layer, Operations, TermsColumn } from '@kbn/visualizations-plugin/common/convert_to_lens'; -import { Layer as ExtendedLayer, excludeMetaFromColumn, ColumnsWithoutMeta } from '../lib/convert'; +import { + Layer as ExtendedLayer, + excludeMetaFromColumn, + ColumnsWithoutMeta, + Column, +} from './lib/convert'; +import { getSeriesAgg } from './lib/series'; +import { Metric, Series } from '../../common/types'; export const excludeMetaFromLayers = ( layers: Record @@ -63,3 +70,20 @@ export const getUniqueBuckets = (buckets: ColumnsWithoutMeta[]) => return deepEqual(bucketWithoutColumnIds1, bucketWithoutColumnIds2); }); + +export const getMetricWithCollapseFn = (series: Series | undefined) => { + if (!series) { + return; + } + const { metrics, seriesAgg } = getSeriesAgg(series.metrics); + const visibleMetric = metrics[metrics.length - 1]; + return { metric: visibleMetric, collapseFn: seriesAgg }; +}; + +export const findMetricColumn = (metric: Metric | undefined, columns: Column[]) => { + if (!metric) { + return; + } + + return columns.find((column) => 'meta' in column && column.meta.metricId === metric.id); +}; diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/gauge.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/gauge.ts new file mode 100644 index 0000000000000..bb2bc9a15ce9b --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/gauge.ts @@ -0,0 +1,44 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { visualize, visualBuilder, lens } = getPageObjects(['visualBuilder', 'visualize', 'lens']); + + const testSubjects = getService('testSubjects'); + + describe('Gauge', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + beforeEach(async () => { + await visualize.navigateToNewVisualization(); + await visualize.clickVisualBuilder(); + await visualBuilder.checkVisualBuilderIsPresent(); + await visualBuilder.resetPage(); + await visualBuilder.clickGauge(); + await visualBuilder.clickDataTab('gauge'); + }); + + it('should show the "Edit Visualization in Lens" menu item', async () => { + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(true); + }); + + it('should convert to Lens', async () => { + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('mtrVis'); + + const metricData = await lens.getMetricVisualizationData(); + expect(metricData[0].title).to.eql('Count of records'); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts index c786ed22c6a1a..1093b0e154947 100644 --- a/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/tsvb/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('TSVB to Lens', function () { loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./gauge')); loadTestFile(require.resolve('./timeseries')); loadTestFile(require.resolve('./dashboard')); });