diff --git a/src/plugins/vis_types/timeseries/public/application/components/series_editor.js b/src/plugins/vis_types/timeseries/public/application/components/series_editor.js index 7bd72b85edc1d..531075b36244b 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/series_editor.js +++ b/src/plugins/vis_types/timeseries/public/application/components/series_editor.js @@ -65,6 +65,10 @@ export class SeriesEditor extends Component { } }; + handleSeriesChange = (doc) => { + handleChange(this.props, doc); + }; + render() { const { limit, model, name, fields, colorPicker } = this.props; const list = model[name].filter((val, index) => index < (limit || Infinity)); @@ -89,7 +93,7 @@ export class SeriesEditor extends Component { disableDelete={model[name].length < 2} fields={fields} onAdd={() => handleAdd(this.props, newSeriesFn)} - onChange={(doc) => handleChange(this.props, doc)} + onChange={this.handleSeriesChange} onClone={() => this.handleClone(row)} onDelete={() => handleDelete(this.props, row)} model={row} diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js index 48ba0e5403665..506ce0dbdf2a9 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/config.js @@ -35,7 +35,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; import { checkIfNumericMetric } from '../../lib/check_if_numeric_metric'; import { QueryBarWrapper } from '../../query_bar_wrapper'; -import { DATA_FORMATTERS } from '../../../../../common/enums'; +import { DATA_FORMATTERS, BUCKET_TYPES } from '../../../../../common/enums'; import { isConfigurationFeatureEnabled } from '../../../../../common/check_ui_restrictions'; import { filterCannotBeAppliedErrorMessage } from '../../../../../common/errors'; import { tsvbEditorRowStyles } from '../../../styles/common.styles'; @@ -50,13 +50,20 @@ class TableSeriesConfigUi extends Component { } } + handleAggregateByChange = (selectedOptions) => { + this.props.onChange({ + aggregate_by: selectedOptions?.[0], + }); + }; + + handleSelectChange = createSelectHandler(this.props.onChange); + handleTextChange = createTextHandler(this.props.onChange); + changeModelFormatter = (formatter) => this.props.onChange({ formatter }); render() { const defaults = { offset_time: '', value_template: '{{value}}' }; const model = { ...defaults, ...this.props.model }; - const handleSelectChange = createSelectHandler(this.props.onChange); - const handleTextChange = createTextHandler(this.props.onChange); const htmlId = htmlIdGenerator(); const functionOptions = [ @@ -160,7 +167,7 @@ class TableSeriesConfigUi extends Component { fullWidth > - + @@ -222,11 +229,7 @@ class TableSeriesConfigUi extends Component { fields={this.props.fields} indexPattern={this.props.panel.index_pattern} value={model.aggregate_by} - onChange={(value) => - this.props.onChange({ - aggregate_by: value?.[0], - }) - } + onChange={this.handleAggregateByChange} fullWidth restrict={[ KBN_FIELD_TYPES.NUMBER, @@ -236,7 +239,7 @@ class TableSeriesConfigUi extends Component { KBN_FIELD_TYPES.STRING, ]} uiRestrictions={this.props.uiRestrictions} - type={'terms'} + type={BUCKET_TYPES.TERMS} /> @@ -251,9 +254,10 @@ class TableSeriesConfigUi extends Component { fullWidth > 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 index cbc899981717b..9943cfa627e23 100644 --- 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 @@ -6,10 +6,11 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; 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 { Panel, Metric } from '../../../common/types'; import { convertToLens } from '.'; import { createPanel, createSeries } from '../lib/__mocks__'; import { AvgColumn } from '../lib/convert'; @@ -58,6 +59,10 @@ describe('convertToLens', () => { series: [createSeries({ metrics: [metric] })], }); + const vis = { + params: model, + } as Vis; + const metricColumn: AvgColumn = { columnId: 'col-id', dataType: 'number', @@ -89,51 +94,55 @@ describe('convertToLens', () => { test('should return null for invalid metrics', async () => { mockIsValidMetrics.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); 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], - }, - ], - }); + params: { + ...model, + series: [ + { + ...model.series[0], + metrics: [...model.series[0].metrics, { type: TSVB_METRIC_TYPES.STATIC } as Metric], + }, + ], + }, + } as Vis); 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, - ], - }, - ], - }); + params: { + ...model, + series: [ + { + ...model.series[0], + metrics: [ + { type: TSVB_METRIC_TYPES.SERIES_AGG, function: 'min', id: 'some-id' } as Metric, + ], + }, + ], + }, + } as Vis); expect(result).toBeNull(); }); @@ -142,7 +151,7 @@ describe('convertToLens', () => { mockGetSeriesAgg.mockReturnValue({ metrics: [metric] }); mockGetConfigurationForGauge.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeNull(); }); @@ -151,8 +160,8 @@ describe('convertToLens', () => { mockGetSeriesAgg.mockReturnValue({ metrics: [metric] }); mockGetConfigurationForGauge.mockReturnValue({}); - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], @@ -163,8 +172,8 @@ describe('convertToLens', () => { hidden: false, }), ], - }) - ); + }), + } as Vis); 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 index b97f8d59e9537..87d5333d4be51 100644 --- 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 @@ -45,7 +45,10 @@ const getMaxFormula = (metric: Metric, column?: Column) => { }))`; }; -export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { +export const convertToLens: ConvertTsvbToLensVisualization = async ( + { params: model }, + timeRange +) => { const dataViews = getDataViewsStart(); const series = model.series[0]; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts index 309f066b18f29..a97395f64c113 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import type { Panel } from '../../common/types'; import { convertTSVBtoLensConfiguration } from '.'; @@ -42,7 +43,9 @@ describe('convertTSVBtoLensConfiguration', () => { ...model, type: 'markdown', } as Panel; - const triggerOptions = await convertTSVBtoLensConfiguration(metricModel); + const triggerOptions = await convertTSVBtoLensConfiguration({ + params: metricModel, + } as Vis); expect(triggerOptions).toBeNull(); }); @@ -51,7 +54,9 @@ describe('convertTSVBtoLensConfiguration', () => { ...model, use_kibana_indexes: false, }; - const triggerOptions = await convertTSVBtoLensConfiguration(stringIndexPatternModel); + const triggerOptions = await convertTSVBtoLensConfiguration({ + params: stringIndexPatternModel, + } as Vis); expect(triggerOptions).toBeNull(); }); }); 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 a3d08e89e91a2..3e1982aa0903e 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 @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import { TimeRange } from '@kbn/data-plugin/common'; import type { Panel } from '../../common/types'; import { PANEL_TYPES } from '../../common/enums'; @@ -29,6 +30,10 @@ const getConvertFnByType = (type: PANEL_TYPES) => { const { convertToLens } = await import('./gauge'); return convertToLens; }, + [PANEL_TYPES.TABLE]: async () => { + const { convertToLens } = await import('./table'); + return convertToLens; + }, }; return convertionFns[type]?.(); @@ -39,17 +44,17 @@ const getConvertFnByType = (type: PANEL_TYPES) => { * Returns the Lens model, only if it is supported. If not, it returns null. * In case of null, the menu item is disabled and the user can't navigate to Lens. */ -export const convertTSVBtoLensConfiguration = async (model: Panel, timeRange?: TimeRange) => { +export const convertTSVBtoLensConfiguration = async (vis: Vis, timeRange?: TimeRange) => { // Disables the option for not supported charts, for the string mode and for series with annotations - if (!model.use_kibana_indexes) { + if (!vis.params.use_kibana_indexes) { return null; } // Disables if model is invalid - if (model.isModelInvalid) { + if (vis.params.isModelInvalid) { return null; } - const convertFn = await getConvertFnByType(model.type); + const convertFn = await getConvertFnByType(vis.params.type); - return (await convertFn?.(model, timeRange)) ?? null; + return (await convertFn?.(vis, timeRange)) ?? null; }; 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 index 9cb0238f9d265..0fabef6eebdbe 100644 --- 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 @@ -14,7 +14,7 @@ import { getConfigurationForMetric, getConfigurationForGauge } from '.'; const mockGetPalette = jest.fn(); -jest.mock('./palette', () => ({ +jest.mock('../palette', () => ({ getPalette: jest.fn(() => mockGetPalette()), })); 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 7b49d604b2343..e6814b0797a1a 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 @@ -10,7 +10,7 @@ import color from 'color'; import { MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; import { Panel } from '../../../../../common/types'; import { Column, Layer } from '../../convert'; -import { getPalette } from './palette'; +import { getPalette } from '../palette'; import { findMetricColumn, getMetricWithCollapseFn } from '../../../utils'; export const getConfigurationForMetric = ( 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/palette/index.test.ts similarity index 99% rename from src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts rename to src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/palette/index.test.ts index b7356f094f91a..059f0372c451a 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/palette/index.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getPalette } from './palette'; +import { getPalette } from '.'; describe('getPalette', () => { const baseColor = '#fff'; 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/palette/index.ts similarity index 83% rename from src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts rename to src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/palette/index.ts index 4079ffb396647..159804a1ea29b 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/palette/index.ts @@ -8,7 +8,7 @@ import color from 'color'; import { ColorStop, CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import { uniqBy } from 'lodash'; -import { Panel } from '../../../../../common/types'; +import { Panel, Series } from '../../../../../common/types'; const Operators = { GTE: 'gte', @@ -24,9 +24,11 @@ type ColorStopsWithMinMax = Pick< type MetricColorRules = Exclude; type GaugeColorRules = Exclude; +type SeriesColorRules = Exclude; type MetricColorRule = MetricColorRules[number]; type GaugeColorRule = GaugeColorRules[number]; +type SeriesColorRule = SeriesColorRules[number]; type ValidMetricColorRule = Omit & ( @@ -44,31 +46,47 @@ type ValidGaugeColorRule = Omit & { gauge: Exclude; }; +type ValidSeriesColorRule = Omit & { + text: Exclude; +}; + const isValidColorRule = ( rule: MetricColorRule | GaugeColorRule -): rule is ValidMetricColorRule | ValidGaugeColorRule => { +): rule is ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule => { const { background_color: bColor, color: textColor } = rule as MetricColorRule; const { gauge } = rule as GaugeColorRule; + const { text } = rule as SeriesColorRule; - return rule.operator && (bColor ?? textColor ?? gauge) && rule.value !== undefined ? true : false; + return Boolean( + rule.operator && (bColor ?? textColor ?? gauge ?? text) && rule.value !== undefined + ); }; const isMetricColorRule = ( - rule: ValidMetricColorRule | ValidGaugeColorRule + rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule ): rule is ValidMetricColorRule => { const metricRule = rule as ValidMetricColorRule; return metricRule.background_color ?? metricRule.color ? true : false; }; -const getColor = (rule: ValidMetricColorRule | ValidGaugeColorRule) => { +const isGaugeColorRule = ( + rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule +): rule is ValidGaugeColorRule => { + const metricRule = rule as ValidGaugeColorRule; + return Boolean(metricRule.gauge); +}; + +const getColor = (rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule) => { if (isMetricColorRule(rule)) { return rule.background_color ?? rule.color; + } else if (isGaugeColorRule(rule)) { + return rule.gauge; } - return rule.gauge; + return rule.text; }; const getColorStopsWithMinMaxForAllGteOrWithLte = ( - rules: Array, + rules: Array, tailOperator: string, baseColor?: string ): ColorStopsWithMinMax => { @@ -125,7 +143,7 @@ const getColorStopsWithMinMaxForAllGteOrWithLte = ( }; const getColorStopsWithMinMaxForLtWithLte = ( - rules: Array + rules: Array ): ColorStopsWithMinMax => { const lastRule = rules[rules.length - 1]; const colorStops = rules.reduce((colors, rule, index, rulesArr) => { @@ -166,7 +184,7 @@ const getColorStopsWithMinMaxForLtWithLte = ( }; const getColorStopWithMinMaxForLte = ( - rule: ValidMetricColorRule | ValidGaugeColorRule + rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule ): ColorStopsWithMinMax => { const colorStop = { color: color(getColor(rule)).hex(), @@ -183,7 +201,7 @@ const getColorStopWithMinMaxForLte = ( }; const getColorStopWithMinMaxForGte = ( - rule: ValidMetricColorRule | ValidGaugeColorRule, + rule: ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule, baseColor?: string ): ColorStopsWithMinMax => { const colorStop = { @@ -224,12 +242,14 @@ const getCustomPalette = ( }; export const getPalette = ( - rules: MetricColorRules | GaugeColorRules, + rules: MetricColorRules | GaugeColorRules | SeriesColorRules, baseColor?: string ): PaletteOutput | null | undefined => { - const validRules = (rules as Array).filter< - ValidMetricColorRule | ValidGaugeColorRule - >((rule): rule is ValidMetricColorRule | ValidGaugeColorRule => isValidColorRule(rule)); + const validRules = (rules as Array).filter< + ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule + >((rule): rule is ValidMetricColorRule | ValidGaugeColorRule | ValidSeriesColorRule => + isValidColorRule(rule) + ); validRules.sort((rule1, rule2) => { return rule1.value! - rule2.value!; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.test.ts new file mode 100644 index 0000000000000..6deeaaa39fb8f --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { createSeries } from '../../__mocks__'; +import { getColumnState } from '.'; + +const mockGetPalette = jest.fn(); + +jest.mock('../palette', () => ({ + getPalette: jest.fn(() => mockGetPalette()), +})); + +describe('getColumnState', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetPalette.mockReturnValue({ id: 'custom' }); + }); + + test('should return column state without palette if series is not provided', () => { + const config = getColumnState('test'); + expect(config).toEqual({ + columnId: 'test', + alignment: 'left', + colorMode: 'none', + }); + expect(mockGetPalette).toBeCalledTimes(0); + }); + + test('should return column state with palette if series is provided', () => { + const config = getColumnState('test', undefined, createSeries()); + expect(config).toEqual({ + columnId: 'test', + alignment: 'left', + colorMode: 'text', + palette: { id: 'custom' }, + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); + + test('should return column state with collapseFn if collapseFn is provided', () => { + const config = getColumnState('test', 'max', createSeries()); + expect(config).toEqual({ + columnId: 'test', + alignment: 'left', + colorMode: 'text', + palette: { id: 'custom' }, + collapseFn: 'max', + }); + expect(mockGetPalette).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.ts new file mode 100644 index 0000000000000..7f152fa218842 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/table/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { Series } from '../../../../../common/types'; +import { getPalette } from '../palette'; + +export const getColumnState = (columnId: string, collapseFn?: string, series?: Series) => { + const palette = series ? getPalette(series.color_rules ?? []) : undefined; + return { + columnId, + alignment: 'left' as const, + colorMode: palette ? 'text' : 'none', + ...(palette ? { palette } : {}), + ...(collapseFn ? { collapseFn } : {}), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/column.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/column.ts index c06cc3e722279..2be4e09bf8898 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/column.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/column.ts @@ -32,7 +32,7 @@ interface ExtraColumnFields { const isSupportedFormat = (format: string) => ['bytes', 'number', 'percent'].includes(format); -export const getFormat = (series: Series): FormatParams => { +export const getFormat = (series: Pick): FormatParams => { let suffix; if (!series.formatter || series.formatter === 'default') { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/date_histogram.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/date_histogram.ts index f2173cf56b469..0887ad1168ddf 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/date_histogram.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/date_histogram.ts @@ -9,28 +9,36 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import uuid from 'uuid'; import { DateHistogramParams, DataType } from '@kbn/visualizations-plugin/common/convert_to_lens'; -import { DateHistogramColumn } from './types'; -import type { Panel, Series } from '../../../../common/types'; +import { DateHistogramColumn, DateHistogramSeries } from './types'; +import type { Panel } from '../../../../common/types'; const getInterval = (interval?: string) => { return interval && !interval?.includes('=') ? interval : 'auto'; }; -export const convertToDateHistogramParams = (model: Panel, series: Series): DateHistogramParams => { +export const convertToDateHistogramParams = ( + model: Panel | undefined, + series: DateHistogramSeries, + includeEmptyRows: boolean = true +): DateHistogramParams => { return { - interval: getInterval(series.override_index_pattern ? series.series_interval : model.interval), + interval: getInterval(series.override_index_pattern ? series.series_interval : model?.interval), dropPartials: series.override_index_pattern ? series.series_drop_last_bucket > 0 - : model.drop_last_bucket > 0, - includeEmptyRows: true, + : (model?.drop_last_bucket ?? 0) > 0, + includeEmptyRows, }; }; export const convertToDateHistogramColumn = ( - model: Panel, - series: Series, + model: Panel | undefined, + series: DateHistogramSeries, dataView: DataView, - { fieldName, isSplit }: { fieldName: string; isSplit: boolean } + { + fieldName, + isSplit, + includeEmptyRows = true, + }: { fieldName: string; isSplit: boolean; includeEmptyRows?: boolean } ): DateHistogramColumn | null => { const dateField = dataView.getFieldByName(fieldName); @@ -38,7 +46,7 @@ export const convertToDateHistogramColumn = ( return null; } - const params = convertToDateHistogramParams(model, series); + const params = convertToDateHistogramParams(model, series, includeEmptyRows); return { columnId: uuid(), diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/filters.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/filters.ts index 9134504813b68..05d74337e848d 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/filters.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/filters.ts @@ -8,10 +8,9 @@ import uuid from 'uuid'; import { FiltersParams } from '@kbn/visualizations-plugin/common/convert_to_lens'; -import { FiltersColumn } from './types'; -import type { Series } from '../../../../common/types'; +import { FiltersColumn, FiltersSeries } from './types'; -export const convertToFiltersParams = (series: Series): FiltersParams => { +export const convertToFiltersParams = (series: FiltersSeries): FiltersParams => { const splitFilters = []; if (series.split_mode === 'filter' && series.filter) { splitFilters.push({ filter: series.filter }); @@ -35,7 +34,10 @@ export const convertToFiltersParams = (series: Series): FiltersParams => { }; }; -export const convertToFiltersColumn = (series: Series, isSplit: boolean): FiltersColumn | null => { +export const convertToFiltersColumn = ( + series: FiltersSeries, + isSplit: boolean +): FiltersColumn | null => { const params = convertToFiltersParams(series); if (!params.filters.length) { return null; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/terms.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/terms.ts index 977de1947d4f8..c31d8dca68ced 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/terms.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/terms.ts @@ -9,16 +9,15 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import { DataType, TermsParams } from '@kbn/visualizations-plugin/common'; import uuid from 'uuid'; -import { Series } from '../../../../common/types'; import { excludeMetaFromColumn, getFormat, isColumnWithMeta } from './column'; -import { Column, TermsColumn } from './types'; +import { Column, TermsColumn, TermsSeries } from './types'; interface OrderByWithAgg { orderAgg?: TermsParams['orderAgg']; orderBy: TermsParams['orderBy']; } -const getOrderByWithAgg = (series: Series, columns: Column[]): OrderByWithAgg | null => { +const getOrderByWithAgg = (series: TermsSeries, columns: Column[]): OrderByWithAgg | null => { if (series.terms_order_by === '_key') { return { orderBy: { type: 'alphabetical' } }; } @@ -56,7 +55,7 @@ const getOrderByWithAgg = (series: Series, columns: Column[]): OrderByWithAgg | }; export const convertToTermsParams = ( - series: Series, + series: TermsSeries, columns: Column[], secondaryFields: string[] ): TermsParams | null => { @@ -84,10 +83,11 @@ export const convertToTermsParams = ( export const convertToTermsColumn = ( termFields: [string, ...string[]], - series: Series, + series: TermsSeries, columns: Column[], dataView: DataView, - isSplit: boolean = false + isSplit: boolean = false, + label?: string ): TermsColumn | null => { const [baseField, ...secondaryFields] = termFields; const field = dataView.getFieldByName(baseField); @@ -108,6 +108,7 @@ export const convertToTermsColumn = ( sourceField: field.name, isBucketed: true, isSplit, + label, params: { ...params, ...getFormat(series) }, }; }; 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 4b3b6c582f915..943550aee0066 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 @@ -117,4 +117,24 @@ export interface CommonColumnConverterArgs { dataView: DataView; } +export type TermsSeries = Pick< + Series, + | 'split_mode' + | 'terms_direction' + | 'terms_order_by' + | 'terms_size' + | 'terms_include' + | 'terms_exclude' + | 'terms_field' + | 'formatter' + | 'value_template' +>; + +export type FiltersSeries = Pick; + +export type DateHistogramSeries = Pick< + Series, + 'split_mode' | 'override_index_pattern' | 'series_interval' | 'series_drop_last_bucket' +>; + export { FiltersColumn, TermsColumn, DateHistogramColumn }; 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 debe064940c8e..a2130de36abd4 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 @@ -68,6 +68,7 @@ const supportedPanelTypes: readonly PANEL_TYPES[] = [ PANEL_TYPES.TOP_N, PANEL_TYPES.METRIC, PANEL_TYPES.GAUGE, + PANEL_TYPES.TABLE, ]; const supportedTimeRangeModes: readonly TIME_RANGE_DATA_MODES[] = [ diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/buckets_columns.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/buckets_columns.ts index c0aa201de6837..8ca184d443a68 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/buckets_columns.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/buckets_columns.ts @@ -7,18 +7,21 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import { Series, Panel } from '../../../../common/types'; +import { Panel } from '../../../../common/types'; import { getFieldsForTerms } from '../../../../common/fields_utils'; import { Column, convertToFiltersColumn, convertToDateHistogramColumn, convertToTermsColumn, + TermsSeries, + FiltersSeries, + DateHistogramSeries, } from '../convert'; import { getValidColumns } from './columns'; export const isSplitWithDateHistogram = ( - series: Series, + series: TermsSeries, splitFields: string[], dataView: DataView ) => { @@ -39,27 +42,49 @@ export const isSplitWithDateHistogram = ( return false; }; +const isFiltersSeries = ( + series: DateHistogramSeries | TermsSeries | FiltersSeries +): series is FiltersSeries => { + return series.split_mode === 'filters' || series.split_mode === 'filter'; +}; + +const isTermsSeries = ( + series: DateHistogramSeries | TermsSeries | FiltersSeries +): series is TermsSeries => { + return series.split_mode === 'terms'; +}; + +const isDateHistogramSeries = ( + series: DateHistogramSeries | TermsSeries | FiltersSeries, + isDateHistogram: boolean +): series is DateHistogramSeries => { + return isDateHistogram && series.split_mode === 'terms'; +}; + export const getBucketsColumns = ( - model: Panel, - series: Series, + model: Panel | undefined, + series: DateHistogramSeries | TermsSeries | FiltersSeries, columns: Column[], dataView: DataView, - isSplit: boolean = false + isSplit: boolean = false, + label?: string, + includeEmptyRowsForDateHistogram: boolean = true ) => { - if (series.split_mode === 'filters' || series.split_mode === 'filter') { + if (isFiltersSeries(series)) { const filterColumn = convertToFiltersColumn(series, true); return getValidColumns([filterColumn]); } - if (series.split_mode === 'terms') { + if (isTermsSeries(series)) { const splitFields = getFieldsForTerms(series.terms_field); const isDateHistogram = isSplitWithDateHistogram(series, splitFields, dataView); if (isDateHistogram === null) { return null; } - if (isDateHistogram) { + if (isDateHistogramSeries(series, isDateHistogram)) { const dateHistogramColumn = convertToDateHistogramColumn(model, series, dataView, { fieldName: splitFields[0], isSplit: true, + includeEmptyRows: includeEmptyRowsForDateHistogram, }); return getValidColumns(dateHistogramColumn); } @@ -73,7 +98,8 @@ export const getBucketsColumns = ( series, columns, dataView, - isSplit + isSplit, + label ); return getValidColumns(termsColumn); } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts index 9407599573d9d..6d62994be4447 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import { METRIC_TYPES } from '@kbn/data-plugin/public'; import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { convertToLens } from '.'; import { createPanel, createSeries } from '../lib/__mocks__'; +import { Panel } from '../../../common/types'; const mockGetMetricsColumns = jest.fn(); const mockGetBucketsColumns = jest.fn(); @@ -54,6 +56,10 @@ describe('convertToLens', () => { ], }); + const vis = { + params: model, + } as Vis; + const bucket = { isBucketed: true, isSplit: true, @@ -135,27 +141,27 @@ describe('convertToLens', () => { test('should return null for invalid metrics', async () => { mockIsValidMetrics.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockGetBucketsColumns).toBeCalledTimes(1); }); test('should return state for valid model', async () => { - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsMetric'); expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length); @@ -163,16 +169,16 @@ describe('convertToLens', () => { }); test('should skip hidden series', async () => { - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], hidden: true, }), ], - }) - ); + }), + } as Vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsMetric'); expect(mockIsValidMetrics).toBeCalledTimes(0); @@ -185,8 +191,8 @@ describe('convertToLens', () => { indexPattern: { id: 'test-index-pattern-1' }, }); - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], @@ -197,8 +203,8 @@ describe('convertToLens', () => { hidden: false, }), ], - }) - ); + }), + } as Vis); expect(result).toBeNull(); }); @@ -207,8 +213,8 @@ describe('convertToLens', () => { mockGetBucketsColumns.mockReturnValueOnce([]); mockGetMetricsColumns.mockReturnValueOnce([metric]); - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], @@ -219,8 +225,8 @@ describe('convertToLens', () => { hidden: false, }), ], - }) - ); + }), + } as Vis); expect(result).toBeNull(); }); @@ -229,8 +235,8 @@ describe('convertToLens', () => { mockGetBucketsColumns.mockReturnValueOnce([bucket2]); mockGetMetricsColumns.mockReturnValueOnce([metric]); - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], @@ -241,8 +247,8 @@ describe('convertToLens', () => { hidden: false, }), ], - }) - ); + }), + } as Vis); expect(result).toBeNull(); }); @@ -251,8 +257,8 @@ describe('convertToLens', () => { mockGetBucketsColumns.mockReturnValueOnce([bucket]); mockGetMetricsColumns.mockReturnValueOnce([metric]); - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], @@ -263,8 +269,8 @@ describe('convertToLens', () => { hidden: false, }), ], - }) - ); + }), + } as Vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsMetric'); expect(mockGetConfigurationForMetric).toBeCalledTimes(1); 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 149acc513b9ff..fbf04a2dcf779 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 @@ -22,7 +22,10 @@ import { excludeMetaFromLayers, getUniqueBuckets } from '../utils'; const MAX_SERIES = 2; const MAX_BUCKETS = 2; -export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { +export const convertToLens: ConvertTsvbToLensVisualization = async ( + { params: model }, + timeRange +) => { const dataViews = getDataViewsStart(); const seriesNum = model.series.filter((series) => !series.hidden).length; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.test.ts new file mode 100644 index 0000000000000..37676b302fba1 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.test.ts @@ -0,0 +1,235 @@ +/* + * 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 { TableVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { Vis } from '@kbn/visualizations-plugin/public'; +import { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { convertToLens } from '.'; +import { createPanel, createSeries } from '../lib/__mocks__'; +import { Panel } from '../../../common/types'; +import { TSVB_METRIC_TYPES } from '../../../common/enums'; + +const mockConvertToDateHistogramColumn = jest.fn(); +const mockGetMetricsColumns = jest.fn(); +const mockGetBucketsColumns = jest.fn(); +const mockGetConfigurationForTimeseries = jest.fn(); +const mockIsValidMetrics = jest.fn(); +const mockGetDatasourceValue = jest + .fn() + .mockImplementation(() => Promise.resolve(stubLogstashDataView)); +const mockGetDataSourceInfo = jest.fn(); +const mockGetColumnState = jest.fn(); + +jest.mock('../../services', () => ({ + getDataViewsStart: jest.fn(() => mockGetDatasourceValue), +})); + +jest.mock('../lib/convert', () => ({ + excludeMetaFromColumn: jest.fn().mockReturnValue({}), +})); + +jest.mock('../lib/series', () => ({ + getMetricsColumns: jest.fn(() => mockGetMetricsColumns()), + getBucketsColumns: jest.fn(() => mockGetBucketsColumns()), +})); + +jest.mock('../lib/configurations/table', () => ({ + getColumnState: jest.fn(() => mockGetColumnState()), +})); + +jest.mock('../lib/metrics', () => ({ + isValidMetrics: jest.fn(() => mockIsValidMetrics()), + getReducedTimeRange: jest.fn().mockReturnValue('10'), +})); + +jest.mock('../lib/datasource', () => ({ + getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()), +})); + +describe('convertToLens', () => { + const model = createPanel({ + series: [ + createSeries({ + metrics: [ + { id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }, + { id: 'some-id-1', type: METRIC_TYPES.COUNT }, + ], + }), + ], + }); + + const vis = { + params: model, + uiState: { + get: () => ({}), + }, + } as Vis; + + beforeEach(() => { + mockIsValidMetrics.mockReturnValue(true); + mockGetDataSourceInfo.mockReturnValue({ + indexPatternId: 'test-index-pattern', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern' }, + }); + mockConvertToDateHistogramColumn.mockReturnValue({}); + mockGetMetricsColumns.mockReturnValue([{}]); + mockGetBucketsColumns.mockReturnValue([{}]); + mockGetConfigurationForTimeseries.mockReturnValue({ layers: [] }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null for invalid metrics', async () => { + mockIsValidMetrics.mockReturnValue(null); + const result = await convertToLens(vis); + expect(result).toBeNull(); + expect(mockIsValidMetrics).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported metrics', async () => { + mockGetMetricsColumns.mockReturnValue(null); + const result = await convertToLens(vis); + expect(result).toBeNull(); + expect(mockGetMetricsColumns).toBeCalledTimes(1); + }); + + test('should return null if several series have different “Field” + “Aggregate function”', async () => { + const result = await convertToLens({ + params: createPanel({ + series: [createSeries({ aggregate_by: 'new' }), createSeries({ aggregate_by: 'test' })], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeNull(); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + + test('should return null if “Aggregate function” is not supported', async () => { + const result = await convertToLens({ + params: createPanel({ + series: [createSeries({ aggregate_by: 'new', aggregate_function: 'cumulative_sum' })], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeNull(); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + + test('should return null if model have not visible metrics', async () => { + const result = await convertToLens({ + params: createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: true, + }), + ], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeNull(); + }); + + test('should return null if only static value is visible metric', async () => { + mockGetMetricsColumns.mockReturnValue([ + { columnId: 'metric-column-1', operationType: 'static_value' }, + ]); + const result = await convertToLens({ + params: createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: TSVB_METRIC_TYPES.STATIC }], + hidden: true, + }), + ], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeNull(); + }); + + test('should return state for valid model', async () => { + const result = await convertToLens(vis); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsDatatable'); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + // every series + group by + expect(mockGetColumnState).toBeCalledTimes(model.series.length + 1); + }); + + test('should return state for valid model with “Field” + “Aggregate function”', async () => { + const result = await convertToLens({ + params: createPanel({ + series: [createSeries({ aggregate_by: 'new', aggregate_function: 'sum' })], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsDatatable'); + expect(mockGetBucketsColumns).toBeCalledTimes(2); + // every series + group by + (“Field” + “Aggregate function”) + expect(mockGetColumnState).toBeCalledTimes(model.series.length + 2); + }); + + test('should return correct sorting config', async () => { + mockGetMetricsColumns.mockReturnValue([{ columnId: 'metric-column-1' }]); + const result = await convertToLens({ + params: createPanel({ + series: [createSeries({ id: 'test' })], + }), + uiState: { + get: () => ({ sort: { order: 'decs', column: 'test' } }), + }, + } as Vis); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsDatatable'); + expect((result?.configuration as TableVisConfiguration).sorting).toEqual({ + direction: 'decs', + columnId: 'metric-column-1', + }); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + // every series + group by + expect(mockGetColumnState).toBeCalledTimes(model.series.length + 1); + }); + + test('should skip hidden series', async () => { + const result = await convertToLens({ + params: createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: true, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + }), + ], + }), + uiState: { + get: () => ({}), + }, + } as Vis); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsDatatable'); + expect(mockIsValidMetrics).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.ts new file mode 100644 index 0000000000000..0219d1080724b --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/table/index.ts @@ -0,0 +1,183 @@ +/* + * 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 { getIndexPatternIds, Layer } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { PANEL_TYPES } from '../../../common/enums'; +import { getDataViewsStart } from '../../services'; +import { getColumnState } from '../lib/configurations/table'; +import { getDataSourceInfo } from '../lib/datasource'; +import { getMetricsColumns, getBucketsColumns } from '../lib/series'; +import { getReducedTimeRange, isValidMetrics } from '../lib/metrics'; +import { ConvertTsvbToLensVisualization } from '../types'; +import { Layer as ExtendedLayer, excludeMetaFromColumn, Column } from '../lib/convert'; + +const excludeMetaFromLayers = (layers: Record): Record => { + const newLayers: Record = {}; + Object.entries(layers).forEach(([layerId, layer]) => { + const columns = layer.columns.map(excludeMetaFromColumn); + newLayers[layerId] = { ...layer, columns }; + }); + + return newLayers; +}; + +export const convertToLens: ConvertTsvbToLensVisualization = async ( + { params: model, uiState }, + timeRange +) => { + const columnStates = []; + const dataViews = getDataViewsStart(); + const seriesNum = model.series.filter((series) => !series.hidden).length; + const sortConfig = uiState.get('table')?.sort ?? {}; + + const datasourceInfo = await getDataSourceInfo( + model.index_pattern, + model.time_field, + false, + undefined, + undefined, + dataViews + ); + + if (!datasourceInfo) { + return null; + } + + const { indexPatternId, indexPattern } = datasourceInfo; + + const commonBucketsColumns = getBucketsColumns( + undefined, + { + split_mode: 'terms', + terms_field: model.pivot_id, + terms_size: model.pivot_rows ? model.pivot_rows.toString() : undefined, + }, + [], + indexPattern!, + false, + model.pivot_label, + false + ); + + if (!commonBucketsColumns) { + return null; + } + + const sortConfiguration = { + columnId: commonBucketsColumns[0].columnId, + direction: sortConfig.order, + }; + + columnStates.push(getColumnState(commonBucketsColumns[0].columnId)); + + let bucketsColumns: Column[] | null = []; + + if ( + !model.series.every( + (s) => + ((!s.aggregate_by && !model.series[0].aggregate_by) || + s.aggregate_by === model.series[0].aggregate_by) && + ((!s.aggregate_function && !model.series[0].aggregate_function) || + s.aggregate_function === model.series[0].aggregate_function) + ) + ) { + return null; + } + + if (model.series[0].aggregate_by) { + if ( + !model.series[0].aggregate_function || + !['sum', 'mean', 'min', 'max'].includes(model.series[0].aggregate_function) + ) { + return null; + } + bucketsColumns = getBucketsColumns( + undefined, + { + split_mode: 'terms', + terms_field: model.series[0].aggregate_by, + }, + [], + indexPattern!, + false + ); + if (bucketsColumns === null) { + return null; + } + + columnStates.push( + getColumnState( + bucketsColumns[0].columnId, + model.series[0].aggregate_function === 'mean' ? 'avg' : model.series[0].aggregate_function + ) + ); + } + + const metrics = []; + + // handle multiple layers/series + for (const [_, series] of model.series.entries()) { + if (series.hidden) { + continue; + } + + // not valid time shift + if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') { + return null; + } + + if (!isValidMetrics(series.metrics, PANEL_TYPES.TABLE, series.time_range_mode)) { + return null; + } + + const reducedTimeRange = getReducedTimeRange(model, series, timeRange); + + // handle multiple metrics + const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, { + reducedTimeRange, + }); + if (!metricsColumns) { + return null; + } + + columnStates.push(getColumnState(metricsColumns[0].columnId, undefined, series)); + + if (sortConfig.column === series.id) { + sortConfiguration.columnId = metricsColumns[0].columnId; + } + + metrics.push(...metricsColumns); + } + + if (!metrics.length || metrics.every((metric) => metric.operationType === 'static_value')) { + return null; + } + + const extendedLayer: ExtendedLayer = { + indexPatternId: indexPatternId as string, + layerId: uuid(), + columns: [...metrics, ...commonBucketsColumns, ...bucketsColumns], + columnOrder: [], + }; + + const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer })); + + return { + type: 'lnsDatatable', + layers, + configuration: { + columns: columnStates, + layerId: extendedLayer.layerId, + layerType: 'data', + sorting: sortConfiguration, + }, + indexPatternIds: getIndexPatternIds(layers), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts index c81db38e05384..64fee3484b3b6 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import { METRIC_TYPES } from '@kbn/data-plugin/public'; import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { convertToLens } from '.'; import { createPanel, createSeries } from '../lib/__mocks__'; +import { Panel } from '../../../common/types'; const mockConvertToDateHistogramColumn = jest.fn(); const mockGetMetricsColumns = jest.fn(); @@ -60,6 +62,10 @@ describe('convertToLens', () => { ], }); + const vis = { + params: model, + } as Vis; + beforeEach(() => { mockIsValidMetrics.mockReturnValue(true); mockGetDataSourceInfo.mockReturnValue({ @@ -79,35 +85,35 @@ describe('convertToLens', () => { test('should return null for invalid metrics', async () => { mockIsValidMetrics.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockIsValidMetrics).toBeCalledTimes(1); }); test('should return null for empty time field', async () => { mockGetDataSourceInfo.mockReturnValue({ timeField: null }); - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockGetDataSourceInfo).toBeCalledTimes(1); }); test('should return null for invalid date histogram', async () => { mockConvertToDateHistogramColumn.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockConvertToDateHistogramColumn).toBeCalledTimes(1); }); test('should return null for invalid or unsupported metrics', async () => { mockGetMetricsColumns.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockGetBucketsColumns).toBeCalledTimes(1); }); @@ -119,14 +125,14 @@ describe('convertToLens', () => { operationType: 'static_value', }, ]); - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockGetMetricsColumns).toBeCalledTimes(1); expect(mockGetBucketsColumns).toBeCalledTimes(1); }); test('should return state for valid model', async () => { - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsXY'); expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length); @@ -134,16 +140,16 @@ describe('convertToLens', () => { }); test('should skip hidden series', async () => { - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], hidden: true, }), ], - }) - ); + }), + } as Vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsXY'); expect(mockIsValidMetrics).toBeCalledTimes(0); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts index ef678fcc2dab4..a08b7113c4a7b 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts @@ -14,7 +14,6 @@ import { } from '@kbn/visualizations-plugin/common/convert_to_lens'; import uuid from 'uuid'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { Panel } from '../../../common/types'; import { PANEL_TYPES } from '../../../common/enums'; import { getDataViewsStart } from '../../services'; import { getDataSourceInfo } from '../lib/datasource'; @@ -41,7 +40,7 @@ const excludeMetaFromLayers = (layers: Record): Record { +export const convertToLens: ConvertTsvbToLensVisualization = async ({ params: model }) => { const dataViews: DataViewsPublicPluginStart = getDataViewsStart(); const extendedLayers: Record = {}; const seriesNum = model.series.filter((series) => !series.hidden).length; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts index 7e4776f10ac9f..646323a6691d5 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import { METRIC_TYPES } from '@kbn/data-plugin/public'; import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { convertToLens } from '.'; import { createPanel, createSeries } from '../lib/__mocks__'; +import { Panel } from '../../../common/types'; const mockGetMetricsColumns = jest.fn(); const mockGetBucketsColumns = jest.fn(); @@ -59,6 +61,10 @@ describe('convertToLens', () => { ], }); + const vis = { + params: model, + } as Vis; + beforeEach(() => { mockIsValidMetrics.mockReturnValue(true); mockGetDataSourceInfo.mockReturnValue({ @@ -77,27 +83,27 @@ describe('convertToLens', () => { test('should return null for invalid metrics', async () => { mockIsValidMetrics.mockReturnValue(null); - const result = await convertToLens(model); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); 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); + const result = await convertToLens(vis); expect(result).toBeNull(); expect(mockGetBucketsColumns).toBeCalledTimes(1); }); test('should return state for valid model', async () => { - const result = await convertToLens(model); + const result = await convertToLens(vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsXY'); expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length); @@ -105,16 +111,16 @@ describe('convertToLens', () => { }); test('should skip hidden series', async () => { - const result = await convertToLens( - createPanel({ + const result = await convertToLens({ + params: createPanel({ series: [ createSeries({ metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], hidden: true, }), ], - }) - ); + }), + } as Vis); expect(result).toBeDefined(); expect(result?.type).toBe('lnsXY'); expect(mockIsValidMetrics).toBeCalledTimes(0); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts index 130646f72f127..9505b7f5f0c7e 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts @@ -28,7 +28,10 @@ const excludeMetaFromLayers = (layers: Record): Record { +export const convertToLens: ConvertTsvbToLensVisualization = async ( + { params: model }, + timeRange +) => { const dataViews = getDataViewsStart(); const extendedLayers: Record = {}; const seriesNum = model.series.filter((series) => !series.hidden).length; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts index 69a90c7864eb3..9f00a669ea5c3 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts @@ -6,18 +6,22 @@ * Side Public License, v 1. */ +import { Vis } from '@kbn/visualizations-plugin/public'; import { MetricVisConfiguration, NavigateToLensContext, XYConfiguration, + TableVisConfiguration, } from '@kbn/visualizations-plugin/common'; import { TimeRange } from '@kbn/data-plugin/common'; import type { Panel } from '../../common/types'; export type ConvertTsvbToLensVisualization = ( - model: Panel, + vis: Vis, timeRange?: TimeRange -) => Promise | null>; +) => Promise | null>; export interface Filter { kql?: string | { [key: string]: any } | undefined; diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 3bf9f9b90bf1a..43be3ee3004f4 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -171,15 +171,13 @@ export const metricsVisDefinition: VisTypeDefinition< return { canNavigateToLens: Boolean( vis?.params - ? await convertTSVBtoLensConfiguration(vis.params as Panel, timeFilter?.getAbsoluteTime()) + ? await convertTSVBtoLensConfiguration(vis, timeFilter?.getAbsoluteTime()) : null ), }; }, navigateToLens: async (vis, timeFilter) => - vis?.params - ? await convertTSVBtoLensConfiguration(vis?.params as Panel, timeFilter?.getAbsoluteTime()) - : null, + vis?.params ? await convertTSVBtoLensConfiguration(vis, timeFilter?.getAbsoluteTime()) : null, inspectorAdapters: () => ({ requests: new RequestAdapter(), diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 4f7a5ad715215..f62f61f0c50ab 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -183,6 +183,7 @@ export interface ColumnState { summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; alignment?: 'left' | 'right' | 'center'; collapseFn?: CollapseFunction; + palette?: PaletteOutput; } export interface TableVisConfiguration { diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index e7512c6dd6473..0111c9026397d 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -114,6 +114,7 @@ const TopNav = ({ vis.type, vis.params, uiStateJSON?.vis, + uiStateJSON?.table, vis.data.indexPattern, ]); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index edbd53e80d8c5..1aacde4127f37 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -659,6 +659,28 @@ export class VisualBuilderPageObject extends FtrService { await this.comboBox.setElement(fieldEl, field); } + public async setFieldForAggregateBy(field: string): Promise { + const aggregateBy = await this.testSubjects.find('tsvbAggregateBySelect'); + + await this.retry.try(async () => { + await this.comboBox.setElement(aggregateBy, field); + if (!(await this.comboBox.isOptionSelected(aggregateBy, field))) { + throw new Error(`aggregate by field - ${field} is not selected`); + } + }); + } + + public async setFunctionForAggregateFunction(func: string): Promise { + const aggregateFunction = await this.testSubjects.find('tsvbAggregateFunctionCombobox'); + + await this.retry.try(async () => { + await this.comboBox.setElement(aggregateFunction, func); + if (!(await this.comboBox.isOptionSelected(aggregateFunction, func))) { + throw new Error(`aggregate function - ${func} is not selected`); + } + }); + } + public async checkFieldForAggregationValidity(aggNth: number = 0): Promise { const fieldEl = await this.getFieldForAggregation(aggNth); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/index.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/index.ts index c0b5197983aa4..8428d145c60ef 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/index.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./timeseries')); loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./top_n')); + loadTestFile(require.resolve('./table')); }); } diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/table.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/table.ts new file mode 100644 index 0000000000000..e3d52852cd61b --- /dev/null +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/table.ts @@ -0,0 +1,218 @@ +/* + * 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. + */ + +/* + * 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, header } = getPageObjects([ + 'visualBuilder', + 'visualize', + 'header', + 'lens', + ]); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('Table', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); + + beforeEach(async () => { + await visualBuilder.resetPage(); + await visualBuilder.clickTable(); + await header.waitUntilLoadingHasFinished(); + await visualBuilder.checkTableTabIsPresent(); + await visualBuilder.selectGroupByField('machine.os.raw'); + }); + + it('should not allow converting of not valid panel', async () => { + await visualBuilder.selectAggType('Max'); + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should not allow converting of unsupported aggregations', async () => { + await visualBuilder.selectAggType('Sum of Squares'); + await visualBuilder.setFieldForAggregation('machine.ram'); + + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should not allow converting sibling pipeline aggregations', async () => { + await visualBuilder.createNewAgg(); + + await visualBuilder.selectAggType('Overall Average', 1); + await visualBuilder.setFieldForAggregation('Count', 1); + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should not allow converting parent pipeline aggregations', async () => { + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.clickDataTab('table'); + await visualBuilder.createNewAgg(); + + await visualBuilder.selectAggType('Cumulative Sum', 1); + await visualBuilder.setFieldForAggregation('Count', 1); + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should not allow converting not valid aggregation function', async () => { + await visualBuilder.clickSeriesOption(); + await visualBuilder.setFieldForAggregateBy('clientip'); + await visualBuilder.setFunctionForAggregateFunction('Cumulative Sum'); + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should not allow converting series with different aggregation fucntion or aggregation by', async () => { + await visualBuilder.createNewAggSeries(); + await visualBuilder.selectAggType('Static Value', 1); + await visualBuilder.setStaticValue(10); + await visualBuilder.clickSeriesOption(); + await visualBuilder.setFieldForAggregateBy('bytes'); + await visualBuilder.setFunctionForAggregateFunction('Sum'); + await visualBuilder.clickSeriesOption(1); + await visualBuilder.setFieldForAggregateBy('bytes'); + await visualBuilder.setFunctionForAggregateFunction('Min'); + await header.waitUntilLoadingHasFinished(); + expect(await visualize.hasNavigateToLensButton()).to.be(false); + }); + + it('should allow converting a count aggregation', async () => { + expect(await visualize.hasNavigateToLensButton()).to.be(true); + }); + + it('should convert last value mode to reduced time range', async () => { + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.setIntervalValue('1m'); + await visualBuilder.clickDataTab('table'); + await header.waitUntilLoadingHasFinished(); + + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('lnsDataTable'); + await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); + await testSubjects.click('indexPattern-advanced-accordion'); + const reducedTimeRange = await testSubjects.find('indexPattern-dimension-reducedTimeRange'); + expect(await reducedTimeRange.getVisibleText()).to.be('1 minute (1m)'); + await retry.try(async () => { + const layerCount = await lens.getLayerCount(); + expect(layerCount).to.be(1); + const metricDimensionText = await lens.getDimensionTriggerText('lnsDatatable_metrics', 0); + expect(metricDimensionText).to.be('Count of records last 1m'); + }); + }); + + it('should convert static value to the metric dimension', async () => { + await visualBuilder.createNewAggSeries(); + await visualBuilder.selectAggType('Static Value', 1); + await visualBuilder.setStaticValue(10); + + await header.waitUntilLoadingHasFinished(); + + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('lnsDataTable'); + await retry.try(async () => { + const layerCount = await lens.getLayerCount(); + expect(layerCount).to.be(1); + const metricDimensionText1 = await lens.getDimensionTriggerText('lnsDatatable_metrics', 0); + const metricDimensionText2 = await lens.getDimensionTriggerText('lnsDatatable_metrics', 1); + expect(metricDimensionText1).to.be('Count of records'); + expect(metricDimensionText2).to.be('10'); + }); + }); + + it('should convert aggregate by to split row dimension', async () => { + await visualBuilder.clickSeriesOption(); + await visualBuilder.setFieldForAggregateBy('clientip'); + await visualBuilder.setFunctionForAggregateFunction('Sum'); + await header.waitUntilLoadingHasFinished(); + + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('lnsDataTable'); + await retry.try(async () => { + const layerCount = await lens.getLayerCount(); + expect(layerCount).to.be(1); + const splitRowsText1 = await lens.getDimensionTriggerText('lnsDatatable_rows', 0); + const splitRowsText2 = await lens.getDimensionTriggerText('lnsDatatable_rows', 1); + expect(splitRowsText1).to.be('Top 10 values of machine.os.raw'); + expect(splitRowsText2).to.be('Top 10 values of clientip'); + }); + + await lens.openDimensionEditor('lnsDatatable_rows > lns-dimensionTrigger', 0, 1); + const collapseBy = await testSubjects.find('indexPattern-collapse-by'); + expect(await collapseBy.getAttribute('value')).to.be('sum'); + }); + + it('should convert group by field with custom label', async () => { + await visualBuilder.setColumnLabelValue('test'); + await header.waitUntilLoadingHasFinished(); + + await visualize.navigateToLensFromAnotherVisulization(); + await lens.waitForVisualization('lnsDataTable'); + await retry.try(async () => { + const layerCount = await lens.getLayerCount(); + expect(layerCount).to.be(1); + const splitRowsText = await lens.getDimensionTriggerText('lnsDatatable_rows', 0); + expect(splitRowsText).to.be('test'); + }); + }); + + it('should convert color ranges', async () => { + await visualBuilder.clickSeriesOption(); + + await visualBuilder.setColorRuleOperator('>= greater than or equal'); + await visualBuilder.setColorRuleValue(10); + await visualBuilder.setColorPickerValue('#54B399'); + + await visualBuilder.createColorRule(1); + + await visualBuilder.setColorRuleOperator('>= greater than or equal'); + await visualBuilder.setColorRuleValue(100, 1); + await visualBuilder.setColorPickerValue('#54A000', 1); + + await header.waitUntilLoadingHasFinished(); + await visualize.navigateToLensFromAnotherVisulization(); + + await lens.waitForVisualization('lnsDataTable'); + await retry.try(async () => { + const closePalettePanels = await testSubjects.findAll( + 'lns-indexPattern-PalettePanelContainerBack' + ); + if (closePalettePanels.length) { + await lens.closePalettePanel(); + await lens.closeDimensionEditor(); + } + + await lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); + + await lens.openPalettePanel('lnsDatatable'); + const colorStops = await lens.getPaletteColorStops(); + + expect(colorStops).to.eql([ + { stop: '10', color: 'rgba(84, 179, 153, 1)' }, + { stop: '100', color: 'rgba(84, 160, 0, 1)' }, + { stop: '', color: undefined }, + ]); + }); + }); + }); +}