diff --git a/x-pack/plugins/lens/common/expressions/datatable/utils.ts b/x-pack/plugins/lens/common/expressions/datatable/utils.ts index 483f42424c144..811ecefb299a2 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/utils.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/utils.ts @@ -8,6 +8,20 @@ import { type Datatable, type DatatableColumnMeta } from '@kbn/expressions-plugin/common'; import { getOriginalId } from '@kbn/transpose-utils'; +/** + * Make sure to specifically check for "top_hits" when looking for array values + * + * **Note**: use this utility function only at the expression level, + * not before (i.e. to decide if a column in numeric in a configuration panel) + */ +function isLastValueWithoutArraySupport(meta: DatatableColumnMeta): boolean { + return ( + meta.sourceParams?.type !== 'filtered_metric' || + (meta.sourceParams?.params as { customMetric: { type: 'top_hits' | 'top_metrics' } }) + ?.customMetric?.type !== 'top_hits' + ); +} + /** * Returns true for numerical fields * @@ -15,7 +29,10 @@ import { getOriginalId } from '@kbn/transpose-utils'; * - `range` - Stringified range * - `multi_terms` - Multiple values * - `filters` - Arbitrary label - * - `filtered_metric` - Array of values + * - Last value with array values + * + * **Note**: use this utility function only at the expression level, + * not before (i.e. to decide if a column in numeric in a configuration panel) */ export function isNumericField(meta?: DatatableColumnMeta): boolean { return ( @@ -23,12 +40,15 @@ export function isNumericField(meta?: DatatableColumnMeta): boolean { meta.params?.id !== 'range' && meta.params?.id !== 'multi_terms' && meta.sourceParams?.type !== 'filters' && - meta.sourceParams?.type !== 'filtered_metric' + isLastValueWithoutArraySupport(meta) ); } /** * Returns true for numerical fields, excluding ranges + * + * **Note**: use this utility function only at the expression level, + * not before (i.e. to decide if a column in numeric in a configuration panel) */ export function isNumericFieldForDatatable(table: Datatable | undefined, accessor: string) { const meta = getFieldMetaFromDatatable(table, accessor); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts index b399f8eaa7b54..4425ee20b3a15 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts @@ -2167,6 +2167,7 @@ describe('IndexPattern Data Source', () => { scale: undefined, sortingHint: undefined, interval: undefined, + hasArraySupport: false, } as OperationDescriptor); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index da893707ab2bc..368696c23ca88 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -177,6 +177,9 @@ export function columnToOperation( interval: isColumnOfType('date_histogram', column) ? column.params.interval : undefined, + hasArraySupport: + isColumnOfType('last_value', column) && + column.params.showArrayValues, }; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx index 0a0c0dcc05eeb..aa8272d80af15 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based_suggestions.test.tsx @@ -1314,6 +1314,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, { @@ -1326,6 +1327,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, ], @@ -1406,6 +1408,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, { @@ -1418,6 +1421,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, ], @@ -2255,6 +2259,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, ], @@ -2280,6 +2285,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, ], @@ -2332,6 +2338,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -2345,6 +2352,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], @@ -2411,6 +2419,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, { @@ -2424,6 +2433,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -2437,6 +2447,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], @@ -2524,6 +2535,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, { @@ -2537,6 +2549,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -2550,6 +2563,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], @@ -2660,6 +2674,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, { @@ -2673,6 +2688,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -2686,6 +2702,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], @@ -3193,6 +3210,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, { @@ -3205,6 +3223,7 @@ describe('IndexPattern Data Source suggestions', () => { isStaticValue: false, hasTimeShift: false, hasReducedTimeRange: false, + hasArraySupport: false, }, }, ], @@ -3274,6 +3293,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -3287,6 +3307,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, { @@ -3300,6 +3321,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], @@ -3367,6 +3389,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: 'auto', + hasArraySupport: false, }, }, { @@ -3380,6 +3403,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, { @@ -3393,6 +3417,7 @@ describe('IndexPattern Data Source suggestions', () => { hasTimeShift: false, hasReducedTimeRange: false, interval: undefined, + hasArraySupport: false, }, }, ], diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 5b5e33564cc7d..e0ea0f7596d99 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -756,6 +756,8 @@ export interface OperationMetadata { // document and an aggregated metric which might be handy in some cases. Once we // introduce a raw document datasource, this should be considered here. isStaticValue?: boolean; + // Extra metadata to infer array support in an operation + hasArraySupport?: boolean; } /** diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx index 73f07d66bcb8b..d21a3f8d05901 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx @@ -20,7 +20,7 @@ import { SupportingVisType, } from './dimension_editor'; import { DatasourcePublicAPI } from '../..'; -import { createMockFramePublicAPI, generateActiveData } from '../../mocks'; +import { createMockFramePublicAPI } from '../../mocks'; // see https://github.com/facebook/jest/issues/4402#issuecomment-534516219 const expectCalledBefore = (mock1: jest.Mock, mock2: jest.Mock) => @@ -71,22 +71,29 @@ describe('dimension editor', () => { trendlineBreakdownByAccessor: 'trendline-breakdown-col-id', }; - const nonNumericMetricFrame = createMockFramePublicAPI({ - activeData: generateActiveData([ - { - id: 'first', - rows: Array(3).fill({ - 'metric-col-id': faker.lorem.word(3), - 'max-col-id': faker.random.number(), - }), - }, - ]), - }); - let props: VisualizationDimensionEditorProps & { paletteService: PaletteRegistry; }; + const getNonNumericDatasource = () => + ({ + hasDefaultTimeField: jest.fn(() => true), + getOperationForColumnId: jest.fn(() => ({ + hasReducedTimeRange: false, + dataType: 'keyword', + })), + } as unknown as DatasourcePublicAPI); + + const getNumericDatasourceWithArraySupport = () => + ({ + hasDefaultTimeField: jest.fn(() => true), + getOperationForColumnId: jest.fn(() => ({ + hasReducedTimeRange: false, + dataType: 'number', + hasArraySupport: true, + })), + } as unknown as DatasourcePublicAPI); + beforeEach(() => { props = { layerId: 'first', @@ -97,21 +104,12 @@ describe('dimension editor', () => { hasDefaultTimeField: jest.fn(() => true), getOperationForColumnId: jest.fn(() => ({ hasReducedTimeRange: false, + dataType: 'number', })), } as unknown as DatasourcePublicAPI, removeLayer: jest.fn(), addLayer: jest.fn(), - frame: createMockFramePublicAPI({ - activeData: generateActiveData([ - { - id: 'first', - rows: Array(3).fill({ - 'metric-col-id': faker.random.number(), - 'secondary-metric-col-id': faker.random.number(), - }), - }, - ]), - }), + frame: createMockFramePublicAPI(), setState: jest.fn(), panelRef: {} as React.MutableRefObject, paletteService: chartPluginMock.createPaletteRegistry(), @@ -177,7 +175,16 @@ describe('dimension editor', () => { }); it('Color mode switch is not shown when the primary metric is non-numeric', () => { - const { colorModeGroup } = renderPrimaryMetricEditor({ frame: nonNumericMetricFrame }); + const { colorModeGroup } = renderPrimaryMetricEditor({ + datasource: getNonNumericDatasource(), + }); + expect(colorModeGroup).not.toBeInTheDocument(); + }); + + it('Color mode switch is not shown when the primary metric is numeric but with array support', () => { + const { colorModeGroup } = renderPrimaryMetricEditor({ + datasource: getNumericDatasourceWithArraySupport(), + }); expect(colorModeGroup).not.toBeInTheDocument(); }); @@ -196,7 +203,7 @@ describe('dimension editor', () => { }); it('is visible when metric is non-numeric even if palette is set', () => { const { staticColorPicker } = renderPrimaryMetricEditor({ - frame: nonNumericMetricFrame, + datasource: getNonNumericDatasource(), state: { ...metricAccessorState, palette }, }); expect(staticColorPicker).toBeInTheDocument(); @@ -571,6 +578,7 @@ describe('dimension editor', () => { ...props.datasource, getOperationForColumnId: (id: string) => ({ hasReducedTimeRange: id === stateWOTrend.metricAccessor, + dataType: 'number', }), }, }); @@ -579,7 +587,7 @@ describe('dimension editor', () => { it('should not show a trendline button group when primary metric dimension is non-numeric', () => { const { container } = renderAdditionalSectionEditor({ - frame: nonNumericMetricFrame, + datasource: getNonNumericDatasource(), }); expect(container).toBeEmptyDOMElement(); }); diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx index 78fcd67f36431..1fe3346e64652 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx @@ -30,7 +30,6 @@ import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { css } from '@emotion/react'; import { DebouncedInput, IconSelect } from '@kbn/visualization-ui-components'; import { useDebouncedValue } from '@kbn/visualization-utils'; -import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils'; import { applyPaletteParams, PalettePanelContainer } from '../../shared_components'; import type { VisualizationDimensionEditorProps } from '../../types'; import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config'; @@ -38,6 +37,7 @@ import { DEFAULT_MAX_COLUMNS, getDefaultColor, showingBar } from './visualizatio import { CollapseSetting } from '../../shared_components/collapse_setting'; import { MetricVisualizationState } from './types'; import { metricIconsSet } from '../../shared_components/icon_set'; +import { isMetricNumericType } from './helpers'; export type SupportingVisType = 'none' | 'bar' | 'trendline'; @@ -212,23 +212,21 @@ function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, s function PrimaryMetricEditor(props: SubProps) { const { state, setState, frame, accessor, idPrefix, isInlineEditing } = props; - const currentData = frame.activeData?.[state.layerId]; - - const isMetricNumeric = isNumericFieldForDatatable(currentData, accessor); - if (accessor == null) { return null; } - const hasDynamicColoring = Boolean(isMetricNumeric && state?.palette); + const isMetricNumeric = isMetricNumericType(props.datasource, accessor); + + const hasDynamicColoring = Boolean(isMetricNumeric && state.palette); const supportsPercentPalette = Boolean( state.maxAccessor || (state.breakdownByAccessor && !state.collapseFn) || - state?.palette?.params?.rangeType === 'percent' + state.palette?.params?.rangeType === 'percent' ); - const activePalette = state?.palette || { + const activePalette = state.palette || { type: 'palette', name: (supportsPercentPalette ? defaultPercentagePaletteParams : defaultNumberPaletteParams) .name, @@ -313,7 +311,9 @@ function PrimaryMetricEditor(props: SubProps) { /> )} - {!hasDynamicColoring && } + {!hasDynamicColoring && ( + + )} {hasDynamicColoring && ( ) { + isMetricNumeric, +}: Pick & { isMetricNumeric: boolean }) { const colorLabel = i18n.translate('xpack.lens.metric.color', { defaultMessage: 'Color', }); - const currentData = frame.activeData?.[state.layerId]; - const isMetricNumeric = Boolean( - state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor) - ); const setColor = useCallback( (color: string) => { @@ -420,8 +416,8 @@ export function DimensionEditorAdditionalSection({ }: VisualizationDimensionEditorProps) { const { euiTheme } = useEuiTheme(); - const currentData = frame.activeData?.[state.layerId]; - if (accessor !== state.metricAccessor || !isNumericFieldForDatatable(currentData, accessor)) { + const isMetricNumeric = isMetricNumericType(datasource, accessor); + if (accessor !== state.metricAccessor || !isMetricNumeric) { return null; } diff --git a/x-pack/plugins/lens/public/visualizations/metric/helpers.ts b/x-pack/plugins/lens/public/visualizations/metric/helpers.ts new file mode 100644 index 0000000000000..2af382fabfd4a --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/metric/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { DatasourcePublicAPI } from '../../types'; + +/** + * Infer the numeric type of a metric column purely on the configuration + */ +export function isMetricNumericType( + datasource: DatasourcePublicAPI | undefined, + accessor: string | undefined +) { + // No accessor means it's not a numeric type by default + if (!accessor || !datasource) { + return false; + } + const operation = datasource.getOperationForColumnId(accessor); + const isNumericTypeFromOperation = Boolean( + operation?.dataType === 'number' && !operation.hasArraySupport + ); + return isNumericTypeFromOperation; +} diff --git a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts index 02292a4ddce0a..b5d3b140be63f 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts @@ -21,6 +21,7 @@ import { showingBar } from './metric_visualization'; import { DEFAULT_MAX_COLUMNS, getDefaultColor } from './visualization'; import { MetricVisualizationState } from './types'; import { metricStateDefaults } from './constants'; +import { isMetricNumericType } from './helpers'; // TODO - deduplicate with gauges? function computePaletteParams(params: CustomPaletteParams) { @@ -93,10 +94,7 @@ export const toExpression = ( const datasource = datasourceLayers[state.layerId]; const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; - const isMetricNumeric = Boolean( - state.metricAccessor && - datasource?.getOperationForColumnId(state.metricAccessor)?.dataType === 'number' - ); + const isMetricNumeric = isMetricNumericType(datasource, state.metricAccessor); const maxPossibleTiles = // if there's a collapse function, no need to calculate since we're dealing with a single tile state.breakdownByAccessor && !state.collapseFn diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index 6d3bd42f26cfa..b0276ae9ab6f3 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -33,6 +33,7 @@ import { toExpression } from './to_expression'; import { nonNullable } from '../../utils'; import { METRIC_NUMERIC_MAX } from '../../user_messages_ids'; import { MetricVisualizationState } from './types'; +import { isMetricNumericType } from './helpers'; export const DEFAULT_MAX_COLUMNS = 3; @@ -653,9 +654,9 @@ export const getMetricVisualization = ({ const hasStaticColoring = !!state.color; const hasDynamicColoring = !!state.palette; - const currentData = frame?.activeData?.[state.layerId]; - const isMetricNumeric = Boolean( - state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor) + const isMetricNumeric = isMetricNumericType( + frame?.datasourceLayers[state.layerId], + state.metricAccessor ); return {