diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.test.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.test.ts index 06817bd018298..c7c0fa17bf1b3 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.test.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.test.ts @@ -498,6 +498,7 @@ describe('Metric Schema', () => { }, background_chart: { type: 'trend', + time_field: '@timestamp', }, }, { diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.ts index ef15af8601c62..a04aa1b4cd3e6 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/metric.ts @@ -72,6 +72,7 @@ export const complementaryVizSchemaNoESQL = schema.oneOf([ }), schema.object({ type: schema.literal('trend'), + time_field: schema.string({ meta: { description: 'Time field for trend chart' } }), }), ]); diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_api_config.mock.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_api_config.mock.ts index 709657dd596e5..91b6ba521b014 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_api_config.mock.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_api_config.mock.ts @@ -177,3 +177,67 @@ export const metricAPIWithTermsRankedBySecondary = { }, }, } as MetricState; + +export const trendlineMetricAPIAttributes = { + type: 'metric', + title: 'Metric - Trendline', + description: 'Metric with trendline background chart', + dataset: { type: 'dataView', id: 'testId' }, + metrics: [ + { + type: 'primary', + operation: 'count', + empty_as_null: true, + background_chart: { + type: 'trend', + time_field: 'timestamp', + }, + }, + ], +} as MetricState; + +export const trendlineWithSecondaryMetricAPIAttributes = { + type: 'metric', + title: 'Metric - Trendline with Secondary', + description: 'Metric with trendline and secondary metric', + dataset: { type: 'dataView', id: 'testId' }, + metrics: [ + { + type: 'primary', + operation: 'count', + empty_as_null: true, + background_chart: { + type: 'trend', + time_field: 'timestamp', + }, + }, + { + type: 'secondary', + operation: 'average', + field: 'bytes', + }, + ], +} as MetricState; + +export const trendlineWithBreakdownMetricAPIAttributes = { + type: 'metric', + title: 'Metric - Trendline with Breakdown', + description: 'Metric with trendline and breakdown', + dataset: { type: 'dataView', id: 'testId' }, + metrics: [ + { + type: 'primary', + operation: 'count', + empty_as_null: true, + background_chart: { + type: 'trend', + time_field: 'timestamp', + }, + }, + ], + breakdown_by: { + operation: 'terms', + fields: ['extension.keyword'], + size: 5, + }, +} as MetricState; diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_state_config.mock.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_state_config.mock.ts index 00cacba319e3a..ac5d950e1d41a 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_state_config.mock.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/lens_state_config.mock.ts @@ -10,6 +10,7 @@ import type { AvgIndexPatternColumn, CountIndexPatternColumn, + DateHistogramIndexPatternColumn, FormulaIndexPatternColumn, MathIndexPatternColumn, MedianIndexPatternColumn, @@ -490,3 +491,113 @@ export const breakdownMetricWithFormulaRefColumnsAttributes: LensAttributes = { adHocDataViews: {}, }, }; + +/** + * Metric with trendline generated from kibana + */ +export const trendlineMetricAttributes: LensAttributes = { + title: 'Metric - Trendline', + description: 'Metric with trendline background chart', + visualizationType: 'lnsMetric', + references: [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'indexpattern-datasource-layer-29b51bd9-2fdc-43ff-ad5b-84361c410ff8', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'indexpattern-datasource-layer-77aaea1d-fb58-42d2-bb5c-b7ac363316dd', + }, + ], + state: { + visualization: { + layerId: '29b51bd9-2fdc-43ff-ad5b-84361c410ff8', + layerType: 'data', + metricAccessor: 'ede0ece3-1093-4110-8672-ecf7e1724ccb', + showBar: false, + applyColorTo: 'background', + trendlineLayerId: '77aaea1d-fb58-42d2-bb5c-b7ac363316dd', + trendlineLayerType: 'metricTrendline', + trendlineTimeAccessor: 'ce401516-4d0c-42d0-a2c6-e13a58c17820', + trendlineMetricAccessor: '271f4e55-845f-4d4d-87ec-ea61df913678', + secondaryTrend: { + type: 'none', + }, + secondaryLabelPosition: 'before', + }, + query: { + query: '', + language: 'kuery', + }, + filters: [], + datasourceStates: { + formBased: { + layers: { + '29b51bd9-2fdc-43ff-ad5b-84361c410ff8': { + columns: { + 'ede0ece3-1093-4110-8672-ecf7e1724ccb': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + sourceField: '___records___', + params: { + emptyAsNull: true, + }, + } as CountIndexPatternColumn, + }, + columnOrder: ['ede0ece3-1093-4110-8672-ecf7e1724ccb'], + incompleteColumns: {}, + sampling: 1, + }, + '77aaea1d-fb58-42d2-bb5c-b7ac363316dd': { + linkToLayers: ['29b51bd9-2fdc-43ff-ad5b-84361c410ff8'], + columns: { + 'ce401516-4d0c-42d0-a2c6-e13a58c17820': { + label: 'timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'timestamp', + isBucketed: true, + params: { + interval: 'auto', + includeEmptyRows: true, + dropPartials: false, + }, + } as DateHistogramIndexPatternColumn, + '271f4e55-845f-4d4d-87ec-ea61df913678': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + sourceField: '___records___', + params: { + emptyAsNull: true, + }, + } as CountIndexPatternColumn, + }, + columnOrder: [ + 'ce401516-4d0c-42d0-a2c6-e13a58c17820', + '271f4e55-845f-4d4d-87ec-ea61df913678', + ], + sampling: 1, + ignoreGlobalFilters: false, + incompleteColumns: {}, + }, + }, + }, + // @ts-expect-error + indexpattern: { + layers: {}, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + version: 2, +}; diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/metric.test.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/metric.test.ts index 40700c0de443e..4e872e467331c 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/metric.test.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/metric/metric.test.ts @@ -14,6 +14,7 @@ import { breakdownMetricAttributes, complexMetricAttributes, breakdownMetricWithFormulaRefColumnsAttributes, + trendlineMetricAttributes, } from './lens_state_config.mock'; import { simpleMetricAPIAttributes, @@ -21,6 +22,9 @@ import { complexMetricAPIAttributes, complexESQLMetricAPIAttributes, metricAPIWithTermsRankedBySecondary, + trendlineMetricAPIAttributes, + trendlineWithSecondaryMetricAPIAttributes, + trendlineWithBreakdownMetricAPIAttributes, } from './lens_api_config.mock'; describe('Metric', () => { @@ -36,6 +40,10 @@ describe('Metric', () => { it('should convert a breakdown-by metric', () => { validateConverter(breakdownMetricAttributes, metricStateSchema); }); + + it('should convert a metric with trendline', () => { + validateConverter(trendlineMetricAttributes, metricStateSchema); + }); }); describe('validateAPIConverter', () => { it('should convert a simple metric', () => { @@ -57,6 +65,18 @@ describe('Metric', () => { it('should convert a metric with a terms agg ranked by secondary metric', () => { validateAPIConverter(metricAPIWithTermsRankedBySecondary, metricStateSchema); }); + + it('should convert a metric with trendline', () => { + validateAPIConverter(trendlineMetricAPIAttributes, metricStateSchema); + }); + + it('should convert a metric with trendline and secondary metric', () => { + validateAPIConverter(trendlineWithSecondaryMetricAPIAttributes, metricStateSchema); + }); + + it('should convert a metric with trendline and breakdown', () => { + validateAPIConverter(trendlineWithBreakdownMetricAPIAttributes, metricStateSchema); + }); }); it('should convert a breakdown-by metric with formula reference columns and rank_by in the terms bucket operation', () => { diff --git a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/transforms/charts/metric.ts b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/transforms/charts/metric.ts index 659586f3d70ec..e4e2d45c71e22 100644 --- a/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/transforms/charts/metric.ts +++ b/src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/transforms/charts/metric.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { DateHistogramIndexPatternColumn } from '@kbn/lens-common'; import { LENS_METRIC_BREAKDOWN_DEFAULT_MAX_COLUMNS, LENS_METRIC_STATE_DEFAULTS, @@ -35,7 +36,10 @@ import { fromBucketLensApiToLensState } from '../columns/buckets'; import { getValueApiColumn, getValueColumn } from '../columns/esql_column'; import type { MetricState } from '../../schema'; import { fromMetricAPItoLensState } from '../columns/metric'; -import type { LensApiBucketOperations } from '../../schema/bucket_ops'; +import type { + LensApiBucketOperations, + LensApiDateHistogramOperation, +} from '../../schema/bucket_ops'; import { generateLayer } from '../utils'; import type { MetricStateESQL, @@ -276,6 +280,7 @@ function buildFromTextBasedLayer( function buildFromFormBasedLayer( layer: PersistedIndexPatternLayer, + trendlineLayer: PersistedIndexPatternLayer | undefined, metricAccessor: string, visualization: MetricVisualizationState ): WritableMetricStateWithoutDataset { @@ -284,6 +289,13 @@ function buildFromFormBasedLayer( throw Error('The primary metric must refer to a metric operation.'); } + let trendlineTimeField: string | undefined; + if (trendlineLayer && visualization.trendlineTimeAccessor) { + const trendlineTimeColumn = trendlineLayer.columns[visualization.trendlineTimeAccessor]; + trendlineTimeField = (trendlineTimeColumn as DateHistogramIndexPatternColumn | undefined) + ?.sourceField; + } + const maxValue = visualization.maxAccessor ? operationFromColumn(visualization.maxAccessor, layer) : undefined; @@ -347,13 +359,15 @@ function buildFromFormBasedLayer( } : {}), }, - visualization + visualization, + trendlineTimeField ); } function enrichConfigurationWithVisualizationProperties( state: WritableMetricStateWithoutDataset, - visualization: MetricVisualizationState + visualization: MetricVisualizationState, + trendlineTimeField?: string ): WritableMetricStateWithoutDataset { const [primaryMetric, secondaryMetric] = state.metrics; @@ -368,8 +382,8 @@ function enrichConfigurationWithVisualizationProperties( primaryMetric.sub_label = visualization.subtitle; } - if (visualization.trendlineLayerType) { - primaryMetric.background_chart = { ...primaryMetric.background_chart, type: 'trend' }; + if (visualization.trendlineLayerType && trendlineTimeField) { + primaryMetric.background_chart = { type: 'trend', time_field: trendlineTimeField }; } if (visualization.color) { @@ -461,6 +475,7 @@ function reverseBuildVisualizationState( visualization: MetricVisualizationState, layer: PersistedIndexPatternLayer | TextBasedLayer, layerId: string, + trendlineLayer: PersistedIndexPatternLayer | undefined, adHocDataViews: Record, references: SavedObjectReference[], adhocReferences?: SavedObjectReference[] @@ -480,7 +495,7 @@ function reverseBuildVisualizationState( dataset: dataset satisfies MetricState['dataset'], ...(isTextBasedLayer(layer) ? buildFromTextBasedLayer(layer, metricAccessor, visualization) - : buildFromFormBasedLayer(layer, metricAccessor, visualization)), + : buildFromFormBasedLayer(layer, trendlineLayer, metricAccessor, visualization)), } as MetricState; } @@ -490,6 +505,7 @@ function buildFormBasedLayer(layer: MetricStateNoESQL): FormBasedPersistedState[ throw Error('The primary metric must refer to a metric operation.'); } const newPrimaryColumns = fromMetricAPItoLensState(primaryMetric); + const newSecondaryColumns = secondaryMetric ? fromMetricAPItoLensState(secondaryMetric) : undefined; @@ -502,16 +518,26 @@ function buildFormBasedLayer(layer: MetricStateNoESQL): FormBasedPersistedState[ }; const defaultLayer = layers[DEFAULT_LAYER_ID]; - const trendLineLayer = layers[TRENDLINE_LAYER_ID]; + const trendlineLayer = layers[TRENDLINE_LAYER_ID]; - if (trendLineLayer) { - trendLineLayer.linkToLayers = [DEFAULT_LAYER_ID]; + if (trendlineLayer) { + trendlineLayer.linkToLayers = [DEFAULT_LAYER_ID]; } addLayerColumn(defaultLayer, getAccessorName('metric'), newPrimaryColumns); - if (trendLineLayer) { - addLayerColumn(trendLineLayer, `${ACCESSOR}_trendline`, newPrimaryColumns); - addLayerColumn(trendLineLayer, HISTOGRAM_COLUMN_NAME, newPrimaryColumns); + + if (trendlineLayer && primaryMetric.background_chart?.type === 'trend') { + const op: LensApiDateHistogramOperation = { + field: primaryMetric.background_chart.time_field, + operation: 'date_histogram', + include_empty_rows: true, + suggested_interval: 'auto', + use_original_time_range: false, + }; + const trendlineColumn = fromBucketLensApiToLensState(op, []); + + addLayerColumn(trendlineLayer, `${ACCESSOR}_trendline`, newPrimaryColumns); + addLayerColumn(trendlineLayer, HISTOGRAM_COLUMN_NAME, trendlineColumn, true); } if (layer.breakdown_by) { @@ -528,16 +554,17 @@ function buildFormBasedLayer(layer: MetricStateNoESQL): FormBasedPersistedState[ ); addLayerColumn(defaultLayer, columnName, breakdownColumn, true); - if (trendLineLayer) { - addLayerColumn(trendLineLayer, `${columnName}_trendline`, breakdownColumn, true); + if (trendlineLayer) { + addLayerColumn(trendlineLayer, `${columnName}_trendline`, breakdownColumn, true); } } if (newSecondaryColumns?.length) { const columnName = getAccessorName('secondary'); addLayerColumn(defaultLayer, columnName, newSecondaryColumns); - if (trendLineLayer) { - addLayerColumn(trendLineLayer, `${columnName}_trendline`, newSecondaryColumns, false, 'X0'); + + if (trendlineLayer) { + addLayerColumn(trendlineLayer, `${columnName}_trendline`, newSecondaryColumns); } } @@ -546,9 +573,6 @@ function buildFormBasedLayer(layer: MetricStateNoESQL): FormBasedPersistedState[ const newColumn = fromMetricAPItoLensState(primaryMetric.background_chart.max_value); addLayerColumn(defaultLayer, columnName, newColumn); - if (trendLineLayer) { - addLayerColumn(trendLineLayer, `${columnName}_trendline`, newColumn, false, 'X0'); - } } return layers; @@ -603,8 +627,14 @@ export function fromAPItoLensState(config: MetricState): MetricAttributesWithout const regularDataViews = Object.values(usedDataviews).filter( (v): v is { id: string; type: 'dataView' } => v.type === 'dataView' ); + const hasTrendline = + isPrimaryMetric(config.metrics[0]) && config.metrics[0]?.background_chart?.type === 'trend'; + const references = regularDataViews.length - ? buildReferences({ [DEFAULT_LAYER_ID]: regularDataViews[0]?.id }) + ? buildReferences({ + [DEFAULT_LAYER_ID]: regularDataViews[0]?.id, + ...(hasTrendline ? { [TRENDLINE_LAYER_ID]: regularDataViews[0]?.id } : {}), + }) : []; return { @@ -625,6 +655,9 @@ export function fromLensStateToAPI(config: LensAttributes): MetricState { const visualization = state.visualization as MetricVisualizationState; const layers = getDatasourceLayers(state); const [layerId, layer] = getLensStateLayer(layers, visualization.layerId); + const trendlineLayer = visualization.trendlineLayerId + ? layers[visualization.trendlineLayerId] + : undefined; const visualizationState = { ...getSharedChartLensStateToAPI(config), @@ -632,6 +665,11 @@ export function fromLensStateToAPI(config: LensAttributes): MetricState { visualization, layer, layerId ?? DEFAULT_LAYER_ID, + trendlineLayer && + // this is mostly a type-guard... trendlines are only supported for form based metric visualizations + !isTextBasedLayer(trendlineLayer) + ? trendlineLayer + : undefined, config.state.adHocDataViews ?? {}, config.references, config.state.internalReferences