diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6e6172c014371..07d813b5f6dc8 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -140,6 +140,6 @@ pageLoadAssetSize: visTypeTimeseries: 55203 visTypeVega: 153573 visTypeVislib: 242838 - visTypeXy: 30000 + visTypeXy: 31800 visualizations: 90000 watcher: 43598 diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.test.ts index 4664902d13876..3976f6977e2be 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.test.ts @@ -28,6 +28,7 @@ describe('layeredXyVis', () => { args: { ...rest, layers: [sampleExtendedLayer] }, syncColors: false, syncTooltips: false, + canNavigateToLens: false, }, }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts index 8dcc58cda01a8..d6553ef3dd7b3 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -61,6 +61,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, }, + canNavigateToLens: Boolean(handlers.variables.canNavigateToLens), syncColors: handlers?.isSyncColorsEnabled?.() ?? false, syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 67c7ab8d1e294..a09c5f05adb08 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -38,6 +38,7 @@ describe('xyVis', () => { }, ], }, + canNavigateToLens: false, syncColors: false, syncTooltips: false, }, @@ -346,6 +347,7 @@ describe('xyVis', () => { }, ], }, + canNavigateToLens: false, syncColors: false, syncTooltips: false, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index e29f1e5ffff3c..849f2030a4697 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -136,6 +136,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, }, + canNavigateToLens: Boolean(handlers.variables.canNavigateToLens), syncColors: handlers?.isSyncColorsEnabled?.() ?? false, syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index de387b4113373..94567f563cdb1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -18,6 +18,7 @@ export interface XYChartProps { args: XYProps; syncTooltips: boolean; syncColors: boolean; + canNavigateToLens?: boolean; } export interface XYRender { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 45eca06c670b0..0d88480b00342 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -110,7 +110,7 @@ declare global { } } -export type XYChartRenderProps = XYChartProps & { +export type XYChartRenderProps = Omit & { chartsThemeService: ChartsPluginSetup['theme']; chartsActiveCursorService: ChartsPluginStart['activeCursor']; data: DataPublicPluginStart; diff --git a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx index 5606bad9050c7..e00f5b04bd590 100644 --- a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -52,6 +52,7 @@ interface XyChartRendererDeps { const extractCounterEvents = ( originatingApp: string, { layers, yAxisConfigs }: XYChartProps['args'], + canNavigateToLens: boolean, services: { getDataLayers: typeof getDataLayers; } @@ -149,6 +150,7 @@ const extractCounterEvents = ( (aggregateLayers.length === 1 && dataLayer.splitAccessors?.length) ? 'aggregate_bucket' : undefined, + canNavigateToLens ? `render_${byTypes.mixedXY ? 'mixed_xy' : type}_convertable` : undefined, ] .filter(Boolean) .map((item) => `render_${originatingApp}_${item}`); @@ -188,9 +190,14 @@ export const getXyChartRenderer = ({ const visualizationType = extractVisualizationType(executionContext); if (deps.usageCollection && containerType && visualizationType) { - const uiEvents = extractCounterEvents(visualizationType, config.args, { - getDataLayers, - }); + const uiEvents = extractCounterEvents( + visualizationType, + config.args, + Boolean(config.canNavigateToLens), + { + getDataLayers, + } + ); if (uiEvents) { deps.usageCollection.reportUiCounter(containerType, METRIC_TYPE.COUNT, uiEvents); diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts index 7c04f19400272..6f472ca29af46 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.test.ts @@ -60,7 +60,7 @@ describe('getConfiguration', () => { const metricAccessor = 'metric-id'; const breakdownByAccessor = 'bucket-id'; const metrics = [metricAccessor]; - const buckets = [breakdownByAccessor]; + const buckets = { all: [breakdownByAccessor], customBuckets: {} }; const maxAccessor = 'max-accessor-id'; const collapseFn = 'sum'; expect( @@ -69,7 +69,7 @@ describe('getConfiguration', () => { buckets, maxAccessor, columnsWithoutReferenced: [], - bucketCollapseFn: { [metricAccessor]: collapseFn }, + bucketCollapseFn: { [collapseFn]: [breakdownByAccessor] }, }) ).toEqual({ breakdownByAccessor, diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts index 91e58333e8d56..ec56280fa25cc 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/configurations/goal.ts @@ -22,14 +22,22 @@ export const getConfiguration = ( bucketCollapseFn, }: { metrics: string[]; - buckets: string[]; + buckets: { + all: string[]; + customBuckets: Record; + }; maxAccessor: string; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): MetricVisConfiguration => { const [metricAccessor] = metrics; - const [breakdownByAccessor] = buckets; + const [breakdownByAccessor] = buckets.all; + const collapseFn = bucketCollapseFn + ? Object.keys(bucketCollapseFn).find((key) => + bucketCollapseFn[key].includes(breakdownByAccessor) + ) + : undefined; return { layerId, layerType: 'data', @@ -37,7 +45,7 @@ export const getConfiguration = ( metricAccessor, breakdownByAccessor, maxAccessor, - collapseFn: Object.values(bucketCollapseFn ?? {})[0], + collapseFn, subtitle: gauge.labels.show && gauge.style.subText ? gauge.style.subText : undefined, }; }; diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.test.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.test.ts index f7c07dafd85c6..0da750bfdcef0 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.test.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.test.ts @@ -104,22 +104,26 @@ describe('convertToLens', () => { }); test('should return null if metrics count is more than 1', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1', '2'], - buckets: [], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1', '2'], + buckets: { all: [] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if metric column data type is different from number', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: [], - columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: [] }, + columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); @@ -129,15 +133,17 @@ describe('convertToLens', () => { layerType: 'data', }; - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: [], - columns: [{ columnId: '1', dataType: 'number' }], - columnsWithoutReferenced: [ - { columnId: '1', meta: { aggId: 'agg-1' } }, - { columnId: '2', meta: { aggId: 'agg-2' } }, - ], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: [] }, + columns: [{ columnId: '1', dataType: 'number' }], + columnsWithoutReferenced: [ + { columnId: '1', meta: { aggId: 'agg-1' } }, + { columnId: '2', meta: { aggId: 'agg-2' } }, + ], + }, + ]); mockGetConfiguration.mockReturnValue(config); const result = await convertToLens(vis, timefilter); diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.ts index be664d3500a7c..080d5e84561a9 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/gauge.ts @@ -53,7 +53,7 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim const percentageModeConfig = getPercentageModeConfig(vis.params.gauge, false); - const result = getColumnsFromVis( + const layers = getColumnsFromVis( vis, timefilter, dataView, @@ -63,17 +63,19 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim { dropEmptyRowsInDateHistogram: true, ...percentageModeConfig } ); - if (result === null) { + if (layers === null) { return null; } + const [layerConfig] = layers; + // for now, multiple metrics are not supported - if (result.metrics.length > 1 || result.buckets.length) { + if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length) { return null; } - if (result.metrics[0]) { - const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]); + if (layerConfig.metrics[0]) { + const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]); if (metric?.dataType !== 'number') { return null; } @@ -82,11 +84,11 @@ export const convertToLens: ConvertGaugeVisToLensVisualization = async (vis, tim const layerId = uuid(); const indexPatternId = dataView.id!; - const metricAccessor = result.metrics[0]; + const metricAccessor = layerConfig.metrics[0]; const { min, max, isPercentageMode } = percentageModeConfig as PercentageModeConfigWithMinMax; const minColumn = createStaticValueColumn(isPercentageMode ? 0 : min); const maxColumn = createStaticValueColumn(isPercentageMode ? 1 : max); - const columns = [...result.columns, minColumn, maxColumn]; + const columns = [...layerConfig.columns, minColumn, maxColumn]; return { type: 'lnsGauge', diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/goal.test.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/goal.test.ts index 4d9247293e7a8..88566694f55bf 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/goal.test.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/goal.test.ts @@ -104,31 +104,37 @@ describe('convertToLens', () => { }); test('should return null if metrics count is more than 1', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1', '2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1', '2'], + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if buckets count is more than 1', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: [], - buckets: ['1', '2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: [], + buckets: { all: ['1', '2'] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if metric column data type is different from number', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); @@ -139,15 +145,17 @@ describe('convertToLens', () => { metricAccessor: '1', }; - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }], - columnsWithoutReferenced: [ - { columnId: '1', meta: { aggId: 'agg-1' } }, - { columnId: '2', meta: { aggId: 'agg-2' } }, - ], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }], + columnsWithoutReferenced: [ + { columnId: '1', meta: { aggId: 'agg-1' } }, + { columnId: '2', meta: { aggId: 'agg-2' } }, + ], + }, + ]); mockGetConfiguration.mockReturnValue(config); const result = await convertToLens(vis, timefilter); diff --git a/src/plugins/vis_types/gauge/public/convert_to_lens/goal.ts b/src/plugins/vis_types/gauge/public/convert_to_lens/goal.ts index a57dfedb02581..624ce45b3e848 100644 --- a/src/plugins/vis_types/gauge/public/convert_to_lens/goal.ts +++ b/src/plugins/vis_types/gauge/public/convert_to_lens/goal.ts @@ -53,7 +53,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time const percentageModeConfig = getPercentageModeConfig(vis.params.gauge, false); - const result = getColumnsFromVis( + const layers = getColumnsFromVis( vis, timefilter, dataView, @@ -63,17 +63,19 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time { dropEmptyRowsInDateHistogram: true, ...percentageModeConfig } ); - if (result === null) { + if (layers === null) { return null; } + const [layerConfig] = layers; + // for now, multiple metrics are not supported - if (result.metrics.length > 1 || result.buckets.length > 1) { + if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length > 1) { return null; } - if (result.metrics[0]) { - const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]); + if (layerConfig.metrics[0]) { + const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]); if (metric?.dataType !== 'number') { return null; } @@ -81,7 +83,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time const { isPercentageMode, max } = percentageModeConfig as PercentageModeConfigWithMinMax; const maxColumn = createStaticValueColumn(isPercentageMode ? 1 : max); - const columns = [...result.columns, maxColumn]; + const columns = [...layerConfig.columns, maxColumn]; const layerId = uuid(); const indexPatternId = dataView.id!; @@ -100,7 +102,7 @@ export const convertToLens: ConvertGoalVisToLensVisualization = async (vis, time vis.params, getPalette(vis.params.gauge, percentageModeConfig, true), { - ...result, + ...layerConfig, maxAccessor: maxColumn.columnId, } ), diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts index 29669c0286529..5ccfb169c91ae 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.test.ts @@ -48,9 +48,9 @@ describe('getConfiguration', () => { expect( getConfiguration(layerId, params, palette, { metrics: [metric], - buckets: [bucket], + buckets: { all: [bucket], customBuckets: { metric: bucket } }, columnsWithoutReferenced: [], - bucketCollapseFn: { [metric]: collapseFn }, + bucketCollapseFn: { [collapseFn]: [bucket] }, }) ).toEqual({ breakdownByAccessor: bucket, diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts index ae62b82408eeb..7b1b42a0211f5 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/configurations/index.ts @@ -21,19 +21,27 @@ export const getConfiguration = ( bucketCollapseFn, }: { metrics: string[]; - buckets: string[]; + buckets: { + all: string[]; + customBuckets: Record; + }; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): MetricVisConfiguration => { const [metricAccessor] = metrics; - const [breakdownByAccessor] = buckets; + const [breakdownByAccessor] = buckets.all; + const collapseFn = bucketCollapseFn + ? Object.keys(bucketCollapseFn).find((key) => + bucketCollapseFn[key].includes(breakdownByAccessor) + ) + : undefined; return { layerId, layerType: 'data', palette: params.metric.metricColorMode !== 'None' ? palette : undefined, metricAccessor, breakdownByAccessor, - collapseFn: Object.values(bucketCollapseFn ?? {})[0], + collapseFn, }; }; diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/metric/public/convert_to_lens/index.test.ts index e0c50cad16719..b3ad4ccf6be59 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/index.test.ts @@ -83,31 +83,37 @@ describe('convertToLens', () => { }); test('should return null if metrics count is more than 1', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1', '2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1', '2'], + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if buckets count is more than 1', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: [], - buckets: ['1', '2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: [], + buckets: { all: ['1', '2'] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if metric column data type is different from number', async () => { - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1', dataType: 'string' }], + }, + ]); const result = await convertToLens(vis, timefilter); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); @@ -118,15 +124,17 @@ describe('convertToLens', () => { metricAccessor: '1', }; - mockGetColumnsFromVis.mockReturnValue({ - metrics: ['1'], - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }], - columnsWithoutReferenced: [ - { columnId: '1', meta: { aggId: 'agg-1' } }, - { columnId: '2', meta: { aggId: 'agg-2' } }, - ], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + metrics: ['1'], + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1', dataType: 'number' }], + columnsWithoutReferenced: [ + { columnId: '1', meta: { aggId: 'agg-1' } }, + { columnId: '2', meta: { aggId: 'agg-2' } }, + ], + }, + ]); mockGetConfiguration.mockReturnValue(config); const result = await convertToLens(vis, timefilter); diff --git a/src/plugins/vis_types/metric/public/convert_to_lens/index.ts b/src/plugins/vis_types/metric/public/convert_to_lens/index.ts index 55f9e500b0a29..5b9cb985a2799 100644 --- a/src/plugins/vis_types/metric/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/metric/public/convert_to_lens/index.ts @@ -46,7 +46,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti await Promise.all([convertToLensModule, import('./configurations')]); const percentageModeConfig = getPercentageModeConfig(vis.params.metric); - const result = getColumnsFromVis( + const layers = getColumnsFromVis( vis, timefilter, dataView, @@ -56,17 +56,19 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti { dropEmptyRowsInDateHistogram: true, ...percentageModeConfig } ); - if (result === null) { + if (layers === null) { return null; } + const [layerConfig] = layers; + // for now, multiple metrics are not supported - if (result.metrics.length > 1 || result.buckets.length > 1) { + if (layerConfig.metrics.length > 1 || layerConfig.buckets.all.length > 1) { return null; } - if (result.metrics[0]) { - const metric = result.columns.find(({ columnId }) => columnId === result.metrics[0]); + if (layerConfig.metrics[0]) { + const metric = layerConfig.columns.find(({ columnId }) => columnId === layerConfig.metrics[0]); if (metric?.dataType !== 'number') { return null; } @@ -81,7 +83,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti { indexPatternId, layerId, - columns: result.columns.map(excludeMetaFromColumn), + columns: layerConfig.columns.map(excludeMetaFromColumn), columnOrder: [], }, ], @@ -89,7 +91,7 @@ export const convertToLens: ConvertMetricVisToLensVisualization = async (vis, ti layerId, vis.params, getPalette(vis.params.metric, percentageModeConfig), - result + layerConfig ), indexPatternIds: [indexPatternId], }; diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts index 0a10a5bd7c0c0..87ec0d3b57b3f 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts @@ -19,7 +19,7 @@ describe('getConfiguration', () => { expect( getConfiguration('test1', samplePieVis as any, { metrics: ['metric-1'], - buckets: ['bucket-1'], + buckets: { all: ['bucket-1'], customBuckets: {} }, }) ).toEqual({ layers: [ @@ -55,7 +55,7 @@ describe('getConfiguration', () => { { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'hide' } } as any, { metrics: ['metric-1'], - buckets: ['bucket-1'], + buckets: { all: ['bucket-1'], customBuckets: {} }, } ) ).toEqual({ @@ -73,7 +73,7 @@ describe('getConfiguration', () => { { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay: 'show' } } as any, { metrics: ['metric-1'], - buckets: ['bucket-1'], + buckets: { all: ['bucket-1'], customBuckets: {} }, } ) ).toEqual({ @@ -92,7 +92,7 @@ describe('getConfiguration', () => { { ...samplePieVis, params: { ...samplePieVis.params, legendDisplay } } as any, { metrics: ['metric-1'], - buckets: ['bucket-1'], + buckets: { all: ['bucket-1'], customBuckets: {} }, } ) ).toEqual({ diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts index 9a3420581c1fd..d1d1daf9fe009 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts @@ -68,12 +68,15 @@ export const getConfiguration = ( buckets, }: { metrics: string[]; - buckets: string[]; + buckets: { + all: string[]; + customBuckets: Record; + }; } ): PartitionVisConfiguration => { return { shape: vis.params.isDonut ? 'donut' : 'pie', - layers: getLayers(layerId, vis, metrics, buckets), + layers: getLayers(layerId, vis, metrics, buckets.all), palette: vis.params.palette, }; }; diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts index c1e39d741f84d..bf4dfbd3ffc72 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/index.test.ts @@ -40,28 +40,34 @@ describe('convertToLens', () => { }); test('should return null if more than three split slice levels', async () => { - mockGetColumnsFromVis.mockReturnValue({ - buckets: ['1', '2', '3', '4'], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['1', '2', '3', '4'] }, + }, + ]); const result = await convertToLens(samplePieVis as any, {} as any); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should return null if no one split slices', async () => { - mockGetColumnsFromVis.mockReturnValue({ - buckets: [], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: [] }, + }, + ]); const result = await convertToLens(samplePieVis as any, {} as any); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(result).toBeNull(); }); test('should state for valid vis', async () => { - mockGetColumnsFromVis.mockReturnValue({ - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + }, + ]); const result = await convertToLens(samplePieVis as any, {} as any); expect(mockGetColumnsFromVis).toBeCalledTimes(1); expect(mockGetConfiguration).toBeCalledTimes(1); diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/index.ts index 5b1973507c7df..c7231af7098c8 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/index.ts @@ -44,19 +44,21 @@ export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilt } const { getColumnsFromVis } = await convertToLensModule; - const result = getColumnsFromVis(vis, timefilter, dataView, { + const layers = getColumnsFromVis(vis, timefilter, dataView, { buckets: [], splits: ['segment'], unsupported: ['split_row', 'split_column'], }); - if (result === null) { + if (layers === null) { return null; } + const [layerConfig] = layers; + // doesn't support more than three split slice levels // doesn't support pie without at least one split slice - if (result.buckets.length > 3 || !result.buckets.length) { + if (layerConfig.buckets.all.length > 3 || !layerConfig.buckets.all.length) { return null; } @@ -69,11 +71,11 @@ export const convertToLens: ConvertPieToLensVisualization = async (vis, timefilt { indexPatternId, layerId, - columns: result.columns.map(excludeMetaFromColumn), + columns: layerConfig.columns.map(excludeMetaFromColumn), columnOrder: [], }, ], - configuration: getConfiguration(layerId, vis, result), + configuration: getConfiguration(layerId, vis, layerConfig), indexPatternIds: [indexPatternId], }; }; diff --git a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts index 4393ec86c2271..d2e68436700d3 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.test.ts @@ -25,7 +25,7 @@ describe('getConfiguration', () => { expect( getConfiguration('test1', params, { metrics: ['metric-1'], - buckets: ['bucket-1'], + buckets: { all: ['bucket-1'], customBuckets: { 'metric-1': 'bucket-1' } }, columnsWithoutReferenced: [ { columnId: 'metric-1', @@ -48,7 +48,7 @@ describe('getConfiguration', () => { }, }, ], - bucketCollapseFn: { 'bucket-1': 'sum' }, + bucketCollapseFn: { sum: ['bucket-1'] }, }) ).toEqual({ columns: [ diff --git a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts index d98cb917b40ac..5079b25258a75 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/configurations/index.ts @@ -12,19 +12,21 @@ import { TableVisParams } from '../../../common'; const getColumns = ( params: TableVisParams, metrics: string[], - buckets: string[], columns: Column[], - bucketCollapseFn?: Record + bucketCollapseFn?: Record ) => { const { showTotal, totalFunc } = params; - return columns.map(({ columnId }) => ({ - columnId, - alignment: 'left' as const, - ...(showTotal && metrics.includes(columnId) ? { summaryRow: totalFunc } : {}), - ...(bucketCollapseFn && bucketCollapseFn[columnId] - ? { collapseFn: bucketCollapseFn[columnId] } - : {}), - })); + return columns.map(({ columnId }) => { + const collapseFn = bucketCollapseFn + ? Object.keys(bucketCollapseFn).find((key) => bucketCollapseFn[key].includes(columnId)) + : undefined; + return { + columnId, + alignment: 'left' as const, + ...(showTotal && metrics.includes(columnId) ? { summaryRow: totalFunc } : {}), + ...(collapseFn ? { collapseFn } : {}), + }; + }); }; const getPagination = ({ perPage }: TableVisParams): PagingState => { @@ -54,15 +56,18 @@ export const getConfiguration = ( bucketCollapseFn, }: { metrics: string[]; - buckets: string[]; + buckets: { + all: string[]; + customBuckets: Record; + }; columnsWithoutReferenced: Column[]; - bucketCollapseFn?: Record; + bucketCollapseFn?: Record; } ): TableVisConfiguration => { return { layerId, layerType: 'data', - columns: getColumns(params, metrics, buckets, columnsWithoutReferenced, bucketCollapseFn), + columns: getColumns(params, metrics, columnsWithoutReferenced, bucketCollapseFn), paging: getPagination(params), ...getRowHeight(params), }; diff --git a/src/plugins/vis_types/table/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/table/public/convert_to_lens/index.test.ts index 5c1ad0578be11..f11f18f754eb9 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/index.test.ts @@ -63,14 +63,16 @@ describe('convertToLens', () => { }); test('should return null if can not build percentage column', async () => { - mockGetColumnsFromVis.mockReturnValue({ - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - columnsWithoutReferenced: [ - { columnId: '1', meta: { aggId: 'agg-1' } }, - { columnId: '2', meta: { aggId: 'agg-2' } }, - ], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + columnsWithoutReferenced: [ + { columnId: '1', meta: { aggId: 'agg-1' } }, + { columnId: '2', meta: { aggId: 'agg-2' } }, + ], + }, + ]); mockGetVisSchemas.mockReturnValue({ metric: [{ label: 'Count', aggId: 'agg-1' }], }); @@ -83,14 +85,16 @@ describe('convertToLens', () => { }); test('should return correct state for valid vis', async () => { - mockGetColumnsFromVis.mockReturnValue({ - buckets: ['2'], - columns: [{ columnId: '2' }, { columnId: '1' }], - columnsWithoutReferenced: [ - { columnId: '1', meta: { aggId: 'agg-1' } }, - { columnId: '2', meta: { aggId: 'agg-2' } }, - ], - }); + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['2'] }, + columns: [{ columnId: '2' }, { columnId: '1' }], + columnsWithoutReferenced: [ + { columnId: '1', meta: { aggId: 'agg-1' } }, + { columnId: '2', meta: { aggId: 'agg-2' } }, + ], + }, + ]); mockGetVisSchemas.mockReturnValue({ metric: [{ label: 'Count', aggId: 'agg-1' }], }); diff --git a/src/plugins/vis_types/table/public/convert_to_lens/index.ts b/src/plugins/vis_types/table/public/convert_to_lens/index.ts index 1b37e36f1d982..e69faccbfd7ec 100644 --- a/src/plugins/vis_types/table/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/table/public/convert_to_lens/index.ts @@ -46,7 +46,7 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi } const { getColumnsFromVis, getPercentageColumnFormulaColumn } = await convertToLensModule; - const result = getColumnsFromVis( + const layers = getColumnsFromVis( vis, timefilter, dataView, @@ -57,10 +57,12 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi { dropEmptyRowsInDateHistogram: true, isPercentageMode: false } ); - if (result === null) { + if (layers === null) { return null; } + const [layerConfig] = layers; + if (vis.params.percentageCol) { const visSchemas = getVisSchemas(vis, { timefilter, @@ -78,12 +80,12 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi if (!percentageColumn) { return null; } - result.columns.splice( - result.columnsWithoutReferenced.findIndex((c) => c.meta.aggId === metricAgg.aggId) + 1, + layerConfig.columns.splice( + layerConfig.columnsWithoutReferenced.findIndex((c) => c.meta.aggId === metricAgg.aggId) + 1, 0, percentageColumn ); - result.columnsWithoutReferenced.push(percentageColumn); + layerConfig.columnsWithoutReferenced.push(percentageColumn); } const layerId = uuid(); @@ -94,11 +96,11 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi { indexPatternId, layerId, - columns: result.columns.map(excludeMetaFromColumn), + columns: layerConfig.columns.map(excludeMetaFromColumn), columnOrder: [], }, ], - configuration: getConfiguration(layerId, vis.params, result), + configuration: getConfiguration(layerId, vis.params, layerConfig), indexPatternIds: [indexPatternId], }; }; diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index fa942c1530142..474a70431fc73 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["charts", "visualizations", "data", "expressions"], + "requiredPlugins": ["charts", "visualizations", "data", "expressions", "dataViews"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "extraPublicDirs": ["common/index"], "owner": { diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.test.ts new file mode 100644 index 0000000000000..2d8c7da9ba801 --- /dev/null +++ b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { Column } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { getConfiguration } from '.'; +import { Layer } from '..'; +import { ChartType } from '../..'; +import { sampleAreaVis } from '../../sample_vis.test.mocks'; +import { ChartMode, InterpolationMode } from '../../types'; + +describe('getConfiguration', () => { + const layers: Layer[] = [ + { + indexPatternId: '', + layerId: 'layer-1', + columns: [ + { columnId: '1', isBucketed: false }, + { columnId: '2', isBucketed: true, isSplit: false, operationType: 'date_histogram' }, + { columnId: '3', isBucketed: true, isSplit: true }, + ] as Column[], + metrics: ['1'], + columnOrder: [], + seriesIdsMap: { 1: '1' }, + collapseFn: 'max', + isReferenceLineLayer: false, + }, + { + indexPatternId: '', + layerId: 'layer-2', + columns: [ + { columnId: '4', isBucketed: false }, + { columnId: '5', isBucketed: true, isSplit: false, operationType: 'date_histogram' }, + ] as Column[], + metrics: ['4'], + columnOrder: [], + seriesIdsMap: { 4: '2' }, + collapseFn: undefined, + isReferenceLineLayer: false, + }, + { + indexPatternId: '', + layerId: 'layer-3', + columns: [{ columnId: '7', isBucketed: false }] as Column[], + columnOrder: [], + metrics: ['7'], + seriesIdsMap: {}, + collapseFn: undefined, + isReferenceLineLayer: true, + }, + ]; + const series = [ + { + show: true, + type: ChartType.Area, + mode: ChartMode.Stacked, + data: { + label: 'Sum of total_quantity', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + circlesRadius: 5, + interpolate: InterpolationMode.Linear, + valueAxis: 'ValueAxis-1', + }, + { + show: true, + type: ChartType.Line, + mode: ChartMode.Stacked, + data: { + label: 'Sum of total_quantity 1', + id: '2', + }, + drawLinesBetweenPoints: true, + showCircles: true, + circlesRadius: 5, + interpolate: InterpolationMode.Linear, + valueAxis: 'ValueAxis-1', + }, + ]; + + test('should return correct configuration', () => { + expect(getConfiguration(layers, series, sampleAreaVis as any)).toEqual({ + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + curveType: 'LINEAR', + fillOpacity: 0.5, + fittingFunction: undefined, + gridlinesVisibilitySettings: { x: false, yLeft: false, yRight: true }, + labelsOrientation: { x: -0, yLeft: -0, yRight: -90 }, + layers: [ + { + accessors: ['1'], + collapseFn: 'max', + isHistogram: true, + layerId: 'layer-1', + layerType: 'data', + palette: { name: 'default' }, + seriesType: 'area_stacked', + simpleView: false, + splitAccessor: '3', + xAccessor: '2', + xScaleType: 'ordinal', + yConfig: [{ axisMode: 'left', forAccessor: '1' }], + }, + { + accessors: ['4'], + collapseFn: undefined, + isHistogram: true, + layerId: 'layer-2', + layerType: 'data', + palette: { name: 'default' }, + seriesType: 'area_stacked', + simpleView: false, + splitAccessor: undefined, + xAccessor: '5', + xScaleType: 'ordinal', + yConfig: [{ axisMode: 'left', forAccessor: '4' }], + }, + { + accessors: ['7'], + layerId: 'layer-3', + layerType: 'referenceLine', + yConfig: [ + { + axisMode: 'left', + color: '#E7664C', + forAccessor: '7', + lineStyle: 'solid', + lineWidth: 1, + }, + ], + }, + ], + legend: { + isVisible: true, + legendSize: 'small', + maxLines: 1, + position: 'top', + shouldTruncate: true, + showSingleSeries: true, + }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + valuesInLegend: false, + xTitle: undefined, + yLeftExtent: { enforce: true, lowerBound: undefined, mode: 'full', upperBound: undefined }, + yLeftScale: 'linear', + yRightExtent: undefined, + yRightScale: 'linear', + yRightTitle: undefined, + yTitle: 'Sum of total_quantity', + showCurrentTimeMarker: false, + }); + }); +}); diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts new file mode 100644 index 0000000000000..fa9cc01c6a7ca --- /dev/null +++ b/src/plugins/vis_types/xy/public/convert_to_lens/configurations/index.ts @@ -0,0 +1,277 @@ +/* + * 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 { Position, ScaleType as ECScaleType } from '@elastic/charts'; +import { + SeriesTypes, + Column, + XYConfiguration, + XYDataLayerConfig, + XYReferenceLineLayerConfig, +} from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { Vis } from '@kbn/visualizations-plugin/public'; +import { Layer } from '..'; +import { ChartType } from '../../../common'; +import { + CategoryAxis, + ChartMode, + InterpolationMode, + Scale, + ScaleType, + SeriesParam, + ValueAxis, + VisParams, +} from '../../types'; +import { getCurveType, getLineStyle, getMode, getYAxisPosition } from '../../utils/common'; + +function getYScaleType(scale?: Scale): XYConfiguration['yLeftScale'] | undefined { + const type = scale?.type; + if (type === ScaleType.SquareRoot) { + return ECScaleType.Sqrt; + } + + return type; +} + +function getXScaleType(xColumn?: Column) { + if (xColumn?.dataType === 'date') return ECScaleType.Time; + + if (xColumn?.dataType !== 'number') { + return ECScaleType.Ordinal; + } + + return ECScaleType.Linear; +} + +function getLabelOrientation(data?: CategoryAxis, isTimeChart = false) { + // lens doesn't support 75 as rotate option, we should use 45 instead + return -(data?.labels.rotate === 75 ? 45 : data?.labels.rotate ?? (isTimeChart ? 0 : 90)); +} + +function getExtents(axis: ValueAxis, series: SeriesParam[]) { + // for area and bar charts we should include 0 to bounds + const isAssignedToAreaOrBar = series.some( + (s) => s.valueAxis === axis.id && (s.type === 'histogram' || s.type === 'area') + ); + return { + mode: getMode(axis.scale), + lowerBound: + axis.scale.min !== null + ? isAssignedToAreaOrBar && axis.scale.min && axis.scale.min > 0 + ? 0 + : axis.scale.min + : undefined, + upperBound: + axis.scale.max !== null + ? isAssignedToAreaOrBar && axis.scale.max && axis.scale.max < 0 + ? 0 + : axis.scale.max + : undefined, + enforce: true, + }; +} + +function getSeriesType( + type?: ChartType, + mode?: ChartMode, + isHorizontal?: boolean, + isPercentage?: boolean +): XYDataLayerConfig['seriesType'] { + let seriesType: XYDataLayerConfig['seriesType'] = + type === 'histogram' ? SeriesTypes.BAR : type ?? SeriesTypes.AREA; + + // only bar chart supports horizontal mode + if (isHorizontal && seriesType === SeriesTypes.BAR) { + seriesType = (seriesType + '_horizontal') as XYDataLayerConfig['seriesType']; + } + + // line percentage should convert to area percentage + if (isPercentage) { + seriesType = ((seriesType !== SeriesTypes.LINE ? seriesType : SeriesTypes.AREA) + + '_percentage') as XYDataLayerConfig['seriesType']; + } + + // percentage chart should be stacked + // line stacked should convert to area stacked + if (isPercentage || mode === 'stacked') { + seriesType = ((seriesType !== SeriesTypes.LINE ? seriesType : SeriesTypes.AREA) + + '_stacked') as XYDataLayerConfig['seriesType']; + } + + return seriesType; +} + +function getDataLayers( + layers: Layer[], + series: SeriesParam[], + vis: Vis +): XYDataLayerConfig[] { + const overwriteColors: Record = vis.uiState.get('vis.colors', {}); + return layers.map((layer) => { + const xColumn = layer.columns.find((c) => c.isBucketed && !c.isSplit); + const splitAccessor = layer.columns.find( + (column) => column.isBucketed && column.isSplit + )?.columnId; + // as type and mode will be the same for all metrics we can use first to define it + const firstSeries = series.find((s) => s.data.id === layer.seriesIdsMap[layer.metrics[0]]); + const isHistogram = + xColumn?.operationType === 'date_histogram' || + (xColumn?.operationType === 'range' && xColumn.params.type === 'histogram'); + const firstYAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find( + (axis) => axis.id === firstSeries?.valueAxis + ); + const isPercentage = firstYAxis?.scale.mode === 'percentage'; + const isHorizontal = + firstYAxis?.position !== Position.Left && firstYAxis?.position !== Position.Right; + const seriesType = getSeriesType( + firstSeries?.type, + firstSeries?.mode, + isHorizontal, + isPercentage + ); + + return { + layerId: layer.layerId, + accessors: layer.metrics, + layerType: 'data', + seriesType, + xAccessor: xColumn?.columnId, + simpleView: false, + splitAccessor, + palette: vis.params.palette ?? vis.type.visConfig.defaults.palette, + yConfig: layer.metrics.map((metricId) => { + const serie = series.find((s) => s.data.id === layer.seriesIdsMap[metricId]); + const yAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find( + (axis) => axis.id === serie?.valueAxis + ); + return { + forAccessor: metricId, + axisMode: getYAxisPosition(yAxis?.position ?? 'left'), + color: + !splitAccessor && serie?.data.label ? overwriteColors[serie?.data.label] : undefined, + }; + }), + xScaleType: getXScaleType(xColumn), + isHistogram, + collapseFn: layer.collapseFn, + }; + }); +} + +function getReferenceLineLayers( + layers: Layer[], + vis: Vis +): XYReferenceLineLayerConfig[] { + const thresholdLineConfig = vis.params.thresholdLine ?? vis.type.visConfig.defaults.thresholdLine; + // threshold line is always assigned to the first value axis + const yAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes)[0]; + return layers.map((layer) => { + return { + layerType: 'referenceLine', + layerId: layer.layerId, + accessors: layer.metrics, + yConfig: layer.metrics.map((metricId) => { + return { + forAccessor: metricId, + axisMode: getYAxisPosition(yAxis?.position ?? 'left'), + color: thresholdLineConfig.color, + lineWidth: thresholdLineConfig.width !== null ? thresholdLineConfig.width : undefined, + lineStyle: getLineStyle(thresholdLineConfig.style), + }; + }), + }; + }); +} + +export const getConfiguration = ( + layers: Layer[], + series: SeriesParam[], + vis: Vis +): XYConfiguration => { + const legendDisplayFromUiState = vis.uiState.get('vis.legendOpen') ?? true; + const yRightAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find( + (axis) => getYAxisPosition(axis.position) === Position.Right + ); + const yLeftAxis = (vis.params.valueAxes ?? vis.type.visConfig.defaults.valueAxes).find( + (axis) => getYAxisPosition(axis.position) === Position.Left + ); + // as we have only one x-axis + const xAxis = (vis.params.categoryAxes ?? vis.type.visConfig.defaults.categoryAxes)[0]; + const axisTitlesVisibilitySettings = { + x: xAxis.show, + yLeft: yLeftAxis?.show ?? true, + yRight: yRightAxis?.show ?? true, + }; + const xColumn = layers[0].columns.find((c) => c.isBucketed && !c.isSplit); + const isTimeChart = xColumn?.operationType === 'date_histogram'; + const fittingFunction = vis.params.fittingFunction ?? vis.type.visConfig.defaults.fittingFunction; + return { + layers: [ + ...getDataLayers( + layers.filter((l) => !l.isReferenceLineLayer), + series, + vis + ), + ...getReferenceLineLayers( + layers.filter((l) => l.isReferenceLineLayer), + vis + ), + ], + legend: { + isVisible: + legendDisplayFromUiState && (vis.params.addLegend ?? vis.type.visConfig.defaults.addLegend), + position: vis.params.legendPosition ?? vis.type.visConfig.defaults.legendPosition, + legendSize: vis.params.legendSize ?? vis.type.visConfig.defaults.legendSize, + shouldTruncate: vis.params.truncateLegend ?? vis.type.visConfig.defaults.truncateLegend, + maxLines: vis.params.maxLegendLines ?? vis.type.visConfig.defaults.maxLegendLines, + showSingleSeries: true, + }, + fittingFunction: fittingFunction + ? fittingFunction[0].toUpperCase() + fittingFunction.slice(1) + : undefined, + fillOpacity: vis.params.fillOpacity ?? vis.type.visConfig.defaults.fillOpacity, + gridlinesVisibilitySettings: { + x: vis.params.grid.categoryLines ?? vis.type.visConfig.defaults.grid?.categoryLines, + yLeft: + (vis.params.grid.valueAxis ?? vis.type.visConfig.defaults.grid?.valueAxis) === + yLeftAxis?.id, + yRight: + (vis.params.grid.valueAxis ?? vis.type.visConfig.defaults.grid?.valueAxis) === + yRightAxis?.id, + }, + axisTitlesVisibilitySettings, + tickLabelsVisibilitySettings: { + x: axisTitlesVisibilitySettings.x && (xAxis.labels.show ?? true), + yLeft: axisTitlesVisibilitySettings.yLeft && (yLeftAxis?.labels.show ?? true), + yRight: axisTitlesVisibilitySettings.yRight && (yRightAxis?.labels.show ?? true), + }, + labelsOrientation: { + x: getLabelOrientation(xAxis, isTimeChart), + yLeft: getLabelOrientation(yLeftAxis), + yRight: getLabelOrientation(yRightAxis), + }, + yLeftScale: getYScaleType(yLeftAxis?.scale) ?? ECScaleType.Linear, + yRightScale: getYScaleType(yRightAxis?.scale) ?? ECScaleType.Linear, + yLeftExtent: yLeftAxis?.scale ? getExtents(yLeftAxis, series) : undefined, + yRightExtent: yRightAxis?.scale ? getExtents(yRightAxis, series) : undefined, + yTitle: yLeftAxis?.title.text, + yRightTitle: yRightAxis?.title.text, + xTitle: xAxis.title.text, + valueLabels: + vis.params.labels.show ?? vis.type.visConfig.defaults.labels?.show ? 'show' : 'hide', + valuesInLegend: Boolean(vis.params.labels.show ?? vis.type.visConfig.defaults.labels?.show), + showCurrentTimeMarker: isTimeChart + ? Boolean(vis.params.addTimeMarker ?? vis.type.visConfig.defaults.addTimeMarker) + : undefined, + curveType: getCurveType( + series[0].interpolate === InterpolationMode.StepAfter + ? InterpolationMode.Linear + : series[0].interpolate + ), + }; +}; diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/xy/public/convert_to_lens/index.test.ts new file mode 100644 index 0000000000000..c77794e22ab78 --- /dev/null +++ b/src/plugins/vis_types/xy/public/convert_to_lens/index.test.ts @@ -0,0 +1,202 @@ +/* + * 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 { convertToLens } from '.'; +import { sampleAreaVis } from '../sample_vis.test.mocks'; + +const mockGetColumnsFromVis = jest.fn(); +const mockCreateStaticValueColumn = jest.fn().mockReturnValue({ operationType: 'static_value' }); +const mockGetVisSchemas = jest.fn().mockReturnValue({ + metric: [{ aggId: '1' }], +}); +const mockGetConfiguration = jest.fn().mockReturnValue({}); + +jest.mock('../services', () => ({ + getDataViewsStart: jest.fn(() => ({ get: () => ({}), getDefault: () => ({}) })), +})); + +jest.mock('../utils/get_series_params', () => ({ + getSeriesParams: jest.fn(() => undefined), +})); + +jest.mock('@kbn/visualizations-plugin/public', () => ({ + convertToLensModule: Promise.resolve({ + getColumnsFromVis: jest.fn(() => mockGetColumnsFromVis()), + createStaticValueColumn: jest.fn(() => mockCreateStaticValueColumn()), + }), + getDataViewByIndexPatternId: jest.fn(() => ({ id: 'index-pattern' })), + getVisSchemas: jest.fn(() => mockGetVisSchemas()), +})); + +jest.mock('./configurations', () => ({ + getConfiguration: jest.fn(() => mockGetConfiguration()), +})); + +describe('convertToLens', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null if getColumnsFromVis returns null', async () => { + mockGetColumnsFromVis.mockReturnValue(null); + const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if multi split series defined', async () => { + mockGetVisSchemas.mockReturnValue({ + metric: [{ aggId: '1' }], + group: [{}, {}], + }); + const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any); + expect(mockGetVisSchemas).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if sibling pipeline agg defined together with split series', async () => { + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['1'], customBuckets: { metric1: '2' } }, + }, + ]); + mockGetVisSchemas.mockReturnValue({ + metric: [{ aggId: '1' }], + group: [{}], + }); + const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if defined several layers with terms split series which uses one of the metrics as order agg', async () => { + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['1'], customBuckets: { metric1: '2' } }, + columns: [{ isSplit: true, params: { orderBy: { type: 'column' } } }], + }, + { + buckets: { all: ['2'], customBuckets: { metric1: '2' } }, + columns: [{}], + }, + ]); + mockGetVisSchemas.mockReturnValue({ + metric: [{ aggId: '1' }, { aggId: '2' }], + }); + const result = await convertToLens(sampleAreaVis as any, { getAbsoluteTime: () => {} } as any); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should return null if more than one axis left/right/top/bottom defined', async () => { + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['1'], customBuckets: {} }, + columns: [], + }, + ]); + mockGetVisSchemas.mockReturnValue({ + metric: [{ aggId: '1' }, { aggId: '2' }], + }); + const result = await convertToLens( + { + ...sampleAreaVis, + params: { + ...sampleAreaVis.params, + valueAxes: [ + ...sampleAreaVis.params.valueAxes, + { + id: 'ValueAxis-2', + name: 'LeftAxis-2', + type: 'value', + position: 'left', + data: { + id: '2', + }, + }, + ], + seriesParams: [ + ...sampleAreaVis.params.seriesParams, + { show: true, valueAxis: 'ValueAxis-2', data: { id: '2' } }, + ], + }, + } as any, + { getAbsoluteTime: () => {} } as any + ); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(result).toBeNull(); + }); + + test('should state for valid vis', async () => { + mockGetColumnsFromVis.mockReturnValue([ + { + buckets: { all: ['2', '3'], customBuckets: { 1: '3' } }, + columns: [ + { columnId: '2', isBucketed: true }, + { columnId: '1', meta: { aggId: '1' } }, + { columnId: '3', isBucketed: true }, + ], + bucketCollapseFn: { sum: ['3'] }, + metrics: ['1'], + }, + { + buckets: { all: ['2'], customBuckets: {} }, + columns: [ + { columnId: '2', isBucketed: true }, + { columnId: '1', meta: { aggId: '2' } }, + ], + metrics: ['1'], + bucketCollapseFn: {}, + }, + ]); + mockGetVisSchemas.mockReturnValue({ + metric: [{ aggId: '1' }], + }); + const result = await convertToLens( + { + ...sampleAreaVis, + params: { + ...sampleAreaVis.params, + valueAxes: [ + ...sampleAreaVis.params.valueAxes, + { + id: 'ValueAxis-2', + name: 'LeftAxis-2', + type: 'value', + position: 'left', + data: { + id: '2', + }, + }, + ], + seriesParams: [ + ...sampleAreaVis.params.seriesParams, + { show: true, valueAxis: 'ValueAxis-2', data: { id: '2' } }, + ], + thresholdLine: { ...sampleAreaVis.params.thresholdLine, show: true }, + }, + } as any, + { getAbsoluteTime: () => {} } as any + ); + expect(mockGetColumnsFromVis).toBeCalledTimes(1); + expect(mockGetConfiguration).toBeCalledTimes(1); + expect(mockCreateStaticValueColumn).toBeCalledTimes(1); + expect(result?.type).toEqual('lnsXY'); + expect(result?.layers.length).toEqual(3); + expect(result?.layers[0].columns).toEqual([ + { columnId: '2', isBucketed: true }, + { columnId: '1' }, + { columnId: '3', isBucketed: true }, + ]); + expect(result?.layers[1].columns).toEqual([ + { columnId: '2', isBucketed: true }, + { columnId: '1' }, + ]); + expect(result?.layers[2].columns).toEqual([{ operationType: 'static_value' }]); + }); +}); diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/index.ts b/src/plugins/vis_types/xy/public/convert_to_lens/index.ts new file mode 100644 index 0000000000000..3b4339828c6d5 --- /dev/null +++ b/src/plugins/vis_types/xy/public/convert_to_lens/index.ts @@ -0,0 +1,214 @@ +/* + * 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 { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common'; +import { + convertToLensModule, + getVisSchemas, + getDataViewByIndexPatternId, +} from '@kbn/visualizations-plugin/public'; +import { getDataViewsStart } from '../services'; +import { getSeriesParams } from '../utils/get_series_params'; +import { ConvertXYToLensVisualization } from './types'; + +export interface Layer { + indexPatternId: string; + layerId: string; + columns: Column[]; + metrics: string[]; + columnOrder: never[]; + seriesIdsMap: Record; + isReferenceLineLayer: boolean; + collapseFn?: string; +} + +const SIBBLING_PIPELINE_AGGS: string[] = [ + METRIC_TYPES.AVG_BUCKET, + METRIC_TYPES.SUM_BUCKET, + METRIC_TYPES.MAX_BUCKET, + METRIC_TYPES.MIN_BUCKET, +]; + +export const isColumnWithMeta = (column: Column): column is ColumnWithMeta => { + if ((column as ColumnWithMeta).meta) { + return true; + } + return false; +}; + +export const excludeMetaFromColumn = (column: Column) => { + if (isColumnWithMeta(column)) { + const { meta, ...rest } = column; + return rest; + } + return column; +}; + +export const convertToLens: ConvertXYToLensVisualization = async (vis, timefilter) => { + if (!timefilter) { + return null; + } + + const dataViews = getDataViewsStart(); + const dataView = await getDataViewByIndexPatternId(vis.data.indexPattern?.id, dataViews); + + if (!dataView) { + return null; + } + + const visSchemas = getVisSchemas(vis, { + timefilter, + timeRange: timefilter.getAbsoluteTime(), + }); + + // doesn't support multi split series + if (visSchemas.group && visSchemas.group.length > 1) { + return null; + } + + const firstValueAxesId = vis.params.valueAxes[0].id; + const updatedSeries = getSeriesParams( + vis.data.aggs, + vis.params.seriesParams, + 'metric', + firstValueAxesId + ); + + const finalSeriesParams = updatedSeries ?? vis.params.seriesParams; + const visibleSeries = finalSeriesParams.filter( + (param) => param.show && visSchemas.metric.some((m) => m.aggId?.split('.')[0] === param.data.id) + ); + + const [{ getColumnsFromVis, createStaticValueColumn }, { getConfiguration }] = await Promise.all([ + convertToLensModule, + import('./configurations'), + ]); + const dataLayers = getColumnsFromVis( + vis, + timefilter, + dataView, + { + buckets: ['segment'], + splits: ['group'], + unsupported: ['split_row', 'split_column', 'radius'], + }, + { + dropEmptyRowsInDateHistogram: true, + supportMixedSiblingPipelineAggs: true, + isPercentageMode: false, + }, + visibleSeries + .reduce>((acc, s) => { + const series = acc.find(({ type, mode }) => type === s.type && mode === s.mode); + // sibling pipeline agg always generate new layer because of custom bucket + if ( + series && + visSchemas.metric.some( + (m) => + m.aggId?.split('.')[0] === s.data.id && !SIBBLING_PIPELINE_AGGS.includes(m.aggType) + ) + ) { + series.metrics.push(s.data.id); + } else { + acc.push({ metrics: [s.data.id], type: s.type, mode: s.mode }); + } + return acc; + }, []) + .map(({ metrics }) => ({ metrics })) + ); + + if (dataLayers === null) { + return null; + } + + // doesn't support several layers with terms split series which uses one of the metrics as order agg + if ( + dataLayers.length > 1 && + dataLayers.some((l) => + l.columns.some( + (c) => c.isSplit && 'orderBy' in c.params && c.params.orderBy.type === 'column' + ) + ) + ) { + return null; + } + + // doesn't support sibling pipeline aggs and split series together + if ( + visSchemas.group?.length && + dataLayers.some((l) => Object.keys(l.buckets.customBuckets).length) + ) { + return null; + } + + const visibleYAxes = vis.params.valueAxes.filter((axis) => + visibleSeries.some((seriesParam) => seriesParam.valueAxis === axis.id) + ); + + const positions = visibleYAxes.map((axis) => axis.position); + const uniqPoisitions = new Set(positions); + + // doesn't support more than one axis left/right/top/bottom + if (visibleYAxes.length > 1 && uniqPoisitions.size !== positions.length) { + return null; + } + + const indexPatternId = dataView.id!; + + const uuid = await import('uuid/v4'); + + const layers = dataLayers.map((l) => { + const layerId = uuid.default(); + const seriesIdsMap: Record = {}; + visibleSeries.forEach((s) => { + const column = l.columns.find( + (c) => !c.isBucketed && c.meta.aggId.split('.')[0] === s.data.id + ); + if (column) { + seriesIdsMap[column.columnId] = s.data.id; + } + }); + const collapseFn = l.bucketCollapseFn + ? Object.keys(l.bucketCollapseFn).find((key) => + l.bucketCollapseFn[key].includes(l.buckets.customBuckets[l.metrics[0]]) + ) + : undefined; + return { + indexPatternId, + layerId, + columns: l.columns.map(excludeMetaFromColumn), + metrics: l.metrics, + columnOrder: [], + seriesIdsMap, + collapseFn, + isReferenceLineLayer: false, + }; + }); + + if (vis.params.thresholdLine.show) { + const staticValueColumn = createStaticValueColumn(vis.params.thresholdLine.value || 0); + layers.push({ + indexPatternId, + layerId: uuid.default(), + columns: [staticValueColumn], + columnOrder: [], + metrics: [staticValueColumn.columnId], + isReferenceLineLayer: true, + collapseFn: undefined, + seriesIdsMap: {}, + }); + } + + return { + type: 'lnsXY', + layers: layers.map(({ seriesIdsMap, collapseFn, isReferenceLineLayer, ...rest }) => rest), + configuration: getConfiguration(layers, visibleSeries, vis), + indexPatternIds: [indexPatternId], + }; +}; diff --git a/src/plugins/vis_types/xy/public/convert_to_lens/types.ts b/src/plugins/vis_types/xy/public/convert_to_lens/types.ts new file mode 100644 index 0000000000000..5fa52b4221107 --- /dev/null +++ b/src/plugins/vis_types/xy/public/convert_to_lens/types.ts @@ -0,0 +1,17 @@ +/* + * 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 { TimefilterContract } from '@kbn/data-plugin/public'; +import { NavigateToLensContext, XYConfiguration } from '@kbn/visualizations-plugin/common'; +import { Vis } from '@kbn/visualizations-plugin/public'; +import { VisParams } from '../types'; + +export type ConvertXYToLensVisualization = ( + vis: Vis, + timefilter?: TimefilterContract +) => Promise | null>; diff --git a/src/plugins/vis_types/xy/public/plugin.ts b/src/plugins/vis_types/xy/public/plugin.ts index 4561006e43e92..ad75af4dfffdb 100644 --- a/src/plugins/vis_types/xy/public/plugin.ts +++ b/src/plugins/vis_types/xy/public/plugin.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -import type { CoreSetup, Plugin } from '@kbn/core/public'; +import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; -import { setUISettings, setPalettesService } from './services'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { setUISettings, setPalettesService, setDataViewsStart } from './services'; import { visTypesDefinitions } from './vis_types'; @@ -24,6 +25,11 @@ export interface VisTypeXyPluginSetupDependencies { charts: ChartsPluginSetup; } +/** @internal */ +export interface VisTypeXyPluginStartDependencies { + dataViews: DataViewsPublicPluginStart; +} + type VisTypeXyCoreSetup = CoreSetup<{}, VisTypeXyPluginStart>; /** @internal */ @@ -42,7 +48,8 @@ export class VisTypeXyPlugin return {}; } - public start() { + public start(core: CoreStart, { dataViews }: VisTypeXyPluginStartDependencies) { + setDataViewsStart(dataViews); return {}; } } diff --git a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts index e55debd7c77ba..b2660b7c66551 100644 --- a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts @@ -8,6 +8,8 @@ import { LegendSize } from '@kbn/visualizations-plugin/common'; +const mockUiStateGet = jest.fn().mockReturnValue(() => false); + export const sampleAreaVis = { type: { name: 'area', @@ -1918,5 +1920,10 @@ export const sampleAreaVis = { }, }, isHierarchical: () => false, - uiState: {}, + uiState: { + vis: { + legendOpen: false, + }, + get: mockUiStateGet, + }, }; diff --git a/src/plugins/vis_types/xy/public/services.ts b/src/plugins/vis_types/xy/public/services.ts index 2358bcb5ede2e..7513f6188ef0e 100644 --- a/src/plugins/vis_types/xy/public/services.ts +++ b/src/plugins/vis_types/xy/public/services.ts @@ -8,6 +8,7 @@ import type { CoreSetup } from '@kbn/core/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; @@ -16,3 +17,6 @@ export const [getUISettings, setUISettings] = export const [getPalettesService, setPalettesService] = createGetterSetter('xy charts.palette'); + +export const [getDataViewsStart, setDataViewsStart] = + createGetterSetter('dataViews'); diff --git a/src/plugins/vis_types/xy/public/to_ast.ts b/src/plugins/vis_types/xy/public/to_ast.ts index 4041075b98c4d..b584fbac26bba 100644 --- a/src/plugins/vis_types/xy/public/to_ast.ts +++ b/src/plugins/vis_types/xy/public/to_ast.ts @@ -18,8 +18,8 @@ import { } from '@kbn/visualizations-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { BUCKET_TYPES } from '@kbn/data-plugin/public'; -import { TimeRangeBounds } from '@kbn/data-plugin/common'; -import { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types'; +import type { TimeRangeBounds } from '@kbn/data-plugin/common'; +import type { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types'; import { Dimensions, Dimension, @@ -30,32 +30,15 @@ import { ValueAxis, Scale, ChartMode, - InterpolationMode, ScaleType, } from './types'; import { ChartType } from '../common'; import { getSeriesParams } from './utils/get_series_params'; import { getSafeId } from './utils/accessors'; - -interface Bounds { - min?: string | number; - max?: string | number; -} +import { Bounds, getCurveType, getLineStyle, getMode, getYAxisPosition } from './utils/common'; type YDimension = Omit & { accessor: string }; -const getCurveType = (type?: InterpolationMode) => { - switch (type) { - case 'cardinal': - return 'CURVE_MONOTONE_X'; - case 'step-after': - return 'CURVE_STEP_AFTER'; - case 'linear': - default: - return 'LINEAR'; - } -}; - const prepareLengend = (params: VisParams, legendSize?: LegendSize) => { const legend = buildExpressionFunction('legendConfig', { isVisible: params.addLegend, @@ -162,16 +145,6 @@ const prepareLayers = ( return buildExpression([dataLayer]); }; -const getMode = (scale: Scale, bounds?: Bounds) => { - if (scale.defaultYExtents) { - return 'dataBounds'; - } - - if (scale.setYExtents || bounds) { - return 'custom'; - } -}; - const getLabelArgs = (data: CategoryAxis, isTimeChart?: boolean) => { return { truncate: data.labels.truncate, @@ -215,18 +188,6 @@ function getScaleType( return type; } -function getYAxisPosition(position: Position) { - if (position === Position.Top) { - return Position.Right; - } - - if (position === Position.Bottom) { - return Position.Left; - } - - return position; -} - function getXAxisPosition(position: Position) { if (position === Position.Left) { return Position.Bottom; @@ -274,16 +235,6 @@ const prepareYAxis = (data: ValueAxis, showGridLines?: boolean) => { return buildExpression([yAxisConfig]); }; -const getLineStyle = (style: ThresholdLine['style']) => { - switch (style) { - case 'full': - return 'solid'; - case 'dashed': - case 'dot-dashed': - return style; - } -}; - const prepareReferenceLine = (thresholdLine: ThresholdLine, axisId: string) => { const referenceLine = buildExpressionFunction('referenceLine', { value: thresholdLine.value, @@ -483,6 +434,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params splitColumnAccessor: dimensions.splitColumn?.map(prepareVisDimension), splitRowAccessor: dimensions.splitRow?.map(prepareVisDimension), valueLabels: vis.params.labels.show ? 'show' : 'hide', + valuesInLegend: vis.params.labels.show, singleTable: true, }); diff --git a/src/plugins/vis_types/xy/public/utils/common.ts b/src/plugins/vis_types/xy/public/utils/common.ts new file mode 100644 index 0000000000000..522cac2aa00ac --- /dev/null +++ b/src/plugins/vis_types/xy/public/utils/common.ts @@ -0,0 +1,62 @@ +/* + * 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 { Position } from '@elastic/charts'; +import type { AxisExtentConfig } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import type { InterpolationMode, Scale, ThresholdLine } from '../types'; + +export interface Bounds { + min?: string | number; + max?: string | number; +} + +export const getCurveType = (type?: InterpolationMode) => { + switch (type) { + case 'cardinal': + return 'CURVE_MONOTONE_X'; + case 'step-after': + return 'CURVE_STEP_AFTER'; + case 'linear': + default: + return 'LINEAR'; + } +}; + +export const getMode = (scale: Scale, bounds?: Bounds): AxisExtentConfig['mode'] => { + if (scale.defaultYExtents) { + return 'dataBounds'; + } + + if (scale.setYExtents || bounds) { + return 'custom'; + } + + return 'full'; +}; + +export const getYAxisPosition = (position: Position) => { + if (position === Position.Top) { + return Position.Right; + } + + if (position === Position.Bottom) { + return Position.Left; + } + + return position; +}; + +export const getLineStyle = (style: ThresholdLine['style']) => { + switch (style) { + case 'full': + return 'solid'; + case 'dashed': + case 'dot-dashed': + return style; + } +}; diff --git a/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts index c1f79b041e807..1f7ead86843d9 100644 --- a/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts +++ b/src/plugins/vis_types/xy/public/vis_types/get_vis_type_from_params.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { VisParams } from '@kbn/visualizations-plugin/common'; +import type { VisParams } from '@kbn/visualizations-plugin/common'; export const getVisTypeFromParams = (params?: VisParams) => { let type = params?.seriesParams?.[0]?.type; diff --git a/src/plugins/vis_types/xy/public/vis_types/index.ts b/src/plugins/vis_types/xy/public/vis_types/index.ts index 93c973b5316c9..2f7a03b6aaf1c 100644 --- a/src/plugins/vis_types/xy/public/vis_types/index.ts +++ b/src/plugins/vis_types/xy/public/vis_types/index.ts @@ -6,14 +6,27 @@ * Side Public License, v 1. */ +import type { VisTypeDefinition } from '@kbn/visualizations-plugin/public'; +import type { VisParams } from '../types'; import { areaVisTypeDefinition } from './area'; import { lineVisTypeDefinition } from './line'; import { histogramVisTypeDefinition } from './histogram'; import { horizontalBarVisTypeDefinition } from './horizontal_bar'; +import { convertToLens } from '../convert_to_lens'; export const visTypesDefinitions = [ areaVisTypeDefinition, lineVisTypeDefinition, histogramVisTypeDefinition, horizontalBarVisTypeDefinition, -]; +].map>((defenition) => { + return { + ...defenition, + navigateToLens: async (vis, timefilter) => (vis ? convertToLens(vis, timefilter) : null), + getExpressionVariables: async (vis, timeFilter) => { + return { + canNavigateToLens: Boolean(vis?.params ? await convertToLens(vis, timeFilter) : null), + }; + }, + }; +}); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts index 03e1d955dd045..a8389cb8601e4 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts @@ -13,7 +13,7 @@ import { convertToSchemaConfig } from '../../../vis_schemas'; export const convertToSiblingPipelineColumns = ( columnConverterArgs: ExtendedColumnConverterArgs ): AggBasedColumn | null => { - const { aggParams, label } = columnConverterArgs.agg; + const { aggParams, label, aggId } = columnConverterArgs.agg; if (!aggParams) { return null; } @@ -23,7 +23,7 @@ export const convertToSiblingPipelineColumns = ( } const customMetricColumn = convertMetricToColumns( - { ...convertToSchemaConfig(aggParams.customMetric), label }, + { ...convertToSchemaConfig(aggParams.customMetric), label, aggId }, columnConverterArgs.dataView, columnConverterArgs.aggs ); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts index 3dfaee67a61e0..8e6f9ec9443bb 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts @@ -58,7 +58,7 @@ export type SiblingPipelineMetric = | METRIC_TYPES.MIN_BUCKET | METRIC_TYPES.MAX_BUCKET; -export type BucketColumn = DateHistogramColumn | TermsColumn | FiltersColumn; +export type BucketColumn = DateHistogramColumn | TermsColumn | FiltersColumn | RangeColumn; export interface CommonColumnConverterArgs< Agg extends SupportedAggregation = SupportedAggregation > { diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts index 73118b6ad4f03..1f91201aff503 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/utils.test.ts @@ -365,7 +365,7 @@ describe('getCustomBucketsFromSiblingAggs', () => { }, params: {}, aggType: METRIC_TYPES.AVG_BUCKET, - aggId: 'some-agg-id', + aggId: 'some-agg-id-1', aggParams: { customBucket: bucketWithSerialize1, }, @@ -381,7 +381,7 @@ describe('getCustomBucketsFromSiblingAggs', () => { }, params: {}, aggType: METRIC_TYPES.AVG_BUCKET, - aggId: 'some-agg-id', + aggId: 'some-agg-id-2', aggParams: { customBucket: bucketWithSerialize2, }, @@ -399,7 +399,7 @@ describe('getCustomBucketsFromSiblingAggs', () => { }, params: {}, aggType: METRIC_TYPES.AVG_BUCKET, - aggId: 'some-agg-id', + aggId: 'some-agg-id-3', aggParams: { customBucket: bucketWithSerialize3, }, @@ -407,8 +407,8 @@ describe('getCustomBucketsFromSiblingAggs', () => { test("should filter out duplicated custom buckets, ignoring id's", () => { expect(getCustomBucketsFromSiblingAggs([metric1, metric2, metric3])).toEqual([ - bucketWithSerialize1, - bucketWithSerialize2, + { customBucket: bucketWithSerialize1, metricIds: ['some-agg-id-1', 'some-agg-id-3'] }, + { customBucket: bucketWithSerialize2, metricIds: ['some-agg-id-2'] }, ]); }); }); diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts b/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts index c4e5c5474bf0c..ce50312d92cf3 100644 --- a/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts +++ b/src/plugins/visualizations/common/convert_to_lens/lib/utils.ts @@ -151,19 +151,19 @@ export const isStdDevAgg = (metric: SchemaConfig): metric is SchemaConfig { - return metrics.reduce((acc, metric) => { - if ( - isSiblingPipeline(metric) && - metric.aggParams?.customBucket && - acc.every( - (bucket) => - !isEqual( - omit(metric.aggParams?.customBucket?.serialize(), ['id']), - omit(bucket.serialize(), ['id']) - ) - ) - ) { - acc.push(metric.aggParams.customBucket); + return metrics.reduce>((acc, metric) => { + if (isSiblingPipeline(metric) && metric.aggParams?.customBucket && metric.aggId) { + const customBucket = acc.find((bucket) => + isEqual( + omit(metric.aggParams?.customBucket?.serialize(), ['id']), + omit(bucket.customBucket.serialize(), ['id']) + ) + ); + if (customBucket) { + customBucket.metricIds.push(metric.aggId); + } else { + acc.push({ customBucket: metric.aggParams.customBucket, metricIds: [metric.aggId] }); + } } return acc; 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 fbc9e17d77727..2abcc3a281626 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -162,6 +162,7 @@ export interface XYConfiguration { fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; + showCurrentTimeMarker?: boolean; } export interface SortingState { diff --git a/src/plugins/visualizations/common/convert_to_lens/utils.ts b/src/plugins/visualizations/common/convert_to_lens/utils.ts index 3536282d830d3..51c7e8239a439 100644 --- a/src/plugins/visualizations/common/convert_to_lens/utils.ts +++ b/src/plugins/visualizations/common/convert_to_lens/utils.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { SupportedMetric } from './lib/convert/supported_metrics'; -import { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import type { SupportedMetric } from './lib/convert/supported_metrics'; +import type { Layer, XYAnnotationsLayerConfig, XYLayerConfig } from './types'; export const isAnnotationsLayer = ( layer: Pick diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts index 5b8b7832730b9..54975d08b8486 100644 --- a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts +++ b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts @@ -21,6 +21,7 @@ import { getColumnsFromVis } from './schemas'; const mockConvertMetricToColumns = jest.fn(); const mockConvertBucketToColumns = jest.fn(); const mockGetCutomBucketsFromSiblingAggs = jest.fn(); +const mockGetCustomBucketColumns = jest.fn(); const mockGetVisSchemas = jest.fn(); const mockGetBucketCollapseFn = jest.fn(); @@ -55,6 +56,7 @@ jest.mock('./utils', () => ({ getMetricsWithoutDuplicates: jest.fn(() => mockGetMetricsWithoutDuplicates()), isValidVis: jest.fn(() => mockIsValidVis()), sortColumns: jest.fn(() => mockSortColumns()), + getCustomBucketColumns: jest.fn(() => mockGetCustomBucketColumns()), })); describe('getColumnsFromVis', () => { @@ -73,6 +75,7 @@ describe('getColumnsFromVis', () => { jest.clearAllMocks(); mockGetVisSchemas.mockReturnValue({}); mockIsValidVis.mockReturnValue(true); + mockGetCustomBucketColumns.mockReturnValue({ customBucketColumns: [], customBucketsMap: {} }); }); test('should return null if vis is not valid', () => { @@ -107,7 +110,10 @@ describe('getColumnsFromVis', () => { test('should return null if one sibling agg was provided and it is not supported', () => { const buckets: AggConfig[] = [aggConfig]; mockGetCutomBucketsFromSiblingAggs.mockReturnValue(buckets); - mockConvertBucketToColumns.mockReturnValue(null); + mockGetCustomBucketColumns.mockReturnValue({ + customBucketColumns: [null], + customBucketsMap: {}, + }); mockGetMetricsWithoutDuplicates.mockReturnValue([{}]); const result = getColumnsFromVis(vis, dataServiceMock.query.timefilter.timefilter, dataView, { @@ -120,7 +126,7 @@ describe('getColumnsFromVis', () => { expect(mockIsValidVis).toBeCalledTimes(1); expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1); expect(mockGetMetricsWithoutDuplicates).toBeCalledTimes(1); - expect(mockConvertBucketToColumns).toBeCalledTimes(1); + expect(mockGetCustomBucketColumns).toBeCalledTimes(1); expect(mockGetBucketColumns).toBeCalledTimes(0); }); @@ -190,7 +196,7 @@ describe('getColumnsFromVis', () => { expect(mockSortColumns).toBeCalledTimes(0); }); - test('should return columns', () => { + test('should return one layer with columns', () => { const buckets: AggConfig[] = [aggConfig]; const bucketColumns = [ { @@ -238,13 +244,18 @@ describe('getColumnsFromVis', () => { buckets: [], }); - expect(result).toEqual({ - bucketCollapseFn, - buckets: [bucketId], - columns: [...metrics, ...buckets], - columnsWithoutReferenced, - metrics: [metricId], - }); + expect(result).toEqual([ + { + bucketCollapseFn, + buckets: { + all: [bucketId], + customBuckets: {}, + }, + columns: [...metrics, ...buckets], + columnsWithoutReferenced, + metrics: [metricId], + }, + ]); expect(mockGetVisSchemas).toBeCalledTimes(1); expect(mockIsValidVis).toBeCalledTimes(1); expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1); @@ -254,4 +265,84 @@ describe('getColumnsFromVis', () => { expect(mockSortColumns).toBeCalledTimes(1); expect(mockGetColumnsWithoutReferenced).toBeCalledTimes(1); }); + + test('should return several layer with columns if series is provided', () => { + const buckets: AggConfig[] = [aggConfig]; + const bucketColumns = [ + { + sourceField: 'some-field', + columnId: 'col3', + operationType: 'date_histogram', + isBucketed: false, + isSplit: false, + dataType: 'string', + params: { interval: '1h' }, + meta: { aggId: 'agg-id-1' }, + }, + ]; + const mectricAggs = [{ aggId: 'col-id-3' }, { aggId: 'col-id-4' }]; + const metrics = [ + { + sourceField: 'some-field', + columnId: 'col2', + operationType: 'max', + isBucketed: false, + isSplit: false, + dataType: 'string', + params: {}, + meta: { aggId: 'col-id-3' }, + }, + { + sourceField: 'some-field', + columnId: 'col3', + operationType: 'max', + isBucketed: false, + isSplit: false, + dataType: 'string', + params: {}, + meta: { aggId: 'col-id-4' }, + }, + ]; + + const columnsWithoutReferenced = ['col2']; + const metricId = 'metric1'; + const bucketId = 'bucket1'; + const bucketCollapseFn = 'max'; + + mockGetCutomBucketsFromSiblingAggs.mockReturnValue([]); + mockGetMetricsWithoutDuplicates.mockReturnValue(mectricAggs); + mockConvertMetricToColumns.mockReturnValue(metrics); + mockConvertBucketToColumns.mockReturnValue(bucketColumns); + mockGetBucketColumns.mockReturnValue(bucketColumns); + mockGetColumnsWithoutReferenced.mockReturnValue(columnsWithoutReferenced); + mockSortColumns.mockReturnValue([...metrics, ...buckets]); + mockGetColumnIds.mockReturnValueOnce([metricId]); + mockGetColumnIds.mockReturnValueOnce([bucketId]); + mockGetColumnIds.mockReturnValueOnce([metricId]); + mockGetColumnIds.mockReturnValueOnce([bucketId]); + mockGetBucketCollapseFn.mockReturnValueOnce(bucketCollapseFn); + mockGetBucketCollapseFn.mockReturnValueOnce(bucketCollapseFn); + + const result = getColumnsFromVis( + vis, + dataServiceMock.query.timefilter.timefilter, + dataView, + { + splits: [], + buckets: [], + }, + undefined, + [{ metrics: ['col-id-3'] }, { metrics: ['col-id-4'] }] + ); + + expect(result?.length).toEqual(2); + expect(mockGetVisSchemas).toBeCalledTimes(1); + expect(mockIsValidVis).toBeCalledTimes(1); + expect(mockGetCutomBucketsFromSiblingAggs).toBeCalledTimes(1); + expect(mockGetMetricsWithoutDuplicates).toBeCalledTimes(1); + expect(mockConvertMetricToColumns).toBeCalledTimes(2); + expect(mockGetBucketColumns).toBeCalledTimes(4); + expect(mockSortColumns).toBeCalledTimes(2); + expect(mockGetColumnsWithoutReferenced).toBeCalledTimes(2); + }); }); diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.ts index ecfbbf34ad9c9..3a225e540faae 100644 --- a/src/plugins/visualizations/public/convert_to_lens/schemas.ts +++ b/src/plugins/visualizations/public/convert_to_lens/schemas.ts @@ -7,16 +7,17 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import { METRIC_TYPES, TimefilterContract } from '@kbn/data-plugin/public'; +import { IAggConfig, METRIC_TYPES, TimefilterContract } from '@kbn/data-plugin/public'; import { AggBasedColumn, PercentageModeConfig, SchemaConfig } from '../../common'; import { convertMetricToColumns } from '../../common/convert_to_lens/lib/metrics'; -import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets'; import { getCustomBucketsFromSiblingAggs } from '../../common/convert_to_lens/lib/utils'; +import { BucketColumn } from '../../common/convert_to_lens/lib'; import type { Vis } from '../types'; import { getVisSchemas, Schemas } from '../vis_schemas'; import { getBucketCollapseFn, getBucketColumns, + getCustomBucketColumns, getColumnIds, getColumnsWithoutReferenced, getMetricsWithoutDuplicates, @@ -31,66 +32,44 @@ const areVisSchemasValid = (visSchemas: Schemas, unsupported: Array( - vis: Vis, - timefilter: TimefilterContract, +const createLayer = ( + visSchemas: Schemas, + allMetrics: Array>, + metricsForLayer: Array>, + customBucketsWithMetricIds: Array<{ + customBucket: IAggConfig; + metricIds: string[]; + }>, dataView: DataView, { splits = [], buckets = [], - unsupported = [], }: { splits?: Array; buckets?: Array; - unsupported?: Array; } = {}, - config?: { - dropEmptyRowsInDateHistogram?: boolean; - } & (PercentageModeConfig | void) + percentageModeConfig: PercentageModeConfig, + dropEmptyRowsInDateHistogram?: boolean ) => { - const { dropEmptyRowsInDateHistogram, ...percentageModeConfig } = config ?? { - isPercentageMode: false, - }; - const visSchemas = getVisSchemas(vis, { - timefilter, - timeRange: timefilter.getAbsoluteTime(), - }); - - if (!isValidVis(visSchemas) || !areVisSchemasValid(visSchemas, unsupported)) { - return null; - } - - const customBuckets = getCustomBucketsFromSiblingAggs(visSchemas.metric); - - // doesn't support sibbling pipeline aggs with different bucket aggs - if (customBuckets.length > 1) { + const metricColumns = metricsForLayer.flatMap((m) => + convertMetricToColumns(m, dataView, allMetrics, percentageModeConfig) + ); + if (metricColumns.includes(null)) { return null; } + const metricColumnsWithoutNull = metricColumns as AggBasedColumn[]; - const metricsWithoutDuplicates = getMetricsWithoutDuplicates(visSchemas.metric); - const aggs = metricsWithoutDuplicates as Array>; - - const metricColumns = aggs.flatMap((m) => - convertMetricToColumns(m, dataView, aggs, percentageModeConfig) + const { customBucketColumns, customBucketsMap } = getCustomBucketColumns( + customBucketsWithMetricIds, + metricColumnsWithoutNull, + dataView, + allMetrics, + dropEmptyRowsInDateHistogram ); - if (metricColumns.includes(null)) { + if (customBucketColumns.includes(null)) { return null; } - const metrics = metricColumns as AggBasedColumn[]; - const customBucketColumns = []; - - if (customBuckets.length) { - const customBucketColumn = convertBucketToColumns( - { agg: customBuckets[0], dataView, metricColumns: metrics, aggs }, - false, - dropEmptyRowsInDateHistogram - ); - if (!customBucketColumn) { - return null; - } - customBucketColumns.push(customBucketColumn); - } const bucketColumns = getBucketColumns( visSchemas, @@ -117,19 +96,121 @@ export const getColumnsFromVis = ( } const columns = sortColumns( - [...metrics, ...bucketColumns, ...splitBucketColumns, ...customBucketColumns], + [ + ...metricColumnsWithoutNull, + ...bucketColumns, + ...splitBucketColumns, + ...(customBucketColumns as BucketColumn[]), + ], visSchemas, [...buckets, ...splits], - metricsWithoutDuplicates + metricsForLayer ); const columnsWithoutReferenced = getColumnsWithoutReferenced(columns); return { metrics: getColumnIds(columnsWithoutReferenced.filter((с) => !с.isBucketed)), - buckets: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)), - bucketCollapseFn: getBucketCollapseFn(visSchemas.metric, customBucketColumns), + buckets: { + all: getColumnIds(columnsWithoutReferenced.filter((c) => c.isBucketed)), + customBuckets: customBucketsMap, + }, + bucketCollapseFn: getBucketCollapseFn( + visSchemas.metric, + customBucketColumns as BucketColumn[], + customBucketsMap, + metricColumnsWithoutNull + ), columnsWithoutReferenced, columns, }; }; + +export const getColumnsFromVis = ( + vis: Vis, + timefilter: TimefilterContract, + dataView: DataView, + { + splits = [], + buckets = [], + unsupported = [], + }: { + splits?: Array; + buckets?: Array; + unsupported?: Array; + } = {}, + config?: { + dropEmptyRowsInDateHistogram?: boolean; + supportMixedSiblingPipelineAggs?: boolean; + } & (PercentageModeConfig | void), + series?: Array<{ metrics: string[] }> +) => { + const { dropEmptyRowsInDateHistogram, supportMixedSiblingPipelineAggs, ...percentageModeConfig } = + config ?? { + isPercentageMode: false, + }; + const visSchemas = getVisSchemas(vis, { + timefilter, + timeRange: timefilter.getAbsoluteTime(), + }); + + if ( + !isValidVis(visSchemas, supportMixedSiblingPipelineAggs) || + !areVisSchemasValid(visSchemas, unsupported) + ) { + return null; + } + + const customBucketsWithMetricIds = getCustomBucketsFromSiblingAggs(visSchemas.metric); + + // doesn't support sibbling pipeline aggs with different bucket aggs + if (!supportMixedSiblingPipelineAggs && customBucketsWithMetricIds.length > 1) { + return null; + } + + const metricsWithoutDuplicates = getMetricsWithoutDuplicates(visSchemas.metric); + const aggs = metricsWithoutDuplicates as Array>; + const layers = []; + + if (series && series.length) { + for (const { metrics: metricAggIds } of series) { + const metrics = aggs.filter( + (agg) => agg.aggId && metricAggIds.includes(agg.aggId.split('.')[0]) + ); + const customBucketsForLayer = customBucketsWithMetricIds.filter((c) => + c.metricIds.some((m) => metricAggIds.includes(m)) + ); + const layer = createLayer( + visSchemas, + aggs, + metrics, + customBucketsForLayer, + dataView, + { splits, buckets }, + percentageModeConfig, + dropEmptyRowsInDateHistogram + ); + if (!layer) { + return null; + } + layers.push(layer); + } + } else { + const layer = createLayer( + visSchemas, + aggs, + aggs, + customBucketsWithMetricIds, + dataView, + { splits, buckets }, + percentageModeConfig, + dropEmptyRowsInDateHistogram + ); + if (!layer) { + return null; + } + layers.push(layer); + } + + return layers; +}; diff --git a/src/plugins/visualizations/public/convert_to_lens/utils.test.ts b/src/plugins/visualizations/public/convert_to_lens/utils.test.ts index 734a250a2972c..50f667430a8cb 100644 --- a/src/plugins/visualizations/public/convert_to_lens/utils.test.ts +++ b/src/plugins/visualizations/public/convert_to_lens/utils.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { BUCKET_TYPES, METRIC_TYPES } from '@kbn/data-plugin/common'; +import { BUCKET_TYPES, IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common'; import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { AggBasedColumn, @@ -27,6 +27,7 @@ import { getBucketColumns, getColumnIds, getColumnsWithoutReferenced, + getCustomBucketColumns, getMetricsWithoutDuplicates, isReferenced, isValidVis, @@ -104,6 +105,7 @@ describe('getColumnsWithoutReferenced', () => { describe('getBucketCollapseFn', () => { const metric1: SchemaConfig = { accessor: 0, + aggId: '1', label: '', format: { id: undefined, @@ -115,21 +117,25 @@ describe('getBucketCollapseFn', () => { const metric2: SchemaConfig = { ...metric1, + aggId: '2', aggType: METRIC_TYPES.AVG_BUCKET, }; const metric3: SchemaConfig = { ...metric1, + aggId: '3', aggType: METRIC_TYPES.MAX_BUCKET, }; const metric4: SchemaConfig = { ...metric1, + aggId: '4', aggType: METRIC_TYPES.MIN_BUCKET, }; const metric5: SchemaConfig = { ...metric1, + aggId: '5', aggType: METRIC_TYPES.SUM_BUCKET, }; @@ -151,18 +157,54 @@ describe('getBucketCollapseFn', () => { test.each< [ string, - [Array>, AggBasedColumn[]], - Record + [ + Array>, + AggBasedColumn[], + Record, + AggBasedColumn[] + ], + Record ] >([ - ['avg', [[metric1, metric2], [customBucketColum]], { [customBucketColum.columnId]: 'avg' }], - ['max', [[metric1, metric3], [customBucketColum]], { [customBucketColum.columnId]: 'max' }], - ['min', [[metric1, metric4], [customBucketColum]], { [customBucketColum.columnId]: 'min' }], - ['sum', [[metric1, metric5], [customBucketColum]], { [customBucketColum.columnId]: 'sum' }], [ - 'undefined if no sibling pipeline agg is provided', - [[metric1], [customBucketColum]], - { [customBucketColum.columnId]: undefined }, + 'avg', + [ + [metric1, metric2], + [customBucketColum], + { test: 'bucket-1' }, + [{ columnId: 'test', meta: { aggId: metric2.aggId } } as AggBasedColumn], + ], + { sum: [], min: [], max: [], avg: [customBucketColum.columnId] }, + ], + [ + 'max', + [ + [metric1, metric3], + [customBucketColum], + { test: 'bucket-1' }, + [{ columnId: 'test', meta: { aggId: metric3.aggId } } as AggBasedColumn], + ], + { sum: [], min: [], max: [customBucketColum.columnId], avg: [] }, + ], + [ + 'min', + [ + [metric1, metric4], + [customBucketColum], + { test: 'bucket-1' }, + [{ columnId: 'test', meta: { aggId: metric4.aggId } } as AggBasedColumn], + ], + { sum: [], min: [customBucketColum.columnId], max: [], avg: [] }, + ], + [ + 'sum', + [ + [metric1, metric5], + [customBucketColum], + { test: 'bucket-1' }, + [{ columnId: 'test', meta: { aggId: metric5.aggId } } as AggBasedColumn], + ], + { sum: [customBucketColum.columnId], min: [], max: [], avg: [] }, ], ])('should return%s', (_, input, expected) => { expect(getBucketCollapseFn(...input)).toEqual(expected); @@ -607,4 +649,77 @@ describe('getColumnIds', () => { colId4, ]); }); + + describe('getCustomBucketColumns', () => { + const dataView = stubLogstashDataView; + const baseMetric = { + accessor: 0, + label: '', + format: { + id: undefined, + params: undefined, + }, + params: {}, + }; + const metric1: SchemaConfig = { + ...baseMetric, + accessor: 2, + aggType: METRIC_TYPES.COUNT, + aggId: '3', + }; + const metric2: SchemaConfig = { + ...baseMetric, + accessor: 3, + aggType: METRIC_TYPES.MAX, + aggId: '4', + }; + const customBucketsWithMetricIds = [ + { + customBucket: {} as IAggConfig, + metricIds: ['3', '4'], + }, + { + customBucket: {} as IAggConfig, + metricIds: ['5'], + }, + ]; + test('return custom buckets columns and map', () => { + mockConvertBucketToColumns.mockReturnValueOnce({ + columnId: 'col-1', + operationType: 'date_histogram', + }); + mockConvertBucketToColumns.mockReturnValueOnce({ + columnId: 'col-2', + operationType: 'terms', + }); + expect( + getCustomBucketColumns( + customBucketsWithMetricIds, + [ + { columnId: 'col-3', meta: { aggId: '3' } }, + { columnId: 'col-4', meta: { aggId: '4' } }, + { columnId: 'col-5', meta: { aggId: '5' } }, + ] as AggBasedColumn[], + dataView, + [metric1, metric2] + ) + ).toEqual({ + customBucketColumns: [ + { + columnId: 'col-1', + operationType: 'date_histogram', + }, + { + columnId: 'col-2', + operationType: 'terms', + }, + ], + customBucketsMap: { + 'col-3': 'col-1', + 'col-4': 'col-1', + 'col-5': 'col-2', + }, + }); + }); + }); }); diff --git a/src/plugins/visualizations/public/convert_to_lens/utils.ts b/src/plugins/visualizations/public/convert_to_lens/utils.ts index a5337568b5568..0cab4f698fb2f 100644 --- a/src/plugins/visualizations/public/convert_to_lens/utils.ts +++ b/src/plugins/visualizations/public/convert_to_lens/utils.ts @@ -7,10 +7,11 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; -import { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/public'; import { AggBasedColumn, SchemaConfig, SupportedAggregation } from '../../common'; import { convertBucketToColumns } from '../../common/convert_to_lens/lib/buckets'; import { isSiblingPipeline } from '../../common/convert_to_lens/lib/utils'; +import { BucketColumn } from '../../common/convert_to_lens/lib'; import { Schemas } from '../vis_schemas'; export const isReferenced = (columnId: string, references: string[]) => @@ -25,14 +26,31 @@ export const getColumnsWithoutReferenced = (columns: AggBasedColumn[]) => { export const getBucketCollapseFn = ( metrics: Array>, - customBucketColumns: AggBasedColumn[] + customBucketColumns: AggBasedColumn[], + customBucketsMap: Record, + metricColumns: AggBasedColumn[] ) => { - const collapseFn = metrics.find((m) => isSiblingPipeline(m))?.aggType.split('_')[0]; - return customBucketColumns.length - ? { - [customBucketColumns[0].columnId]: collapseFn, + const collapseFnMap: Record = { + min: [], + max: [], + sum: [], + avg: [], + }; + customBucketColumns.forEach((bucket) => { + const metricColumnsIds = Object.keys(customBucketsMap).filter( + (key) => customBucketsMap[key] === bucket.columnId + ); + metricColumnsIds.forEach((metricColumnsId) => { + const metricColumn = metricColumns.find((c) => c.columnId === metricColumnsId)!; + const collapseFn = metrics + .find((m) => m.aggId === metricColumn.meta.aggId) + ?.aggType.split('_')[0]; + if (collapseFn) { + collapseFnMap[collapseFn].push(bucket.columnId); } - : {}; + }); + }); + return collapseFnMap; }; export const getBucketColumns = ( @@ -67,7 +85,7 @@ export const getBucketColumns = ( return columns; }; -export const isValidVis = (visSchemas: Schemas) => { +export const isValidVis = (visSchemas: Schemas, supportMixedSiblingPipelineAggs?: boolean) => { const { metric } = visSchemas; const siblingPipelineAggs = metric.filter((m) => isSiblingPipeline(m)); @@ -76,7 +94,10 @@ export const isValidVis = (visSchemas: Schemas) => { } // doesn't support mixed sibling pipeline aggregations - if (siblingPipelineAggs.some((agg) => agg.aggType !== siblingPipelineAggs[0].aggType)) { + if ( + siblingPipelineAggs.some((agg) => agg.aggType !== siblingPipelineAggs[0].aggType) && + !supportMixedSiblingPipelineAggs + ) { return false; } @@ -103,7 +124,8 @@ export const sortColumns = ( ...acc, ...(key === 'metric' ? metricsWithoutDuplicates : visSchemas[key])?.reduce( (newAcc, schema) => { - newAcc[schema.aggId] = schema.accessor; + // metrics should always have sort more than buckets + newAcc[schema.aggId] = key === 'metric' ? schema.accessor : 1000 + schema.accessor; return newAcc; }, {} @@ -121,3 +143,36 @@ export const sortColumns = ( }; export const getColumnIds = (columns: AggBasedColumn[]) => columns.map(({ columnId }) => columnId); + +export const getCustomBucketColumns = ( + customBucketsWithMetricIds: Array<{ + customBucket: IAggConfig; + metricIds: string[]; + }>, + metricColumns: AggBasedColumn[], + dataView: DataView, + aggs: Array>, + dropEmptyRowsInDateHistogram?: boolean +) => { + const customBucketColumns: Array = []; + const customBucketsMap: Record = {}; + customBucketsWithMetricIds.forEach((customBucketWithMetricIds) => { + const customBucketColumn = convertBucketToColumns( + { agg: customBucketWithMetricIds.customBucket, dataView, metricColumns, aggs }, + true, + dropEmptyRowsInDateHistogram + ); + customBucketColumns.push(customBucketColumn); + if (customBucketColumn) { + customBucketWithMetricIds.metricIds.forEach((metricAggId) => { + const metricColumnId = metricColumns.find( + (metricColumn) => metricColumn?.meta.aggId === metricAggId + )?.columnId; + if (metricColumnId) { + customBucketsMap[metricColumnId] = customBucketColumn.columnId; + } + }); + } + }); + return { customBucketColumns, customBucketsMap }; +}; diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts index cc28ead0c55cd..39357058e7aff 100644 --- a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Agg based Vis to Lens', function () { loadTestFile(require.resolve('./pie')); loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./xy')); loadTestFile(require.resolve('./gauge')); loadTestFile(require.resolve('./goal')); }); diff --git a/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/xy.ts b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/xy.ts new file mode 100644 index 0000000000000..161a3549e856c --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/open_in_lens/agg_based/xy.ts @@ -0,0 +1,73 @@ +/* + * 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, visEditor, lens, timePicker, header, visChart } = getPageObjects([ + 'visualize', + 'lens', + 'visEditor', + 'timePicker', + 'header', + 'visChart', + ]); + + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('XY', function describeIndexTests() { + const isNewChartsLibraryEnabled = true; + + before(async () => { + await visualize.initTests(isNewChartsLibraryEnabled); + }); + + beforeEach(async () => { + await visualize.navigateToNewAggBasedVisualization(); + await visualize.clickLineChart(); + await visualize.clickNewSearch(); + await timePicker.setDefaultAbsoluteRange(); + }); + + it('should show the "Edit Visualization in Lens" menu item', async () => { + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(true); + }); + + it('should hide the "Edit Visualization in Lens" menu item if dot size aggregation is defined', async () => { + await visEditor.clickBucket('Dot size', 'metrics'); + await visEditor.selectAggregation('Max', 'metrics'); + await visEditor.selectField('memory', 'metrics'); + await visEditor.clickGo(isNewChartsLibraryEnabled); + const button = await testSubjects.exists('visualizeEditInLensButton'); + expect(button).to.eql(false); + }); + + it('should convert to Lens', async () => { + await visEditor.clickBucket('Split series'); + await visEditor.selectAggregation('Terms'); + await visEditor.selectField('machine.os.raw'); + await header.waitUntilLoadingHasFinished(); + await visEditor.clickGo(isNewChartsLibraryEnabled); + const expectedData = await visChart.getLegendEntriesXYCharts('xyVisChart'); + + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('xyVisChart'); + const data = await lens.getCurrentChartDebugState('xyVisChart'); + await retry.try(async () => { + const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); + expect(dimensions).to.have.length(2); + expect(await dimensions[0].getVisibleText()).to.be('Count'); + expect(await dimensions[1].getVisibleText()).to.be('machine.os.raw: Descending'); + }); + expect(data?.legend?.items.map((item) => item.name)).to.eql(expectedData); + }); + }); +}