diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx
index 5b39fd0482bb4..4342c00c98854 100644
--- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx
+++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.test.tsx
@@ -9,12 +9,15 @@
import { render, act, screen } from '@testing-library/react';
import React from 'react';
+import type { DatatableColumn } from '@kbn/expressions-plugin/common';
+import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
+import { DataViewField } from '@kbn/data-views-plugin/common';
import { UnifiedHistogramBreakdownContext } from '../types';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { BreakdownFieldSelector } from './breakdown_field_selector';
describe('BreakdownFieldSelector', () => {
- it('should render correctly', () => {
+ it('should render correctly for dataview fields', () => {
const onBreakdownFieldChange = jest.fn();
const breakdown: UnifiedHistogramBreakdownContext = {
field: undefined,
@@ -63,6 +66,67 @@ describe('BreakdownFieldSelector', () => {
`);
});
+ it('should render correctly for ES|QL columns', () => {
+ const onBreakdownFieldChange = jest.fn();
+ const breakdown: UnifiedHistogramBreakdownContext = {
+ field: undefined,
+ };
+
+ render(
+
+ );
+
+ const button = screen.getByTestId('unifiedHistogramBreakdownSelectorButton');
+ expect(button.getAttribute('data-selected-value')).toBe(null);
+
+ act(() => {
+ button.click();
+ });
+
+ const options = screen.getAllByRole('option');
+ expect(
+ options.map((option) => ({
+ label: option.getAttribute('title'),
+ value: option.getAttribute('value'),
+ checked: option.getAttribute('aria-checked'),
+ }))
+ ).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "checked": "true",
+ "label": "No breakdown",
+ "value": "__EMPTY_SELECTOR_OPTION__",
+ },
+ Object {
+ "checked": "false",
+ "label": "bytes",
+ "value": "bytes",
+ },
+ Object {
+ "checked": "false",
+ "label": "extension",
+ "value": "extension",
+ },
+ ]
+ `);
+ });
+
it('should mark the option as checked if breakdown.field is defined', () => {
const onBreakdownFieldChange = jest.fn();
const field = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!;
@@ -111,7 +175,7 @@ describe('BreakdownFieldSelector', () => {
`);
});
- it('should call onBreakdownFieldChange with the selected field when the user selects a field', () => {
+ it('should call onBreakdownFieldChange with the selected field when the user selects a dataview field', () => {
const onBreakdownFieldChange = jest.fn();
const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'bytes')!;
const breakdown: UnifiedHistogramBreakdownContext = {
@@ -135,4 +199,45 @@ describe('BreakdownFieldSelector', () => {
expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField);
});
+
+ it('should call onBreakdownFieldChange with the selected field when the user selects an ES|QL field', () => {
+ const onBreakdownFieldChange = jest.fn();
+ const esqlColumns = [
+ {
+ name: 'bytes',
+ meta: { type: 'number' },
+ id: 'bytes',
+ },
+ {
+ name: 'extension',
+ meta: { type: 'string' },
+ id: 'extension',
+ },
+ ] as DatatableColumn[];
+ const breakdownColumn = esqlColumns.find((c) => c.name === 'bytes')!;
+ const selectedField = new DataViewField(
+ convertDatatableColumnToDataViewFieldSpec(breakdownColumn)
+ );
+ const breakdown: UnifiedHistogramBreakdownContext = {
+ field: undefined,
+ };
+ render(
+
+ );
+
+ act(() => {
+ screen.getByTestId('unifiedHistogramBreakdownSelectorButton').click();
+ });
+
+ act(() => {
+ screen.getByTitle('bytes').click();
+ });
+
+ expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField);
+ });
});
diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx
index 7d29827a1389b..b3c49e27c6011 100644
--- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx
+++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx
@@ -11,7 +11,9 @@ import React, { useCallback, useMemo } from 'react';
import { EuiSelectableOption } from '@elastic/eui';
import { FieldIcon, getFieldIconProps, comboBoxFieldOptionMatcher } from '@kbn/field-utils';
import { css } from '@emotion/react';
-import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
+import { type DataView, DataViewField } from '@kbn/data-views-plugin/common';
+import type { DatatableColumn } from '@kbn/expressions-plugin/common';
+import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { i18n } from '@kbn/i18n';
import { UnifiedHistogramBreakdownContext } from '../types';
import { fieldSupportsBreakdown } from '../utils/field_supports_breakdown';
@@ -25,17 +27,32 @@ import {
export interface BreakdownFieldSelectorProps {
dataView: DataView;
breakdown: UnifiedHistogramBreakdownContext;
+ esqlColumns?: DatatableColumn[];
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
}
+const mapToDropdownFields = (dataView: DataView, esqlColumns?: DatatableColumn[]) => {
+ if (esqlColumns) {
+ return (
+ esqlColumns
+ .map((column) => new DataViewField(convertDatatableColumnToDataViewFieldSpec(column)))
+ // filter out unsupported field types
+ .filter((field) => field.type !== 'unknown')
+ );
+ }
+
+ return dataView.fields.filter(fieldSupportsBreakdown);
+};
+
export const BreakdownFieldSelector = ({
dataView,
breakdown,
+ esqlColumns,
onBreakdownFieldChange,
}: BreakdownFieldSelectorProps) => {
+ const fields = useMemo(() => mapToDropdownFields(dataView, esqlColumns), [dataView, esqlColumns]);
const fieldOptions: SelectableEntry[] = useMemo(() => {
- const options: SelectableEntry[] = dataView.fields
- .filter(fieldSupportsBreakdown)
+ const options: SelectableEntry[] = fields
.map((field) => ({
key: field.name,
name: field.name,
@@ -69,16 +86,16 @@ export const BreakdownFieldSelector = ({
});
return options;
- }, [dataView, breakdown.field]);
+ }, [fields, breakdown?.field]);
const onChange = useCallback>(
(chosenOption) => {
- const field = chosenOption?.value
- ? dataView.fields.find((currentField) => currentField.name === chosenOption.value)
+ const breakdownField = chosenOption?.value
+ ? fields.find((currentField) => currentField.name === chosenOption.value)
: undefined;
- onBreakdownFieldChange?.(field);
+ onBreakdownFieldChange?.(breakdownField);
},
- [dataView.fields, onBreakdownFieldChange]
+ [fields, onBreakdownFieldChange]
);
return (
diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx
index 4204658273447..4fb1b9cbe6471 100644
--- a/src/plugins/unified_histogram/public/chart/chart.tsx
+++ b/src/plugins/unified_histogram/public/chart/chart.tsx
@@ -19,6 +19,7 @@ import type {
LensEmbeddableInput,
LensEmbeddableOutput,
} from '@kbn/lens-plugin/public';
+import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { Histogram } from './histogram';
@@ -79,6 +80,7 @@ export interface ChartProps {
onFilter?: LensEmbeddableInput['onFilter'];
onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
withDefaultActions: EmbeddableComponentProps['withDefaultActions'];
+ columns?: DatatableColumn[];
}
const HistogramMemoized = memo(Histogram);
@@ -114,6 +116,7 @@ export function Chart({
onBrushEnd,
withDefaultActions,
abortController,
+ columns,
}: ChartProps) {
const lensVisServiceCurrentSuggestionContext = useObservable(
lensVisService.currentSuggestionContext$
@@ -312,6 +315,7 @@ export function Chart({
dataView={dataView}
breakdown={breakdown}
onBreakdownFieldChange={onBreakdownFieldChange}
+ esqlColumns={isPlainRecord ? columns : undefined}
/>
)}
diff --git a/src/plugins/unified_histogram/public/container/container.tsx b/src/plugins/unified_histogram/public/container/container.tsx
index a4231529a629b..15367ae51d9b5 100644
--- a/src/plugins/unified_histogram/public/container/container.tsx
+++ b/src/plugins/unified_histogram/public/container/container.tsx
@@ -147,6 +147,7 @@ export const UnifiedHistogramContainer = forwardRef<
query,
searchSessionId,
requestAdapter,
+ columns: containerProps.columns,
});
const handleVisContextChange: UnifiedHistogramLayoutProps['onVisContextChanged'] | undefined =
diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts
index e109b5339e728..44a36be34d1ab 100644
--- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts
+++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts
@@ -11,6 +11,8 @@ import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/co
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-test-renderer';
+import type { DatatableColumn } from '@kbn/expressions-plugin/common';
+import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { UnifiedHistogramFetchStatus, UnifiedHistogramSuggestionContext } from '../../types';
import { dataViewMock } from '../../__mocks__/data_view';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
@@ -60,6 +62,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
expect(result.current).toMatchInlineSnapshot(`
@@ -150,11 +153,14 @@ describe('useStateProps', () => {
query: { esql: 'FROM index' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
expect(result.current).toMatchInlineSnapshot(`
Object {
- "breakdown": undefined,
+ "breakdown": Object {
+ "field": undefined,
+ },
"chart": Object {
"hidden": false,
"timeInterval": "auto",
@@ -220,9 +226,13 @@ describe('useStateProps', () => {
},
}
`);
+
+ expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' });
+ expect(result.current.breakdown).toStrictEqual({ field: undefined });
+ expect(result.current.isPlainRecord).toBe(true);
});
- it('should return the correct props when a text based language is used', () => {
+ it('should return the correct props when an ES|QL query is used with transformational commands', () => {
const stateService = getStateService({
initialState: {
...initialState,
@@ -233,9 +243,10 @@ describe('useStateProps', () => {
useStateProps({
stateService,
dataView: dataViewWithTimefieldMock,
- query: { esql: 'FROM index' },
+ query: { esql: 'FROM index | keep field1' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' });
@@ -243,6 +254,82 @@ describe('useStateProps', () => {
expect(result.current.isPlainRecord).toBe(true);
});
+ it('should return the correct props when an ES|QL query is used with breakdown field', () => {
+ const breakdownField = 'extension';
+ const esqlColumns = [
+ {
+ name: 'bytes',
+ meta: { type: 'number' },
+ id: 'bytes',
+ },
+ {
+ name: 'extension',
+ meta: { type: 'string' },
+ id: 'extension',
+ },
+ ] as DatatableColumn[];
+ const stateService = getStateService({
+ initialState: {
+ ...initialState,
+ currentSuggestionContext: undefined,
+ breakdownField,
+ },
+ });
+ const { result } = renderHook(() =>
+ useStateProps({
+ stateService,
+ dataView: dataViewWithTimefieldMock,
+ query: { esql: 'FROM index' },
+ requestAdapter: new RequestAdapter(),
+ searchSessionId: '123',
+ columns: esqlColumns,
+ })
+ );
+
+ const breakdownColumn = esqlColumns.find((c) => c.name === breakdownField)!;
+ const selectedField = new DataViewField(
+ convertDatatableColumnToDataViewFieldSpec(breakdownColumn)
+ );
+ expect(result.current.breakdown).toStrictEqual({ field: selectedField });
+ });
+
+ it('should call the setBreakdown cb when an ES|QL query is used', () => {
+ const breakdownField = 'extension';
+ const esqlColumns = [
+ {
+ name: 'bytes',
+ meta: { type: 'number' },
+ id: 'bytes',
+ },
+ {
+ name: 'extension',
+ meta: { type: 'string' },
+ id: 'extension',
+ },
+ ] as DatatableColumn[];
+ const stateService = getStateService({
+ initialState: {
+ ...initialState,
+ currentSuggestionContext: undefined,
+ },
+ });
+ const { result } = renderHook(() =>
+ useStateProps({
+ stateService,
+ dataView: dataViewWithTimefieldMock,
+ query: { esql: 'FROM index' },
+ requestAdapter: new RequestAdapter(),
+ searchSessionId: '123',
+ columns: esqlColumns,
+ })
+ );
+ const { onBreakdownFieldChange } = result.current;
+ act(() => {
+ onBreakdownFieldChange({ name: breakdownField } as DataViewField);
+ });
+ expect(stateService.setBreakdownField).toHaveBeenLastCalledWith(breakdownField);
+ });
+
it('should return the correct props when a rollup data view is used', () => {
const stateService = getStateService({ initialState });
const { result } = renderHook(() =>
@@ -255,6 +342,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
expect(result.current).toMatchInlineSnapshot(`
@@ -333,6 +421,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
expect(result.current).toMatchInlineSnapshot(`
@@ -411,6 +500,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
const {
@@ -470,6 +560,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
})
);
(stateService.setLensRequestAdapter as jest.Mock).mockClear();
@@ -489,6 +580,7 @@ describe('useStateProps', () => {
query: { language: 'kuery', query: '' },
requestAdapter: new RequestAdapter(),
searchSessionId: '123',
+ columns: undefined,
};
const hook = renderHook((props: Parameters[0]) => useStateProps(props), {
initialProps,
diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts
index bb0e4acc81740..fcc19fcd78a00 100644
--- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts
+++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts
@@ -9,7 +9,10 @@
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/common';
import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query';
+import { hasTransformationalCommand } from '@kbn/esql-utils';
import type { RequestAdapter } from '@kbn/inspector-plugin/public';
+import type { DatatableColumn } from '@kbn/expressions-plugin/common';
+import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { useCallback, useEffect, useMemo } from 'react';
import {
UnifiedHistogramChartLoadEvent,
@@ -34,12 +37,14 @@ export const useStateProps = ({
query,
searchSessionId,
requestAdapter,
+ columns,
}: {
stateService: UnifiedHistogramStateService | undefined;
dataView: DataView;
query: Query | AggregateQuery | undefined;
searchSessionId: string | undefined;
requestAdapter: RequestAdapter | undefined;
+ columns: DatatableColumn[] | undefined;
}) => {
const breakdownField = useStateSelector(stateService?.state$, breakdownFieldSelector);
const chartHidden = useStateSelector(stateService?.state$, chartHiddenSelector);
@@ -86,14 +91,29 @@ export const useStateProps = ({
}, [chartHidden, isPlainRecord, isTimeBased, timeInterval]);
const breakdown = useMemo(() => {
- if (isPlainRecord || !isTimeBased) {
+ if (!isTimeBased) {
return undefined;
}
+ // hide the breakdown field selector when the ES|QL query has a transformational command (STATS, KEEP etc)
+ if (query && isOfAggregateQueryType(query) && hasTransformationalCommand(query.esql)) {
+ return undefined;
+ }
+
+ if (isPlainRecord) {
+ const breakdownColumn = columns?.find((column) => column.name === breakdownField);
+ const field = breakdownColumn
+ ? new DataViewField(convertDatatableColumnToDataViewFieldSpec(breakdownColumn))
+ : undefined;
+ return {
+ field,
+ };
+ }
+
return {
field: breakdownField ? dataView?.getFieldByName(breakdownField) : undefined,
};
- }, [breakdownField, dataView, isPlainRecord, isTimeBased]);
+ }, [isTimeBased, query, isPlainRecord, breakdownField, dataView, columns]);
const request = useMemo(() => {
return {
diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx
index aac1cfe308c60..3e34cf4ee69b3 100644
--- a/src/plugins/unified_histogram/public/layout/layout.tsx
+++ b/src/plugins/unified_histogram/public/layout/layout.tsx
@@ -374,6 +374,7 @@ export const UnifiedHistogramLayout = ({
lensAdapters={lensAdapters}
lensEmbeddableOutput$={lensEmbeddableOutput$}
withDefaultActions={withDefaultActions}
+ columns={columns}
/>
diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts
index f4128146c9f34..28819f7a5c54b 100644
--- a/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts
+++ b/src/plugins/unified_histogram/public/services/lens_vis_service.suggestions.test.ts
@@ -8,6 +8,7 @@
*/
import type { AggregateQuery, Query } from '@kbn/es-query';
+import { DataViewField } from '@kbn/data-views-plugin/common';
import { deepMockedFields, buildDataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { allSuggestionsMock } from '../__mocks__/suggestions';
import { getLensVisMock } from '../__mocks__/lens_vis';
@@ -195,4 +196,55 @@ describe('LensVisService suggestions', () => {
expect(lensVis.currentSuggestionContext?.type).toBe(UnifiedHistogramSuggestionType.unsupported);
expect(lensVis.currentSuggestionContext?.suggestion).not.toBeDefined();
});
+
+ test('should return histogramSuggestion if no suggestions returned by the api with the breakdown field if it is given', async () => {
+ const lensVis = await getLensVisMock({
+ filters: [],
+ query: { esql: 'from the-data-view | limit 100' },
+ dataView: dataViewMock,
+ timeInterval: 'auto',
+ timeRange: {
+ from: '2023-09-03T08:00:00.000Z',
+ to: '2023-09-04T08:56:28.274Z',
+ },
+ breakdownField: { name: 'var0' } as DataViewField,
+ columns: [
+ {
+ id: 'var0',
+ name: 'var0',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ isPlainRecord: true,
+ allSuggestions: [],
+ hasHistogramSuggestionForESQL: true,
+ });
+
+ expect(lensVis.currentSuggestionContext?.type).toBe(
+ UnifiedHistogramSuggestionType.histogramForESQL
+ );
+ expect(lensVis.currentSuggestionContext?.suggestion).toBeDefined();
+ expect(lensVis.currentSuggestionContext?.suggestion?.visualizationState).toHaveProperty(
+ 'layers',
+ [
+ {
+ layerId: '662552df-2cdc-4539-bf3b-73b9f827252c',
+ seriesType: 'bar_stacked',
+ xAccessor: '@timestamp every 30 second',
+ accessors: ['results'],
+ layerType: 'data',
+ splitAccessor: 'var0',
+ },
+ ]
+ );
+
+ const histogramQuery = {
+ esql: `from the-data-view | limit 100
+| EVAL timestamp=DATE_TRUNC(30 minute, @timestamp) | stats results = count(*) by timestamp, \`var0\` | sort \`var0\` asc | rename timestamp as \`@timestamp every 30 minute\``,
+ };
+
+ expect(lensVis.visContext?.attributes.state.query).toStrictEqual(histogramQuery);
+ });
});
diff --git a/src/plugins/unified_histogram/public/services/lens_vis_service.ts b/src/plugins/unified_histogram/public/services/lens_vis_service.ts
index caadc3506ccbe..eccfd663b2557 100644
--- a/src/plugins/unified_histogram/public/services/lens_vis_service.ts
+++ b/src/plugins/unified_histogram/public/services/lens_vis_service.ts
@@ -245,7 +245,10 @@ export class LensVisService {
if (queryParams.isPlainRecord) {
// appends an ES|QL histogram
- const histogramSuggestionForESQL = this.getHistogramSuggestionForESQL({ queryParams });
+ const histogramSuggestionForESQL = this.getHistogramSuggestionForESQL({
+ queryParams,
+ breakdownField,
+ });
if (histogramSuggestionForESQL) {
availableSuggestionsWithType.push({
suggestion: histogramSuggestionForESQL,
@@ -452,16 +455,27 @@ export class LensVisService {
private getHistogramSuggestionForESQL = ({
queryParams,
+ breakdownField,
}: {
queryParams: QueryParams;
+ breakdownField?: DataViewField;
}): Suggestion | undefined => {
- const { dataView, query, timeRange } = queryParams;
+ const { dataView, query, timeRange, columns } = queryParams;
+ const breakdownColumn = breakdownField?.name
+ ? columns?.find((column) => column.name === breakdownField.name)
+ : undefined;
if (dataView.isTimeBased() && query && isOfAggregateQueryType(query) && timeRange) {
const isOnHistogramMode = shouldDisplayHistogram(query);
if (!isOnHistogramMode) return undefined;
const interval = computeInterval(timeRange, this.services.data);
- const esqlQuery = this.getESQLHistogramQuery({ dataView, query, timeRange, interval });
+ const esqlQuery = this.getESQLHistogramQuery({
+ dataView,
+ query,
+ timeRange,
+ interval,
+ breakdownColumn,
+ });
const context = {
dataViewSpec: dataView?.toSpec(),
fieldName: '',
@@ -485,9 +499,38 @@ export class LensVisService {
esql: esqlQuery,
},
};
+
+ if (breakdownColumn) {
+ context.textBasedColumns.push(breakdownColumn);
+ }
const suggestions = this.lensSuggestionsApi(context, dataView, ['lnsDatatable']) ?? [];
if (suggestions.length) {
- return suggestions[0];
+ const suggestion = suggestions[0];
+ const suggestionVisualizationState = Object.assign({}, suggestion?.visualizationState);
+ // the suggestions api will suggest a numeric column as a metric and not as a breakdown,
+ // so we need to adjust it here
+ if (
+ breakdownColumn &&
+ breakdownColumn.meta?.type === 'number' &&
+ suggestion &&
+ 'layers' in suggestionVisualizationState &&
+ Array.isArray(suggestionVisualizationState.layers)
+ ) {
+ return {
+ ...suggestion,
+ visualizationState: {
+ ...(suggestionVisualizationState ?? {}),
+ layers: suggestionVisualizationState.layers.map((layer) => {
+ return {
+ ...layer,
+ accessors: ['results'],
+ splitAccessor: breakdownColumn.name,
+ };
+ }),
+ },
+ };
+ }
+ return suggestion;
}
}
@@ -499,18 +542,23 @@ export class LensVisService {
timeRange,
query,
interval,
+ breakdownColumn,
}: {
dataView: DataView;
timeRange: TimeRange;
query: AggregateQuery;
interval?: string;
+ breakdownColumn?: DatatableColumn;
}): string => {
const queryInterval = interval ?? computeInterval(timeRange, this.services.data);
const language = getAggregateQueryMode(query);
const safeQuery = removeDropCommandsFromESQLQuery(query[language]);
+ const breakdown = breakdownColumn
+ ? `, \`${breakdownColumn.name}\` | sort \`${breakdownColumn.name}\` asc`
+ : '';
return appendToESQLQuery(
safeQuery,
- `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\``
+ `| EVAL timestamp=DATE_TRUNC(${queryInterval}, ${dataView.timeFieldName}) | stats results = count(*) by timestamp${breakdown} | rename timestamp as \`${dataView.timeFieldName} every ${queryInterval}\``
);
};
@@ -548,7 +596,7 @@ export class LensVisService {
externalVisContextStatus: UnifiedHistogramExternalVisContextStatus;
visContext: UnifiedHistogramVisContext | undefined;
} => {
- const { dataView, query, filters, timeRange } = queryParams;
+ const { dataView, query, filters, timeRange, columns } = queryParams;
const { type: suggestionType, suggestion } = currentSuggestionContext;
if (!suggestion || !suggestion.datasourceId || !query || !filters) {
@@ -563,13 +611,20 @@ export class LensVisService {
dataViewId: dataView.id,
timeField: dataView.timeFieldName,
timeInterval: isTextBased ? undefined : timeInterval,
- breakdownField: isTextBased ? undefined : breakdownField?.name,
+ breakdownField: breakdownField?.name,
};
const currentQuery =
suggestionType === UnifiedHistogramSuggestionType.histogramForESQL && isTextBased && timeRange
? {
- esql: this.getESQLHistogramQuery({ dataView, query, timeRange }),
+ esql: this.getESQLHistogramQuery({
+ dataView,
+ query,
+ timeRange,
+ breakdownColumn: breakdownField?.name
+ ? columns?.find((column) => column.name === breakdownField.name)
+ : undefined,
+ }),
}
: query;
diff --git a/src/plugins/unified_histogram/tsconfig.json b/src/plugins/unified_histogram/tsconfig.json
index 2f54a5d33797a..d14adf53889b9 100644
--- a/src/plugins/unified_histogram/tsconfig.json
+++ b/src/plugins/unified_histogram/tsconfig.json
@@ -33,6 +33,7 @@
"@kbn/discover-utils",
"@kbn/visualization-utils",
"@kbn/search-types",
+ "@kbn/data-view-utils",
],
"exclude": [
"target/**/*",
diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts
index 01660925db799..760827816db96 100644
--- a/test/functional/apps/discover/esql/_esql_view.ts
+++ b/test/functional/apps/discover/esql/_esql_view.ts
@@ -675,5 +675,63 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
});
+
+ describe('histogram breakdown', () => {
+ before(async () => {
+ await common.navigateToApp('discover');
+ await timePicker.setDefaultAbsoluteRange();
+ await header.waitUntilLoadingHasFinished();
+ await discover.waitUntilSearchingHasFinished();
+ });
+
+ it('should choose breakdown field', async () => {
+ await discover.selectTextBaseLang();
+ await header.waitUntilLoadingHasFinished();
+ await discover.waitUntilSearchingHasFinished();
+
+ const testQuery = 'from logstash-*';
+ await monacoEditor.setCodeEditorValue(testQuery);
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await discover.waitUntilSearchingHasFinished();
+
+ await discover.chooseBreakdownField('extension');
+ await header.waitUntilLoadingHasFinished();
+ const list = await discover.getHistogramLegendList();
+ expect(list).to.eql(['css', 'gif', 'jpg', 'php', 'png']);
+ });
+
+ it('should add filter using histogram legend values', async () => {
+ await discover.clickLegendFilter('png', '+');
+ await header.waitUntilLoadingHasFinished();
+ await header.waitUntilLoadingHasFinished();
+ await discover.waitUntilSearchingHasFinished();
+ await unifiedFieldList.waitUntilSidebarHasLoaded();
+
+ const editorValue = await monacoEditor.getCodeEditorValue();
+ expect(editorValue).to.eql(`from logstash-*\n| WHERE \`extension\`=="png"`);
+ });
+
+ it('should save breakdown field in saved search', async () => {
+ // revert the filter
+ const testQuery = 'from logstash-*';
+ await monacoEditor.setCodeEditorValue(testQuery);
+ await testSubjects.click('querySubmitButton');
+ await header.waitUntilLoadingHasFinished();
+ await discover.waitUntilSearchingHasFinished();
+
+ await discover.saveSearch('esql view with breakdown');
+
+ await discover.clickNewSearchButton();
+ await header.waitUntilLoadingHasFinished();
+ const prevList = await discover.getHistogramLegendList();
+ expect(prevList).to.eql([]);
+
+ await discover.loadSavedSearch('esql view with breakdown');
+ await header.waitUntilLoadingHasFinished();
+ const list = await discover.getHistogramLegendList();
+ expect(list).to.eql(['css', 'gif', 'jpg', 'php', 'png']);
+ });
+ });
});
}
diff --git a/test/functional/apps/discover/group3/_lens_vis.ts b/test/functional/apps/discover/group3/_lens_vis.ts
index b5907a97bb5ab..1bd6f8099fd22 100644
--- a/test/functional/apps/discover/group3/_lens_vis.ts
+++ b/test/functional/apps/discover/group3/_lens_vis.ts
@@ -56,7 +56,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await discover.getHitCount()).to.be(totalCount);
}
- async function checkESQLHistogramVis(timespan: string, totalCount: string) {
+ async function checkESQLHistogramVis(
+ timespan: string,
+ totalCount: string,
+ hasTransformationalCommand = false
+ ) {
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
@@ -64,7 +68,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.existOrFail('unifiedHistogramSaveVisualization');
await testSubjects.existOrFail('unifiedHistogramEditFlyoutVisualization');
await testSubjects.missingOrFail('unifiedHistogramEditVisualization');
- await testSubjects.missingOrFail('unifiedHistogramBreakdownSelectorButton');
+ if (hasTransformationalCommand) {
+ await testSubjects.missingOrFail('unifiedHistogramBreakdownSelectorButton');
+ } else {
+ await testSubjects.existOrFail('unifiedHistogramBreakdownSelectorButton');
+ }
await testSubjects.missingOrFail('unifiedHistogramTimeIntervalSelectorButton');
expect(await discover.getChartTimespan()).to.be(timespan);
expect(await discover.getHitCount()).to.be(totalCount);
@@ -310,7 +318,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
- await checkESQLHistogramVis(defaultTimespanESQL, '5');
+ await checkESQLHistogramVis(defaultTimespanESQL, '5', true);
await discover.chooseLensSuggestion('pie');
await testSubjects.existOrFail('unsavedChangesBadge');
@@ -359,7 +367,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
- await checkESQLHistogramVis(defaultTimespanESQL, '5');
+ await checkESQLHistogramVis(defaultTimespanESQL, '5', true);
await discover.chooseLensSuggestion('pie');
await testSubjects.existOrFail('unsavedChangesBadge');
@@ -412,7 +420,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
- await checkESQLHistogramVis(defaultTimespanESQL, '5');
+ await checkESQLHistogramVis(defaultTimespanESQL, '5', true);
await discover.chooseLensSuggestion('pie');
await testSubjects.existOrFail('unsavedChangesBadge');
@@ -456,7 +464,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
- await checkESQLHistogramVis(defaultTimespanESQL, '5');
+ await checkESQLHistogramVis(defaultTimespanESQL, '5', true);
await discover.chooseLensSuggestion('pie');
await discover.saveSearch('testCustomESQLVis');
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index 51bcbb4fed635..2d8dad7571c1a 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -24,6 +24,7 @@ import {
getAggregateQueryMode,
ExecutionContextSearch,
getLanguageDisplayName,
+ isOfAggregateQueryType,
} from '@kbn/es-query';
import type { PaletteOutput } from '@kbn/coloring';
import {
@@ -1406,7 +1407,13 @@ export class Embeddable
} else if (isLensTableRowContextMenuClickEvent(event)) {
eventHandler = this.input.onTableRowClick;
}
- const esqlQuery = this.isTextBasedLanguage() ? this.savedVis?.state.query : undefined;
+ // if the embeddable is located in an app where there is the Unified search bar with the ES|QL editor, then use this query
+ // otherwise use the query from the saved object
+ let esqlQuery: AggregateQuery | Query | undefined;
+ if (this.isTextBasedLanguage()) {
+ const query = this.deps.data.query.queryString.getQuery();
+ esqlQuery = isOfAggregateQueryType(query) ? query : this.savedVis?.state.query;
+ }
eventHandler?.({
...event.data,