diff --git a/src/plugins/discover/public/__mocks__/data_view.ts b/src/plugins/discover/public/__mocks__/data_view.ts
index bb57d9eb932ede..67950cd9f5e155 100644
--- a/src/plugins/discover/public/__mocks__/data_view.ts
+++ b/src/plugins/discover/public/__mocks__/data_view.ts
@@ -109,6 +109,7 @@ export const buildDataViewMock = ({
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
isTimeNanosBased: () => false,
isPersisted: () => true,
+ toSpec: () => ({}),
getTimeField: () => {
return dataViewFields.find((field) => field.name === timeFieldName);
},
diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts
index 8c1b10f236e9d4..b87c8aa32f9109 100644
--- a/src/plugins/discover/public/__mocks__/services.ts
+++ b/src/plugins/discover/public/__mocks__/services.ts
@@ -57,6 +57,14 @@ export function createDiscoverServicesMock(): DiscoverServices {
},
}));
dataPlugin.dataViews = createDiscoverDataViewsMock();
+ expressionsPlugin.run = jest.fn(() =>
+ of({
+ partial: false,
+ result: {
+ rows: [],
+ },
+ })
+ ) as unknown as typeof expressionsPlugin.run;
return {
core: coreMock.createStart(),
@@ -152,7 +160,14 @@ export function createDiscoverServicesMock(): DiscoverServices {
savedObjectsTagging: {},
dataViews: dataPlugin.dataViews,
timefilter: dataPlugin.query.timefilter.timefilter,
- lens: { EmbeddableComponent: jest.fn(() => null) },
+ lens: {
+ EmbeddableComponent: jest.fn(() => null),
+ stateHelperApi: jest.fn(() => {
+ return {
+ suggestions: jest.fn(),
+ };
+ }),
+ },
locator: {
useUrl: jest.fn(() => ''),
navigate: jest.fn(),
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx
index 5a74e58184ff06..04a7aed0ff2dea 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_histogram_layout.tsx
@@ -46,6 +46,7 @@ export const DiscoverHistogramLayout = ({
inspectorAdapters,
savedSearchFetch$: stateContainer.dataState.fetch$,
searchSessionId,
+ isPlainRecord,
...commonProps,
});
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
index 313f43679d6f9b..5027fc4cfe67af 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
@@ -163,20 +163,6 @@ describe('Discover component', () => {
).not.toBeNull();
}, 10000);
- test('sql query displays no chart toggle', async () => {
- const container = document.createElement('div');
- await mountComponent(
- dataViewWithTimefieldMock,
- false,
- { attachTo: container },
- { sql: 'SELECT * FROM test' },
- true
- );
- expect(
- container.querySelector('[data-test-subj="unifiedHistogramChartOptionsToggle"]')
- ).toBeNull();
- });
-
test('the saved search title h1 gains focus on navigate', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx
index 4686aaaca1f102..9bde350774bf4e 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx
@@ -72,12 +72,12 @@ export const DiscoverMainContent = ({
gutterSize="none"
responsive={false}
>
- {!isPlainRecord && (
-
-
+
+
+ {!isPlainRecord && (
-
- )}
+ )}
+
{dataState.error && (
{
foundDocuments: true,
}) as DataMain$,
savedSearchFetch$ = new Subject() as DataFetch$,
+ documents$ = new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)),
+ }) as DataDocuments$,
+ isPlainRecord = false,
}: {
stateContainer?: DiscoverStateContainer;
searchSessionId?: string;
@@ -119,12 +125,9 @@ describe('useDiscoverHistogram', () => {
totalHits$?: DataTotalHits$;
main$?: DataMain$;
savedSearchFetch$?: DataFetch$;
+ documents$?: DataDocuments$;
+ isPlainRecord?: boolean;
} = {}) => {
- const documents$ = new BehaviorSubject({
- fetchStatus: FetchStatus.COMPLETE,
- result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewWithTimefieldMock)),
- }) as DataDocuments$;
-
const availableFields$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
fields: [] as string[],
@@ -144,6 +147,7 @@ describe('useDiscoverHistogram', () => {
dataView: dataViewWithTimefieldMock,
inspectorAdapters,
searchSessionId,
+ isPlainRecord,
};
const Wrapper: WrapperComponent = ({ children }) => (
@@ -186,6 +190,7 @@ describe('useDiscoverHistogram', () => {
'timeRange',
'chartHidden',
'timeInterval',
+ 'columns',
'breakdownField',
'searchSessionId',
'totalHitsStatus',
@@ -196,6 +201,9 @@ describe('useDiscoverHistogram', () => {
});
describe('state', () => {
+ beforeEach(() => {
+ mockCheckHitCount.mockClear();
+ });
it('should subscribe to state changes', async () => {
const { hook } = await renderUseDiscoverHistogram();
const api = createMockUnifiedHistogramApi({ initialized: true });
@@ -203,7 +211,7 @@ describe('useDiscoverHistogram', () => {
act(() => {
hook.result.current.setUnifiedHistogramApi(api);
});
- expect(api.state$.subscribe).toHaveBeenCalledTimes(2);
+ expect(api.state$.subscribe).toHaveBeenCalledTimes(4);
});
it('should sync Unified Histogram state with the state container', async () => {
@@ -217,6 +225,7 @@ describe('useDiscoverHistogram', () => {
breakdownField: 'test',
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
+ dataView: dataViewWithTimefieldMock,
} as unknown as UnifiedHistogramState;
const api = createMockUnifiedHistogramApi({ initialized: true });
api.state$ = new BehaviorSubject({ ...state, lensRequestAdapter });
@@ -241,6 +250,7 @@ describe('useDiscoverHistogram', () => {
breakdownField: containerState.breakdownField,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
+ dataView: dataViewWithTimefieldMock,
} as unknown as UnifiedHistogramState;
const api = createMockUnifiedHistogramApi({ initialized: true });
api.state$ = new BehaviorSubject(state);
@@ -303,6 +313,7 @@ describe('useDiscoverHistogram', () => {
breakdownField: containerState.breakdownField,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
+ dataView: dataViewWithTimefieldMock,
} as unknown as UnifiedHistogramState;
const api = createMockUnifiedHistogramApi({ initialized: true });
let params: Partial = {};
@@ -347,7 +358,6 @@ describe('useDiscoverHistogram', () => {
});
it('should update total hits when the total hits state changes', async () => {
- mockCheckHitCount.mockClear();
const totalHits$ = new BehaviorSubject({
fetchStatus: FetchStatus.LOADING,
result: undefined,
@@ -366,6 +376,7 @@ describe('useDiscoverHistogram', () => {
breakdownField: containerState.breakdownField,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
+ dataView: dataViewWithTimefieldMock,
} as unknown as UnifiedHistogramState;
const api = createMockUnifiedHistogramApi({ initialized: true });
api.state$ = new BehaviorSubject({
@@ -384,7 +395,19 @@ describe('useDiscoverHistogram', () => {
});
it('should not update total hits when the total hits state changes to an error', async () => {
- mockCheckHitCount.mockClear();
+ mockQueryState = {
+ query: {
+ query: 'query',
+ language: 'kuery',
+ } as Query | AggregateQuery,
+ filters: [],
+ time: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ };
+
+ mockData.query.getState = () => mockQueryState;
const totalHits$ = new BehaviorSubject({
fetchStatus: FetchStatus.UNINITIALIZED,
result: undefined,
@@ -399,6 +422,7 @@ describe('useDiscoverHistogram', () => {
breakdownField: containerState.breakdownField,
totalHitsStatus: UnifiedHistogramFetchStatus.loading,
totalHitsResult: undefined,
+ dataView: dataViewWithTimefieldMock,
} as unknown as UnifiedHistogramState;
const api = createMockUnifiedHistogramApi({ initialized: true });
api.state$ = new BehaviorSubject({
diff --git a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts
index b1f583589c2aac..46d3ef314b3375 100644
--- a/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts
+++ b/src/plugins/discover/public/application/main/components/layout/use_discover_histogram.ts
@@ -15,17 +15,24 @@ import {
UnifiedHistogramState,
} from '@kbn/unified-histogram-plugin/public';
import { isEqual } from 'lodash';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { distinctUntilChanged, map, Observable } from 'rxjs';
+import { useCallback, useEffect, useRef, useMemo, useState } from 'react';
+import { distinctUntilChanged, filter, map, Observable, skip } from 'rxjs';
+import type { Suggestion } from '@kbn/lens-plugin/public';
+import useLatest from 'react-use/lib/useLatest';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { getUiActions } from '../../../../kibana_services';
import { FetchStatus } from '../../../types';
import { useDataState } from '../../hooks/use_data_state';
import type { InspectorAdapters } from '../../hooks/use_inspector';
-import type { DataFetch$, SavedSearchData } from '../../services/discover_data_state_container';
+import type {
+ DataDocuments$,
+ DataFetch$,
+ SavedSearchData,
+} from '../../services/discover_data_state_container';
import { checkHitCount, sendErrorTo } from '../../hooks/use_saved_search_messages';
import { useAppStateSelector } from '../../services/discover_app_state_container';
import type { DiscoverStateContainer } from '../../services/discover_state';
+import { addLog } from '../../../../utils/add_log';
export interface UseDiscoverHistogramProps {
stateContainer: DiscoverStateContainer;
@@ -34,6 +41,7 @@ export interface UseDiscoverHistogramProps {
inspectorAdapters: InspectorAdapters;
savedSearchFetch$: DataFetch$;
searchSessionId: string | undefined;
+ isPlainRecord: boolean;
}
export const useDiscoverHistogram = ({
@@ -43,6 +51,7 @@ export const useDiscoverHistogram = ({
inspectorAdapters,
savedSearchFetch$,
searchSessionId,
+ isPlainRecord,
}: UseDiscoverHistogramProps) => {
const services = useDiscoverServices();
const timefilter = services.data.query.timefilter.timefilter;
@@ -66,6 +75,7 @@ export const useDiscoverHistogram = ({
hideChart: chartHidden,
interval: timeInterval,
breakdownField,
+ columns,
} = stateContainer.appState.getState();
const { fetchStatus: totalHitsStatus, result: totalHitsResult } =
@@ -85,6 +95,7 @@ export const useDiscoverHistogram = ({
timeRange,
chartHidden,
timeInterval,
+ columns,
breakdownField,
searchSessionId,
totalHitsStatus: totalHitsStatus.toString() as UnifiedHistogramFetchStatus,
@@ -134,18 +145,8 @@ export const useDiscoverHistogram = ({
/**
* Update Unified Histgoram request params
*/
-
- const {
- query,
- filters,
- fromDate: from,
- toDate: to,
- } = useQuerySubscriber({ data: services.data });
-
- const timeRange = useMemo(
- () => (from && to ? { from, to } : timefilter.getTimeDefaults()),
- [timefilter, from, to]
- );
+ const { query, filters } = useQuerySubscriber({ data: services.data });
+ const timeRange = timefilter.getAbsoluteTime();
useEffect(() => {
unifiedHistogram?.setRequestParams({
@@ -214,6 +215,25 @@ export const useDiscoverHistogram = ({
unifiedHistogram?.setBreakdownField(breakdownField);
}, [breakdownField, unifiedHistogram]);
+ /**
+ * Columns
+ */
+
+ // Update the columns only when documents are fetched so the Lens suggestions
+ // don't constantly change when the user modifies the table columns
+ useEffect(() => {
+ const subscription = createDocumentsFetchedObservable(
+ stateContainer.dataState.data$.documents$
+ ).subscribe(({ textBasedQueryColumns }) => {
+ const columns = textBasedQueryColumns?.map(({ name }) => name) ?? [];
+ unifiedHistogram?.setColumns(columns);
+ });
+
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, [stateContainer.appState, stateContainer.dataState.data$.documents$, unifiedHistogram]);
+
/**
* Total hits
*/
@@ -260,41 +280,81 @@ export const useDiscoverHistogram = ({
}, [
savedSearchData$.main$,
savedSearchData$.totalHits$,
- services.data,
setTotalHitsError,
- unifiedHistogram,
+ unifiedHistogram?.state$,
]);
/**
* Data fetching
*/
- const skipRefetch = useRef();
+ const skipDiscoverRefetch = useRef();
+ const skipLensSuggestionRefetch = useRef();
+ const usingLensSuggestion = useLatest(isPlainRecord && !hideChart);
// Skip refetching when showing the chart since Lens will
// automatically fetch when the chart is shown
useEffect(() => {
- if (skipRefetch.current === undefined) {
- skipRefetch.current = false;
+ if (skipDiscoverRefetch.current === undefined) {
+ skipDiscoverRefetch.current = false;
} else {
- skipRefetch.current = !hideChart;
+ skipDiscoverRefetch.current = !hideChart;
}
}, [hideChart]);
// Trigger a unified histogram refetch when savedSearchFetch$ is triggered
useEffect(() => {
const subscription = savedSearchFetch$.subscribe(() => {
- if (!skipRefetch.current) {
+ if (!skipDiscoverRefetch.current) {
+ addLog('Unified Histogram - Discover refetch');
unifiedHistogram?.refetch();
}
- skipRefetch.current = false;
+ skipDiscoverRefetch.current = false;
});
return () => {
subscription.unsubscribe();
};
- }, [savedSearchFetch$, unifiedHistogram]);
+ }, [savedSearchFetch$, unifiedHistogram, usingLensSuggestion]);
+
+ // Reload the chart when the current suggestion changes
+ const [currentSuggestion, setCurrentSuggestion] = useState();
+
+ useEffect(() => {
+ if (!skipLensSuggestionRefetch.current && currentSuggestion && usingLensSuggestion.current) {
+ addLog('Unified Histogram - Lens suggestion refetch');
+ unifiedHistogram?.refetch();
+ }
+
+ skipLensSuggestionRefetch.current = false;
+ }, [currentSuggestion, unifiedHistogram, usingLensSuggestion]);
+
+ useEffect(() => {
+ const subscription = createCurrentSuggestionObservable(unifiedHistogram?.state$)?.subscribe(
+ setCurrentSuggestion
+ );
+
+ return () => {
+ subscription?.unsubscribe();
+ };
+ }, [unifiedHistogram]);
+
+ // When the data view or query changes, which will trigger a current suggestion change,
+ // skip the next refetch since we want to wait for the columns to update first, which
+ // doesn't happen until after the documents are fetched
+ useEffect(() => {
+ const subscription = createSkipFetchObservable(unifiedHistogram?.state$)?.subscribe(() => {
+ if (usingLensSuggestion.current) {
+ skipLensSuggestionRefetch.current = true;
+ skipDiscoverRefetch.current = true;
+ }
+ });
+
+ return () => {
+ subscription?.unsubscribe();
+ };
+ }, [unifiedHistogram?.state$, usingLensSuggestion]);
return { hideChart, setUnifiedHistogramApi };
};
@@ -316,9 +376,31 @@ const createStateSyncObservable = (state$?: Observable) =
);
};
+const createDocumentsFetchedObservable = (documents$: DataDocuments$) => {
+ return documents$.pipe(
+ distinctUntilChanged((prev, curr) => prev.fetchStatus === curr.fetchStatus),
+ filter(({ fetchStatus }) => fetchStatus === FetchStatus.COMPLETE)
+ );
+};
+
const createTotalHitsObservable = (state$?: Observable) => {
return state$?.pipe(
map((state) => ({ status: state.totalHitsStatus, result: state.totalHitsResult })),
distinctUntilChanged((prev, curr) => prev.status === curr.status && prev.result === curr.result)
);
};
+
+const createCurrentSuggestionObservable = (state$?: Observable) => {
+ return state$?.pipe(
+ map((state) => state.currentSuggestion),
+ distinctUntilChanged(isEqual)
+ );
+};
+
+const createSkipFetchObservable = (state$?: Observable) => {
+ return state$?.pipe(
+ map((state) => [state.dataView.id, state.query]),
+ distinctUntilChanged(isEqual),
+ skip(1)
+ );
+};
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
index 28cb0b9bfde279..f415ebd0606a55 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
@@ -262,12 +262,4 @@ describe('discover sidebar', function () {
const createDataViewButton = findTestSubject(compWithPickerInViewerMode, 'dataview-create-new');
expect(createDataViewButton.length).toBe(0);
});
-
- it('should render the Visualize in Lens button in text based languages mode', async () => {
- const compInViewerMode = await mountComponent(getCompProps(), {
- query: { sql: 'SELECT * FROM test' },
- });
- const visualizeField = findTestSubject(compInViewerMode, 'textBased-visualize');
- expect(visualizeField.length).toBe(1);
- });
});
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
index 92cc33852ebc0f..2bea887c21a625 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
@@ -15,7 +15,6 @@ import {
EuiFlexItem,
EuiPageSideBar_Deprecated as EuiPageSideBar,
} from '@elastic/eui';
-import { isOfAggregateQueryType } from '@kbn/es-query';
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
import { type DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public';
import {
@@ -25,14 +24,13 @@ import {
FieldListGroupedProps,
FieldsGroupNames,
GroupedFieldsParams,
- triggerVisualizeActionsTextBasedLanguages,
useGroupedFields,
} from '@kbn/unified-field-list-plugin/public';
import { VIEW_MODE } from '../../../../../common/constants';
import { useAppStateSelector } from '../../services/discover_app_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverField } from './discover_field';
-import { FIELDS_LIMIT_SETTING, PLUGIN_ID } from '../../../../../common';
+import { FIELDS_LIMIT_SETTING } from '../../../../../common';
import {
getSelectedFields,
shouldShowField,
@@ -40,7 +38,6 @@ import {
INITIAL_SELECTED_FIELDS_RESULT,
} from './lib/group_fields';
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
-import { getUiActions } from '../../../../kibana_services';
import { getRawRecordType } from '../../utils/get_raw_record_type';
import { RecordRawType } from '../../services/discover_data_state_container';
@@ -127,7 +124,6 @@ export function DiscoverSidebarComponent({
const isPlainRecord = useAppStateSelector(
(state) => getRawRecordType(state.query) === RecordRawType.PLAIN
);
- const query = useAppStateSelector((state) => state.query);
const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]);
const [selectedFieldsState, setSelectedFieldsState] = useState(
@@ -194,17 +190,6 @@ export function DiscoverSidebarComponent({
]
);
- const visualizeAggregateQuery = useCallback(() => {
- const aggregateQuery = query && isOfAggregateQueryType(query) ? query : undefined;
- triggerVisualizeActionsTextBasedLanguages(
- getUiActions(),
- columns,
- PLUGIN_ID,
- selectedDataView,
- aggregateQuery
- );
- }, [columns, selectedDataView, query]);
-
const popularFieldsLimit = useMemo(() => uiSettings.get(FIELDS_LIMIT_SETTING), [uiSettings]);
const onSupportedFieldFilter: GroupedFieldsParams['onSupportedFieldFilter'] =
useCallback(
@@ -342,20 +327,6 @@ export function DiscoverSidebarComponent({
)}
- {isPlainRecord && (
-
-
- {i18n.translate('discover.textBasedLanguages.visualize.label', {
- defaultMessage: 'Visualize in Lens',
- })}
-
-
- )}
diff --git a/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
index 356939e4f2dfd6..595d54420e7ad6 100644
--- a/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
+++ b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
@@ -15,7 +15,11 @@ import {
import { RecordRawType } from '../services/discover_data_state_container';
export function getRawRecordType(query?: Query | AggregateQuery) {
- if (query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql') {
+ if (
+ query &&
+ isOfAggregateQueryType(query) &&
+ (getAggregateQueryMode(query) === 'sql' || getAggregateQueryMode(query) === 'esql')
+ ) {
return RecordRawType.PLAIN;
}
diff --git a/src/plugins/unified_histogram/kibana.jsonc b/src/plugins/unified_histogram/kibana.jsonc
index a8aa44de27621e..e60b6ed3363ea2 100644
--- a/src/plugins/unified_histogram/kibana.jsonc
+++ b/src/plugins/unified_histogram/kibana.jsonc
@@ -7,11 +7,6 @@
"id": "unifiedHistogram",
"server": false,
"browser": true,
- "requiredBundles": [
- "data",
- "dataViews",
- "embeddable",
- "inspector"
- ]
+ "requiredBundles": ["data", "dataViews", "embeddable", "inspector", "expressions"]
}
}
diff --git a/src/plugins/unified_histogram/public/__mocks__/services.ts b/src/plugins/unified_histogram/public/__mocks__/services.ts
index e771ab7ea8c1ea..f882b2168a7700 100644
--- a/src/plugins/unified_histogram/public/__mocks__/services.ts
+++ b/src/plugins/unified_histogram/public/__mocks__/services.ts
@@ -8,8 +8,10 @@
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
+import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import type { UnifiedHistogramServices } from '../types';
+import { allSuggestionsMock } from './suggestions';
const dataPlugin = dataPluginMock.createStartContract();
dataPlugin.query.filterManager.getFilters = jest.fn(() => []);
@@ -28,11 +30,20 @@ export const unifiedHistogramServicesMock = {
useChartsTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
useChartsBaseTheme: jest.fn(() => EUI_CHARTS_THEME_LIGHT.theme),
},
- lens: { EmbeddableComponent: jest.fn(() => null), navigateToPrefilledEditor: jest.fn() },
+ lens: {
+ EmbeddableComponent: jest.fn(() => null),
+ navigateToPrefilledEditor: jest.fn(),
+ stateHelperApi: jest.fn(() => {
+ return {
+ suggestions: jest.fn(() => allSuggestionsMock),
+ };
+ }),
+ },
storage: {
get: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
clear: jest.fn(),
},
+ expressions: expressionsPluginMock.createStartContract(),
} as unknown as UnifiedHistogramServices;
diff --git a/src/plugins/unified_histogram/public/__mocks__/suggestions.ts b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts
new file mode 100644
index 00000000000000..0a92393f60ec34
--- /dev/null
+++ b/src/plugins/unified_histogram/public/__mocks__/suggestions.ts
@@ -0,0 +1,292 @@
+/*
+ * 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 type { Suggestion } from '@kbn/lens-plugin/public';
+
+export const currentSuggestionMock = {
+ title: 'Heat map',
+ hide: false,
+ score: 0.6,
+ previewIcon: 'heatmap',
+ visualizationId: 'lnsHeatmap',
+ visualizationState: {
+ shape: 'heatmap',
+ layerId: '46aa21fa-b747-4543-bf90-0b40007c546d',
+ layerType: 'data',
+ legend: {
+ isVisible: true,
+ position: 'right',
+ type: 'heatmap_legend',
+ },
+ gridConfig: {
+ type: 'heatmap_grid',
+ isCellLabelVisible: false,
+ isYAxisLabelVisible: true,
+ isXAxisLabelVisible: true,
+ isYAxisTitleVisible: false,
+ isXAxisTitleVisible: false,
+ },
+ valueAccessor: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
+ xAccessor: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
+ },
+ keptLayerIds: ['46aa21fa-b747-4543-bf90-0b40007c546d'],
+ datasourceState: {
+ layers: {
+ '46aa21fa-b747-4543-bf90-0b40007c546d': {
+ index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
+ query: {
+ sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
+ },
+ columns: [
+ {
+ columnId: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
+ fieldName: 'Dest',
+ meta: {
+ type: 'string',
+ },
+ },
+ {
+ columnId: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
+ fieldName: 'AvgTicketPrice',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ allColumns: [
+ {
+ columnId: '81e332d6-ee37-42a8-a646-cea4fc75d2d3',
+ fieldName: 'Dest',
+ meta: {
+ type: 'string',
+ },
+ },
+ {
+ columnId: '5b9b8b76-0836-4a12-b9c0-980c9900502f',
+ fieldName: 'AvgTicketPrice',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ timeField: 'timestamp',
+ },
+ },
+ fieldList: [],
+ indexPatternRefs: [],
+ initialContext: {
+ dataViewSpec: {
+ id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
+ version: 'WzM1ODA3LDFd',
+ title: 'kibana_sample_data_flights',
+ timeFieldName: 'timestamp',
+ sourceFilters: [],
+ fields: {
+ AvgTicketPrice: {
+ count: 0,
+ name: 'AvgTicketPrice',
+ type: 'number',
+ esTypes: ['float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'number',
+ params: {
+ pattern: '$0,0.[00]',
+ },
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ Dest: {
+ count: 0,
+ name: 'Dest',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'string',
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ timestamp: {
+ count: 0,
+ name: 'timestamp',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'date',
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ },
+ allowNoIndex: false,
+ name: 'Kibana Sample Data Flights',
+ },
+ fieldName: '',
+ contextualFields: ['Dest', 'AvgTicketPrice'],
+ query: {
+ sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
+ },
+ },
+ },
+ datasourceId: 'textBased',
+ columns: 2,
+ changeType: 'initial',
+} as Suggestion;
+
+export const allSuggestionsMock = [
+ currentSuggestionMock,
+ {
+ title: 'Donut',
+ score: 0.46,
+ visualizationId: 'lnsPie',
+ previewIcon: 'pie',
+ visualizationState: {
+ shape: 'donut',
+ layers: [
+ {
+ layerId: '2513a3d4-ad9d-48ea-bd58-8b6419ab97e6',
+ primaryGroups: ['923f0681-3fe1-4987-aa27-d9c91fb95fa6'],
+ metrics: ['b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0'],
+ numberDisplay: 'percent',
+ categoryDisplay: 'default',
+ legendDisplay: 'default',
+ nestedLegend: false,
+ layerType: 'data',
+ },
+ ],
+ },
+ keptLayerIds: ['2513a3d4-ad9d-48ea-bd58-8b6419ab97e6'],
+ datasourceState: {
+ layers: {
+ '2513a3d4-ad9d-48ea-bd58-8b6419ab97e6': {
+ index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
+ query: {
+ sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
+ },
+ columns: [
+ {
+ columnId: '923f0681-3fe1-4987-aa27-d9c91fb95fa6',
+ fieldName: 'Dest',
+ meta: {
+ type: 'string',
+ },
+ },
+ {
+ columnId: 'b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0',
+ fieldName: 'AvgTicketPrice',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ allColumns: [
+ {
+ columnId: '923f0681-3fe1-4987-aa27-d9c91fb95fa6',
+ fieldName: 'Dest',
+ meta: {
+ type: 'string',
+ },
+ },
+ {
+ columnId: 'b5f41c04-4bca-4abe-ae5c-b1d4d6fb00e0',
+ fieldName: 'AvgTicketPrice',
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ timeField: 'timestamp',
+ },
+ },
+ fieldList: [],
+ indexPatternRefs: [],
+ initialContext: {
+ dataViewSpec: {
+ id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
+ version: 'WzM1ODA3LDFd',
+ title: 'kibana_sample_data_flights',
+ timeFieldName: 'timestamp',
+ sourceFilters: [],
+ fields: {
+ AvgTicketPrice: {
+ count: 0,
+ name: 'AvgTicketPrice',
+ type: 'number',
+ esTypes: ['float'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'number',
+ params: {
+ pattern: '$0,0.[00]',
+ },
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ Dest: {
+ count: 0,
+ name: 'Dest',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'string',
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ timestamp: {
+ count: 0,
+ name: 'timestamp',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ format: {
+ id: 'date',
+ },
+ shortDotsEnable: false,
+ isMapped: true,
+ },
+ },
+ typeMeta: {},
+ allowNoIndex: false,
+ name: 'Kibana Sample Data Flights',
+ },
+ fieldName: '',
+ contextualFields: ['Dest', 'AvgTicketPrice'],
+ query: {
+ sql: 'SELECT Dest, AvgTicketPrice FROM "kibana_sample_data_flights"',
+ },
+ },
+ },
+ datasourceId: 'textBased',
+ columns: 2,
+ changeType: 'unchanged',
+ } as Suggestion,
+];
diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx
index 151b0f2408150e..428db01cc37737 100644
--- a/src/plugins/unified_histogram/public/chart/chart.test.tsx
+++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx
@@ -10,6 +10,7 @@ import React, { ReactElement } from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { DataView } from '@kbn/data-views-plugin/public';
+import type { Suggestion } from '@kbn/lens-plugin/public';
import type { UnifiedHistogramFetchStatus } from '../types';
import { Chart } from './chart';
import type { ReactWrapper } from 'enzyme';
@@ -20,6 +21,9 @@ import { HitsCounter } from '../hits_counter';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { dataViewMock } from '../__mocks__/data_view';
import { BreakdownFieldSelector } from './breakdown_field_selector';
+import { SuggestionSelector } from './suggestion_selector';
+
+import { currentSuggestionMock, allSuggestionsMock } from '../__mocks__/suggestions';
let mockUseEditVisualization: jest.Mock | undefined = jest.fn();
@@ -34,6 +38,8 @@ async function mountComponent({
chartHidden = false,
appendHistogram,
dataView = dataViewWithTimefieldMock,
+ currentSuggestion,
+ allSuggestions,
}: {
noChart?: boolean;
noHits?: boolean;
@@ -41,6 +47,8 @@ async function mountComponent({
chartHidden?: boolean;
appendHistogram?: ReactElement;
dataView?: DataView;
+ currentSuggestion?: Suggestion;
+ allSuggestions?: Suggestion[];
} = {}) {
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: noHits ? 0 : 2 } } }))
@@ -74,6 +82,8 @@ async function mountComponent({
},
},
breakdown: noBreakdown ? undefined : { field: undefined },
+ currentSuggestion,
+ allSuggestions,
appendHistogram,
onResetChartHeight: jest.fn(),
onChartHiddenChange: jest.fn(),
@@ -191,4 +201,26 @@ describe('Chart', () => {
const component = await mountComponent({ noBreakdown: true });
expect(component.find(BreakdownFieldSelector).exists()).toBeFalsy();
});
+
+ it('should render the Lens SuggestionsSelector when chart is visible and suggestions exist', async () => {
+ const component = await mountComponent({
+ currentSuggestion: currentSuggestionMock,
+ allSuggestions: allSuggestionsMock,
+ });
+ expect(component.find(SuggestionSelector).exists()).toBeTruthy();
+ });
+
+ it('should not render the Lens SuggestionsSelector when chart is hidden', async () => {
+ const component = await mountComponent({
+ chartHidden: true,
+ currentSuggestion: currentSuggestionMock,
+ allSuggestions: allSuggestionsMock,
+ });
+ expect(component.find(SuggestionSelector).exists()).toBeFalsy();
+ });
+
+ it('should not render the Lens SuggestionsSelector when chart is visible and suggestions are undefined', async () => {
+ const component = await mountComponent({ currentSuggestion: currentSuggestionMock });
+ expect(component.find(SuggestionSelector).exists()).toBeFalsy();
+ });
});
diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx
index 4c590217d30e0f..038c7c696ddf86 100644
--- a/src/plugins/unified_histogram/public/chart/chart.tsx
+++ b/src/plugins/unified_histogram/public/chart/chart.tsx
@@ -17,6 +17,7 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import type { Suggestion } from '@kbn/lens-plugin/public';
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import type { LensEmbeddableInput } from '@kbn/lens-plugin/public';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
@@ -36,6 +37,7 @@ import type {
UnifiedHistogramInputMessage,
} from '../types';
import { BreakdownFieldSelector } from './breakdown_field_selector';
+import { SuggestionSelector } from './suggestion_selector';
import { useTotalHits } from './hooks/use_total_hits';
import { useRequestParams } from './hooks/use_request_params';
import { useChartStyles } from './hooks/use_chart_styles';
@@ -50,6 +52,9 @@ export interface ChartProps {
dataView: DataView;
query?: Query | AggregateQuery;
filters?: Filter[];
+ isPlainRecord?: boolean;
+ currentSuggestion?: Suggestion;
+ allSuggestions?: Suggestion[];
timeRange?: TimeRange;
request?: UnifiedHistogramRequestContext;
hits?: UnifiedHistogramHitsContext;
@@ -66,6 +71,7 @@ export interface ChartProps {
onChartHiddenChange?: (chartHidden: boolean) => void;
onTimeIntervalChange?: (timeInterval: string) => void;
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
+ onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
onChartLoad?: (event: UnifiedHistogramChartLoadEvent) => void;
onFilter?: LensEmbeddableInput['onFilter'];
@@ -85,6 +91,9 @@ export function Chart({
hits,
chart,
breakdown,
+ currentSuggestion,
+ allSuggestions,
+ isPlainRecord,
appendHitsCounter,
appendHistogram,
disableAutoFetching,
@@ -95,6 +104,7 @@ export function Chart({
onResetChartHeight,
onChartHiddenChange,
onTimeIntervalChange,
+ onSuggestionChange,
onBreakdownFieldChange,
onTotalHitsChange,
onChartLoad,
@@ -118,6 +128,7 @@ export function Chart({
onTimeIntervalChange,
closePopover: closeChartOptions,
onResetChartHeight,
+ isPlainRecord,
});
const chartVisible = !!(
@@ -150,6 +161,7 @@ export function Chart({
filters,
query,
relativeTimeRange,
+ currentSuggestion,
disableAutoFetching,
input$,
beforeRefetch: updateTimeRange,
@@ -166,6 +178,7 @@ export function Chart({
getTimeRange,
refetch$,
onTotalHitsChange,
+ isPlainRecord,
});
const {
@@ -188,8 +201,17 @@ export function Chart({
dataView,
timeInterval: chart?.timeInterval,
breakdownField: breakdown?.field,
+ suggestion: currentSuggestion,
}),
- [breakdown?.field, chart?.timeInterval, chart?.title, dataView, filters, query]
+ [
+ breakdown?.field,
+ chart?.timeInterval,
+ chart?.title,
+ currentSuggestion,
+ dataView,
+ filters,
+ query,
+ ]
);
const getRelativeTimeRange = useMemo(
@@ -244,6 +266,15 @@ export function Chart({
/>
)}
+ {chartVisible && currentSuggestion && allSuggestions && allSuggestions?.length > 1 && (
+
+
+
+ )}
{onEditVisualization && (
dataView: dataViewWithTimefieldMock,
timeInterval: 'auto',
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
+ suggestion: undefined,
});
function mountComponent() {
diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx
index ab905e33464b0d..d3bb87a2c3f10f 100644
--- a/src/plugins/unified_histogram/public/chart/histogram.tsx
+++ b/src/plugins/unified_histogram/public/chart/histogram.tsx
@@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
-import { useEuiTheme } from '@elastic/eui';
+import { useEuiTheme, useResizeObserver } from '@elastic/eui';
import { css } from '@emotion/react';
-import React, { useState } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import type { IKibanaSearchResponse } from '@kbn/data-plugin/public';
@@ -38,6 +38,7 @@ export interface HistogramProps {
request?: UnifiedHistogramRequestContext;
hits?: UnifiedHistogramHitsContext;
chart: UnifiedHistogramChartContext;
+ isPlainRecord?: boolean;
getTimeRange: () => TimeRange;
refetch$: Observable;
lensAttributes: TypedLensByValueInput['attributes'];
@@ -55,6 +56,7 @@ export function Histogram({
request,
hits,
chart: { timeInterval },
+ isPlainRecord,
getTimeRange,
refetch$,
lensAttributes: attributes,
@@ -66,12 +68,24 @@ export function Histogram({
onBrushEnd,
}: HistogramProps) {
const [bucketInterval, setBucketInterval] = useState();
+ const [chartSize, setChartSize] = useState('100%');
const { timeRangeText, timeRangeDisplay } = useTimeRange({
uiSettings,
bucketInterval,
timeRange: getTimeRange(),
timeInterval,
+ isPlainRecord,
});
+ const chartRef = useRef(null);
+ const { height: containerHeight, width: containerWidth } = useResizeObserver(chartRef.current);
+ useEffect(() => {
+ if (attributes.visualizationType === 'lnsMetric') {
+ const size = containerHeight < containerWidth ? containerHeight : containerWidth;
+ setChartSize(`${size}px`);
+ } else {
+ setChartSize('100%');
+ }
+ }, [attributes, containerHeight, containerWidth]);
const onLoad = useStableCallback(
(isLoading: boolean, adapters: Partial | undefined) => {
@@ -91,7 +105,10 @@ export function Histogram({
return;
}
- const totalHits = adapters?.tables?.tables?.unifiedHistogram?.meta?.statistics?.totalCount;
+ const adapterTables = adapters?.tables?.tables;
+ const totalHits = isPlainRecord
+ ? Object.values(adapterTables ?? {})?.[0]?.rows?.length
+ : adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount;
onTotalHitsChange?.(
isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete,
@@ -129,6 +146,13 @@ export function Histogram({
& > div {
height: 100%;
+ position: absolute;
+ width: 100%;
+ }
+
+ & .lnsExpressionRenderer {
+ width: ${chartSize};
+ margin: auto;
}
& .echLegend .echLegendList {
@@ -145,7 +169,12 @@ export function Histogram({
return (
<>
-
+
{
(resetChartHeightButton.onClick as Function)();
expect(onResetChartHeight).toBeCalled();
});
+ test('useChartsPanel when isPlainRecord', async () => {
+ const { result } = renderHook(() => {
+ return useChartPanels({
+ toggleHideChart: jest.fn(),
+ onTimeIntervalChange: jest.fn(),
+ closePopover: jest.fn(),
+ onResetChartHeight: jest.fn(),
+ isPlainRecord: true,
+ chart: {
+ hidden: true,
+ timeInterval: 'auto',
+ },
+ });
+ });
+ const panels: EuiContextMenuPanelDescriptor[] = result.current;
+ const panel0: EuiContextMenuPanelDescriptor = result.current[0];
+ expect(panels.length).toBe(1);
+ expect(panel0!.items).toHaveLength(1);
+ expect(panel0!.items![0].icon).toBe('eye');
+ });
});
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts b/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts
index 6c6921ba09d6ff..bf1bf4d6b95cdd 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_panels.ts
@@ -20,12 +20,14 @@ export function useChartPanels({
onTimeIntervalChange,
closePopover,
onResetChartHeight,
+ isPlainRecord,
}: {
chart?: UnifiedHistogramChartContext;
toggleHideChart: () => void;
onTimeIntervalChange?: (timeInterval: string) => void;
closePopover: () => void;
onResetChartHeight?: () => void;
+ isPlainRecord?: boolean;
}) {
if (!chart) {
return [];
@@ -71,16 +73,18 @@ export function useChartPanels({
});
}
- mainPanelItems.push({
- name: i18n.translate('unifiedHistogram.timeIntervalWithValue', {
- defaultMessage: 'Time interval: {timeInterval}',
- values: {
- timeInterval: intervalDisplay,
- },
- }),
- panel: 1,
- 'data-test-subj': 'unifiedHistogramTimeIntervalPanel',
- });
+ if (!isPlainRecord) {
+ mainPanelItems.push({
+ name: i18n.translate('unifiedHistogram.timeIntervalWithValue', {
+ defaultMessage: 'Time interval: {timeInterval}',
+ values: {
+ timeInterval: intervalDisplay,
+ },
+ }),
+ panel: 1,
+ 'data-test-subj': 'unifiedHistogramTimeIntervalPanel',
+ });
+ }
}
const panels: EuiContextMenuPanelDescriptor[] = [
@@ -92,7 +96,7 @@ export function useChartPanels({
items: mainPanelItems,
},
];
- if (!chart.hidden) {
+ if (!chart.hidden && !isPlainRecord) {
panels.push({
id: 1,
initialFocusedItemIndex: selectedOptionIdx > -1 ? selectedOptionIdx : 0,
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_lens_props.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_lens_props.test.ts
index bae6b9ca50b22a..8d13ec7eb8102e 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_lens_props.test.ts
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_lens_props.test.ts
@@ -11,6 +11,7 @@ import { act } from 'react-test-renderer';
import { Subject } from 'rxjs';
import type { UnifiedHistogramInputMessage } from '../../types';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
+import { currentSuggestionMock } from '../../__mocks__/suggestions';
import { getLensAttributes } from '../utils/get_lens_attributes';
import { getLensProps, useLensProps } from './use_lens_props';
@@ -29,6 +30,45 @@ describe('useLensProps', () => {
dataView: dataViewWithTimefieldMock,
timeInterval: 'auto',
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
+ suggestion: undefined,
+ });
+ const lensProps = renderHook(() => {
+ return useLensProps({
+ request: {
+ searchSessionId: 'id',
+ adapter: undefined,
+ },
+ getTimeRange,
+ refetch$,
+ attributes,
+ onLoad,
+ });
+ });
+ expect(lensProps.result.current).toEqual(
+ getLensProps({
+ searchSessionId: 'id',
+ getTimeRange,
+ attributes,
+ onLoad,
+ })
+ );
+ });
+
+ it('should return lens props for text based languages', () => {
+ const getTimeRange = jest.fn();
+ const refetch$ = new Subject();
+ const onLoad = jest.fn();
+ const attributes = getLensAttributes({
+ title: 'test',
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: '',
+ },
+ dataView: dataViewWithTimefieldMock,
+ timeInterval: 'auto',
+ breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
+ suggestion: currentSuggestionMock,
});
const lensProps = renderHook(() => {
return useLensProps({
@@ -73,6 +113,7 @@ describe('useLensProps', () => {
dataView: dataViewWithTimefieldMock,
timeInterval: 'auto',
breakdownField: dataViewWithTimefieldMock.getFieldByName('extension'),
+ suggestion: undefined,
}),
onLoad,
};
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_refetch.ts b/src/plugins/unified_histogram/public/chart/hooks/use_refetch.ts
index 344526860477f3..776effd65fb231 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_refetch.ts
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_refetch.ts
@@ -8,6 +8,7 @@
import type { DataView } from '@kbn/data-views-plugin/common';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
+import type { Suggestion } from '@kbn/lens-plugin/public';
import { cloneDeep, isEqual } from 'lodash';
import { useEffect, useMemo, useRef } from 'react';
import { filter, share, tap } from 'rxjs';
@@ -29,6 +30,7 @@ export const useRefetch = ({
filters,
query,
relativeTimeRange,
+ currentSuggestion,
disableAutoFetching,
input$,
beforeRefetch,
@@ -42,6 +44,7 @@ export const useRefetch = ({
filters: Filter[];
query: Query | AggregateQuery;
relativeTimeRange: TimeRange;
+ currentSuggestion?: Suggestion;
disableAutoFetching?: boolean;
input$: UnifiedHistogramInput$;
beforeRefetch: () => void;
@@ -67,6 +70,7 @@ export const useRefetch = ({
filters,
query,
relativeTimeRange,
+ currentSuggestion,
});
if (!isEqual(refetchDeps.current, newRefetchDeps)) {
@@ -80,6 +84,7 @@ export const useRefetch = ({
breakdown,
chart,
chartVisible,
+ currentSuggestion,
dataView,
disableAutoFetching,
filters,
@@ -111,6 +116,7 @@ const getRefetchDeps = ({
filters,
query,
relativeTimeRange,
+ currentSuggestion,
}: {
dataView: DataView;
request: UnifiedHistogramRequestContext | undefined;
@@ -121,6 +127,7 @@ const getRefetchDeps = ({
filters: Filter[];
query: Query | AggregateQuery;
relativeTimeRange: TimeRange;
+ currentSuggestion?: Suggestion;
}) =>
cloneDeep([
dataView.id,
@@ -133,4 +140,5 @@ const getRefetchDeps = ({
filters,
query,
relativeTimeRange,
+ currentSuggestion?.visualizationId,
]);
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_time_range.test.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_time_range.test.tsx
index b32681aaf69461..176d69d984290e 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_time_range.test.tsx
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_time_range.test.tsx
@@ -236,4 +236,35 @@ describe('useTimeRange', () => {
`);
});
+
+ it('should render time range display and no interval for text based languages', () => {
+ const { result } = renderHook(() =>
+ useTimeRange({
+ uiSettings,
+ bucketInterval,
+ timeRange,
+ timeInterval,
+ isPlainRecord: true,
+ })
+ );
+ expect(result.current.timeRangeDisplay).toMatchInlineSnapshot(`
+
+ 2022-11-17T00:00:00.000Z - 2022-11-17T12:00:00.000Z
+
+ `);
+ });
});
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_time_range.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_time_range.tsx
index 8f7786b2c6be7d..089df5124b60a7 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_time_range.tsx
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_time_range.tsx
@@ -20,11 +20,13 @@ export const useTimeRange = ({
bucketInterval,
timeRange: { from, to },
timeInterval,
+ isPlainRecord,
}: {
uiSettings: IUiSettingsClient;
bucketInterval?: UnifiedHistogramBucketInterval;
timeRange: TimeRange;
timeInterval?: string;
+ isPlainRecord?: boolean;
}) => {
const dateFormat = useMemo(() => uiSettings.get('dateFormat'), [uiSettings]);
@@ -47,26 +49,28 @@ export const useTimeRange = ({
to: dateMath.parse(to, { roundUp: true }),
};
- const intervalText = i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', {
- defaultMessage: '(interval: {value})',
- values: {
- value: `${
- timeInterval === 'auto'
- ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', {
- defaultMessage: 'Auto',
- })} - `
- : ''
- }${
- bucketInterval?.description ??
- i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', {
- defaultMessage: 'Loading',
- })
- }`,
- },
- });
+ const intervalText = Boolean(isPlainRecord)
+ ? ''
+ : i18n.translate('unifiedHistogram.histogramTimeRangeIntervalDescription', {
+ defaultMessage: '(interval: {value})',
+ values: {
+ value: `${
+ timeInterval === 'auto'
+ ? `${i18n.translate('unifiedHistogram.histogramTimeRangeIntervalAuto', {
+ defaultMessage: 'Auto',
+ })} - `
+ : ''
+ }${
+ bucketInterval?.description ??
+ i18n.translate('unifiedHistogram.histogramTimeRangeIntervalLoading', {
+ defaultMessage: 'Loading',
+ })
+ }`,
+ },
+ });
return `${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${intervalText}`;
- }, [bucketInterval, from, timeInterval, to, toMoment]);
+ }, [bucketInterval?.description, from, isPlainRecord, timeInterval, to, toMoment]);
const { euiTheme } = useEuiTheme();
const timeRangeCss = css`
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts
index fae169c41f2e76..187bf767510072 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.test.ts
@@ -18,6 +18,7 @@ import { of, Subject, throwError } from 'rxjs';
import { waitFor } from '@testing-library/dom';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { DataViewType, SearchSourceSearchOptions } from '@kbn/data-plugin/common';
+import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
jest.mock('react-use/lib/useDebounce', () => {
return jest.fn((...args) => {
@@ -29,7 +30,20 @@ describe('useTotalHits', () => {
const timeRange = { from: 'now-15m', to: 'now' };
const refetch$: UnifiedHistogramInput$ = new Subject();
const getDeps = () => ({
- services: { data: dataPluginMock.createStartContract() } as any,
+ services: {
+ data: dataPluginMock.createStartContract(),
+ expressions: {
+ ...expressionsPluginMock.createStartContract(),
+ run: jest.fn(() =>
+ of({
+ partial: false,
+ result: {
+ rows: [{}, {}, {}],
+ },
+ })
+ ),
+ },
+ } as any,
dataView: dataViewWithTimefieldMock,
request: undefined,
hits: {
@@ -103,6 +117,22 @@ describe('useTotalHits', () => {
});
});
+ it('should fetch total hits if isPlainRecord is true', async () => {
+ const onTotalHitsChange = jest.fn();
+ const deps = {
+ ...getDeps(),
+ isPlainRecord: true,
+ onTotalHitsChange,
+ query: { sql: 'select * from test' },
+ };
+ renderHook(() => useTotalHits(deps));
+ expect(onTotalHitsChange).toBeCalledTimes(1);
+ await waitFor(() => {
+ expect(deps.services.expressions.run).toBeCalledTimes(1);
+ expect(onTotalHitsChange).toBeCalledWith(UnifiedHistogramFetchStatus.complete, 3);
+ });
+ });
+
it('should not fetch total hits if chartVisible is true', async () => {
const onTotalHitsChange = jest.fn();
const fetchSpy = jest.spyOn(searchSourceInstanceMock, 'fetch$').mockClear();
diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts
index 95916c1f2bc022..6903cdf6b4256b 100644
--- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts
+++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts
@@ -6,13 +6,15 @@
* Side Public License, v 1.
*/
+import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';
import { isCompleteResponse } from '@kbn/data-plugin/public';
import { DataView, DataViewType } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
+import { Datatable, isExpressionValueError } from '@kbn/expressions-plugin/common';
import { i18n } from '@kbn/i18n';
import { MutableRefObject, useEffect, useRef } from 'react';
import useEffectOnce from 'react-use/lib/useEffectOnce';
-import { catchError, filter, lastValueFrom, map, Observable, of } from 'rxjs';
+import { catchError, filter, lastValueFrom, map, Observable, of, pluck } from 'rxjs';
import {
UnifiedHistogramFetchStatus,
UnifiedHistogramHitsContext,
@@ -33,6 +35,7 @@ export const useTotalHits = ({
getTimeRange,
refetch$,
onTotalHitsChange,
+ isPlainRecord,
}: {
services: UnifiedHistogramServices;
dataView: DataView;
@@ -44,6 +47,7 @@ export const useTotalHits = ({
getTimeRange: () => TimeRange;
refetch$: Observable;
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
+ isPlainRecord?: boolean;
}) => {
const abortController = useRef();
const fetch = useStableCallback(() => {
@@ -58,6 +62,7 @@ export const useTotalHits = ({
query,
timeRange: getTimeRange(),
onTotalHitsChange,
+ isPlainRecord,
});
});
@@ -70,16 +75,17 @@ export const useTotalHits = ({
};
const fetchTotalHits = async ({
- services: { data },
+ services,
abortController,
dataView,
request,
hits,
chartVisible,
- filters: originalFilters,
+ filters,
query,
timeRange,
onTotalHitsChange,
+ isPlainRecord,
}: {
services: UnifiedHistogramServices;
abortController: MutableRefObject;
@@ -91,6 +97,7 @@ const fetchTotalHits = async ({
query: Query | AggregateQuery;
timeRange: TimeRange;
onTotalHitsChange?: (status: UnifiedHistogramFetchStatus, result?: number | Error) => void;
+ isPlainRecord?: boolean;
}) => {
abortController.current?.abort();
abortController.current = undefined;
@@ -103,6 +110,53 @@ const fetchTotalHits = async ({
onTotalHitsChange?.(UnifiedHistogramFetchStatus.loading, hits.total);
+ const newAbortController = new AbortController();
+
+ abortController.current = newAbortController;
+
+ const response = isPlainRecord
+ ? await fetchTotalHitsTextBased({
+ services,
+ abortController: newAbortController,
+ dataView,
+ request,
+ query,
+ timeRange,
+ })
+ : await fetchTotalHitsSearchSource({
+ services,
+ abortController: newAbortController,
+ dataView,
+ request,
+ filters,
+ query,
+ timeRange,
+ });
+
+ if (!response) {
+ return;
+ }
+
+ onTotalHitsChange?.(response.resultStatus, response.result);
+};
+
+const fetchTotalHitsSearchSource = async ({
+ services: { data },
+ abortController,
+ dataView,
+ request,
+ filters: originalFilters,
+ query,
+ timeRange,
+}: {
+ services: UnifiedHistogramServices;
+ abortController: AbortController;
+ dataView: DataView;
+ request: UnifiedHistogramRequestContext | undefined;
+ filters: Filter[];
+ query: Query | AggregateQuery;
+ timeRange: TimeRange;
+}) => {
const searchSource = data.search.searchSource.createEmpty();
searchSource
@@ -131,8 +185,6 @@ const fetchTotalHits = async ({
searchSource.setField('filter', filters);
- abortController.current = new AbortController();
-
// Let the consumer inspect the request if they want to track it
const inspector = request?.adapter
? {
@@ -150,7 +202,7 @@ const fetchTotalHits = async ({
.fetch$({
inspector,
sessionId: request?.searchSessionId,
- abortSignal: abortController.current.signal,
+ abortSignal: abortController.signal,
executionContext: {
description: 'fetch total hits',
},
@@ -168,5 +220,63 @@ const fetchTotalHits = async ({
? UnifiedHistogramFetchStatus.error
: UnifiedHistogramFetchStatus.complete;
- onTotalHitsChange?.(resultStatus, result);
+ return { resultStatus, result };
+};
+
+const fetchTotalHitsTextBased = async ({
+ services: { expressions },
+ abortController,
+ dataView,
+ request,
+ query,
+ timeRange,
+}: {
+ services: UnifiedHistogramServices;
+ abortController: AbortController;
+ dataView: DataView;
+ request: UnifiedHistogramRequestContext | undefined;
+ query: Query | AggregateQuery;
+ timeRange: TimeRange;
+}) => {
+ const ast = await textBasedQueryStateToAstWithValidation({
+ query,
+ time: timeRange,
+ dataView,
+ });
+
+ if (abortController.signal.aborted) {
+ return undefined;
+ }
+
+ if (!ast) {
+ return {
+ resultStatus: UnifiedHistogramFetchStatus.error,
+ result: new Error('Invalid text based query'),
+ };
+ }
+
+ const result = await lastValueFrom(
+ expressions
+ .run(ast, null, {
+ inspectorAdapters: { requests: request?.adapter },
+ searchSessionId: request?.searchSessionId,
+ executionContext: {
+ description: 'fetch total hits',
+ },
+ })
+ .pipe(pluck('result'))
+ );
+
+ if (abortController.signal.aborted) {
+ return undefined;
+ }
+
+ if (isExpressionValueError(result)) {
+ return {
+ resultStatus: UnifiedHistogramFetchStatus.error,
+ result: new Error(result.error.message),
+ };
+ }
+
+ return { resultStatus: UnifiedHistogramFetchStatus.complete, result: result.rows.length };
};
diff --git a/src/plugins/unified_histogram/public/chart/suggestion_selector.test.tsx b/src/plugins/unified_histogram/public/chart/suggestion_selector.test.tsx
new file mode 100644
index 00000000000000..a6ed7cdd75f15b
--- /dev/null
+++ b/src/plugins/unified_histogram/public/chart/suggestion_selector.test.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { EuiComboBox } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import React from 'react';
+import { currentSuggestionMock, allSuggestionsMock } from '../__mocks__/suggestions';
+import { SuggestionSelector } from './suggestion_selector';
+
+describe('SuggestionSelector', () => {
+ it('should pass the suggestions charts titles to the EuiComboBox', () => {
+ const onSuggestionChange = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+ const comboBox = wrapper.find(EuiComboBox);
+ expect(comboBox.prop('options')).toEqual(
+ allSuggestionsMock.map((sug) => {
+ return {
+ label: sug.title,
+ value: sug.title,
+ };
+ })
+ );
+ });
+
+ it('should pass the current suggestion as selectedProps to the EuiComboBox', () => {
+ const onSuggestionChange = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+ const comboBox = wrapper.find(EuiComboBox);
+ expect(comboBox.prop('selectedOptions')).toEqual([
+ { label: currentSuggestionMock.title, value: currentSuggestionMock.title },
+ ]);
+ });
+
+ it('should call onSuggestionChange when the user selects another suggestion', () => {
+ const onSuggestionChange = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+ const comboBox = wrapper.find(EuiComboBox);
+ const selectedSuggestion = allSuggestionsMock.find((sug) => sug.title === 'Donut')!;
+ comboBox.prop('onChange')!([
+ { label: selectedSuggestion.title, value: selectedSuggestion.title },
+ ]);
+ expect(onSuggestionChange).toHaveBeenCalledWith(selectedSuggestion);
+ });
+});
diff --git a/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx
new file mode 100644
index 00000000000000..85860cf2c9bc86
--- /dev/null
+++ b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx
@@ -0,0 +1,112 @@
+/*
+ * 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 {
+ EuiComboBox,
+ EuiFlexItem,
+ EuiToolTip,
+ useEuiTheme,
+ EuiFlexGroup,
+ EuiIcon,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import type { Suggestion } from '@kbn/lens-plugin/public';
+import { i18n } from '@kbn/i18n';
+import React, { useCallback, useState } from 'react';
+
+export interface SuggestionSelectorProps {
+ suggestions: Suggestion[];
+ activeSuggestion?: Suggestion;
+ onSuggestionChange?: (sug: Suggestion | undefined) => void;
+}
+
+export const SuggestionSelector = ({
+ suggestions,
+ activeSuggestion,
+ onSuggestionChange,
+}: SuggestionSelectorProps) => {
+ const suggestionOptions = suggestions.map((sug) => {
+ return {
+ label: sug.title,
+ value: sug.title,
+ };
+ });
+
+ const selectedSuggestion = activeSuggestion
+ ? [
+ {
+ label: activeSuggestion.title,
+ value: activeSuggestion.title,
+ },
+ ]
+ : [];
+
+ const onSelectionChange = useCallback(
+ (newOptions) => {
+ const suggestion = newOptions.length
+ ? suggestions.find((current) => current.title === newOptions[0].value)
+ : activeSuggestion;
+
+ onSuggestionChange?.(suggestion);
+ },
+ [activeSuggestion, onSuggestionChange, suggestions]
+ );
+
+ const [suggestionsPopoverDisabled, setSuggestionaPopoverDisabled] = useState(false);
+ const disableFieldPopover = useCallback(() => setSuggestionaPopoverDisabled(true), []);
+ const enableFieldPopover = useCallback(
+ () => setTimeout(() => setSuggestionaPopoverDisabled(false)),
+ []
+ );
+
+ const { euiTheme } = useEuiTheme();
+ const suggestionComboCss = css`
+ width: 100%;
+ max-width: ${euiTheme.base * 22}px;
+ `;
+
+ return (
+
+ {
+ const suggestion = suggestions.find((s) => {
+ return s.title === option.label;
+ });
+ return (
+
+
+
+
+ {option.label}
+
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts
index 442e3d5fa4318d..9bb8707e6d7c21 100644
--- a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts
+++ b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.test.ts
@@ -10,6 +10,7 @@ import { getLensAttributes } from './get_lens_attributes';
import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
+import { currentSuggestionMock } from '../../__mocks__/suggestions';
describe('getLensAttributes', () => {
const dataView: DataView = dataViewWithTimefieldMock;
@@ -45,7 +46,15 @@ describe('getLensAttributes', () => {
it('should return correct attributes', () => {
const breakdownField: DataViewField | undefined = undefined;
expect(
- getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
+ getLensAttributes({
+ title: 'test',
+ filters,
+ query,
+ dataView,
+ timeInterval,
+ breakdownField,
+ suggestion: undefined,
+ })
).toMatchInlineSnapshot(`
Object {
"references": Array [
@@ -185,7 +194,15 @@ describe('getLensAttributes', () => {
(f) => f.name === 'extension'
);
expect(
- getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
+ getLensAttributes({
+ title: 'test',
+ filters,
+ query,
+ dataView,
+ timeInterval,
+ breakdownField,
+ suggestion: undefined,
+ })
).toMatchInlineSnapshot(`
Object {
"references": Array [
@@ -343,7 +360,15 @@ describe('getLensAttributes', () => {
(f) => f.name === 'scripted'
);
expect(
- getLensAttributes({ title: 'test', filters, query, dataView, timeInterval, breakdownField })
+ getLensAttributes({
+ title: 'test',
+ filters,
+ query,
+ dataView,
+ timeInterval,
+ breakdownField,
+ suggestion: undefined,
+ })
).toMatchInlineSnapshot(`
Object {
"references": Array [
@@ -477,4 +502,209 @@ describe('getLensAttributes', () => {
}
`);
});
+
+ it('should return correct attributes for text based languages', () => {
+ expect(
+ getLensAttributes({
+ title: 'test',
+ filters,
+ query,
+ dataView,
+ timeInterval,
+ breakdownField: undefined,
+ suggestion: currentSuggestionMock,
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "references": Array [
+ Object {
+ "id": "index-pattern-with-timefield-id",
+ "name": "indexpattern-datasource-current-indexpattern",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "index-pattern-with-timefield-id",
+ "name": "indexpattern-datasource-layer-unifiedHistogram",
+ "type": "index-pattern",
+ },
+ ],
+ "state": Object {
+ "datasourceStates": Object {
+ "textBased": Object {
+ "fieldList": Array [],
+ "indexPatternRefs": Array [],
+ "initialContext": Object {
+ "contextualFields": Array [
+ "Dest",
+ "AvgTicketPrice",
+ ],
+ "dataViewSpec": Object {
+ "allowNoIndex": false,
+ "fields": Object {
+ "AvgTicketPrice": Object {
+ "aggregatable": true,
+ "count": 0,
+ "esTypes": Array [
+ "float",
+ ],
+ "format": Object {
+ "id": "number",
+ "params": Object {
+ "pattern": "$0,0.[00]",
+ },
+ },
+ "isMapped": true,
+ "name": "AvgTicketPrice",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "shortDotsEnable": false,
+ "type": "number",
+ },
+ "Dest": Object {
+ "aggregatable": true,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "format": Object {
+ "id": "string",
+ },
+ "isMapped": true,
+ "name": "Dest",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "shortDotsEnable": false,
+ "type": "string",
+ },
+ "timestamp": Object {
+ "aggregatable": true,
+ "count": 0,
+ "esTypes": Array [
+ "date",
+ ],
+ "format": Object {
+ "id": "date",
+ },
+ "isMapped": true,
+ "name": "timestamp",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "shortDotsEnable": false,
+ "type": "date",
+ },
+ },
+ "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
+ "name": "Kibana Sample Data Flights",
+ "sourceFilters": Array [],
+ "timeFieldName": "timestamp",
+ "title": "kibana_sample_data_flights",
+ "version": "WzM1ODA3LDFd",
+ },
+ "fieldName": "",
+ "query": Object {
+ "sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"",
+ },
+ },
+ "layers": Object {
+ "46aa21fa-b747-4543-bf90-0b40007c546d": Object {
+ "allColumns": Array [
+ Object {
+ "columnId": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
+ "fieldName": "Dest",
+ "meta": Object {
+ "type": "string",
+ },
+ },
+ Object {
+ "columnId": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
+ "fieldName": "AvgTicketPrice",
+ "meta": Object {
+ "type": "number",
+ },
+ },
+ ],
+ "columns": Array [
+ Object {
+ "columnId": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
+ "fieldName": "Dest",
+ "meta": Object {
+ "type": "string",
+ },
+ },
+ Object {
+ "columnId": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
+ "fieldName": "AvgTicketPrice",
+ "meta": Object {
+ "type": "number",
+ },
+ },
+ ],
+ "index": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
+ "query": Object {
+ "sql": "SELECT Dest, AvgTicketPrice FROM \\"kibana_sample_data_flights\\"",
+ },
+ "timeField": "timestamp",
+ },
+ },
+ },
+ },
+ "filters": Array [
+ Object {
+ "$state": Object {
+ "store": "appState",
+ },
+ "meta": Object {
+ "alias": null,
+ "disabled": false,
+ "index": "index-pattern-with-timefield-id",
+ "key": "extension",
+ "negate": false,
+ "params": Object {
+ "query": "js",
+ },
+ "type": "phrase",
+ },
+ "query": Object {
+ "match": Object {
+ "extension": Object {
+ "query": "js",
+ "type": "phrase",
+ },
+ },
+ },
+ },
+ ],
+ "query": Object {
+ "language": "kuery",
+ "query": "extension : css",
+ },
+ "visualization": Object {
+ "gridConfig": Object {
+ "isCellLabelVisible": false,
+ "isXAxisLabelVisible": true,
+ "isXAxisTitleVisible": false,
+ "isYAxisLabelVisible": true,
+ "isYAxisTitleVisible": false,
+ "type": "heatmap_grid",
+ },
+ "layerId": "46aa21fa-b747-4543-bf90-0b40007c546d",
+ "layerType": "data",
+ "legend": Object {
+ "isVisible": true,
+ "position": "right",
+ "type": "heatmap_legend",
+ },
+ "shape": "heatmap",
+ "valueAccessor": "5b9b8b76-0836-4a12-b9c0-980c9900502f",
+ "xAccessor": "81e332d6-ee37-42a8-a646-cea4fc75d2d3",
+ },
+ },
+ "title": "test",
+ "visualizationType": "lnsHeatmap",
+ }
+ `);
+ });
});
diff --git a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.ts b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.ts
index dc6f9216b4e565..5f6b8214e5863c 100644
--- a/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.ts
+++ b/src/plugins/unified_histogram/public/chart/utils/get_lens_attributes.ts
@@ -15,6 +15,7 @@ import type {
GenericIndexPatternColumn,
TermsIndexPatternColumn,
TypedLensByValueInput,
+ Suggestion,
} from '@kbn/lens-plugin/public';
import { fieldSupportsBreakdown } from './field_supports_breakdown';
@@ -25,6 +26,7 @@ export const getLensAttributes = ({
dataView,
timeInterval,
breakdownField,
+ suggestion,
}: {
title?: string;
filters: Filter[];
@@ -32,6 +34,7 @@ export const getLensAttributes = ({
dataView: DataView;
timeInterval: string | undefined;
breakdownField: DataViewField | undefined;
+ suggestion: Suggestion | undefined;
}) => {
const showBreakdown = breakdownField && fieldSupportsBreakdown(breakdownField);
@@ -103,35 +106,27 @@ export const getLensAttributes = ({
};
}
- return {
- title:
- title ??
- i18n.translate('unifiedHistogram.lensTitle', {
- defaultMessage: 'Edit visualization',
- }),
- references: [
- {
- id: dataView.id ?? '',
- name: 'indexpattern-datasource-current-indexpattern',
- type: 'index-pattern',
- },
- {
- id: dataView.id ?? '',
- name: 'indexpattern-datasource-layer-unifiedHistogram',
- type: 'index-pattern',
- },
- ],
- state: {
- datasourceStates: {
- formBased: {
- layers: {
- unifiedHistogram: { columnOrder, columns },
+ const suggestionDatasourceState = Object.assign({}, suggestion?.datasourceState);
+ const suggestionVisualizationState = Object.assign({}, suggestion?.visualizationState);
+ const datasourceStates =
+ suggestion && suggestion.datasourceState
+ ? {
+ [suggestion.datasourceId!]: {
+ ...suggestionDatasourceState,
},
- },
- },
- filters,
- query: 'language' in query ? query : { language: 'kuery', query: '' },
- visualization: {
+ }
+ : {
+ formBased: {
+ layers: {
+ unifiedHistogram: { columnOrder, columns },
+ },
+ },
+ };
+ const visualization = suggestion
+ ? {
+ ...suggestionVisualizationState,
+ }
+ : {
layers: [
{
accessors: ['count_column'],
@@ -173,8 +168,32 @@ export const getLensAttributes = ({
yLeft: true,
yRight: false,
},
+ };
+
+ return {
+ title:
+ title ??
+ i18n.translate('unifiedHistogram.lensTitle', {
+ defaultMessage: 'Edit visualization',
+ }),
+ references: [
+ {
+ id: dataView.id ?? '',
+ name: 'indexpattern-datasource-current-indexpattern',
+ type: 'index-pattern',
+ },
+ {
+ id: dataView.id ?? '',
+ name: 'indexpattern-datasource-layer-unifiedHistogram',
+ type: 'index-pattern',
},
+ ],
+ state: {
+ datasourceStates,
+ filters,
+ query,
+ visualization,
},
- visualizationType: 'lnsXY',
+ visualizationType: suggestion ? suggestion.visualizationId : 'lnsXY',
} as TypedLensByValueInput['attributes'];
};
diff --git a/src/plugins/unified_histogram/public/container/container.test.tsx b/src/plugins/unified_histogram/public/container/container.test.tsx
index c9481c4ff4cbba..17457695288513 100644
--- a/src/plugins/unified_histogram/public/container/container.test.tsx
+++ b/src/plugins/unified_histogram/public/container/container.test.tsx
@@ -31,6 +31,8 @@ describe('UnifiedHistogramContainer', () => {
topPanelHeight: 100,
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
totalHitsResult: undefined,
+ columns: [],
+ currentSuggestion: undefined,
};
it('should set ref', () => {
@@ -61,7 +63,7 @@ describe('UnifiedHistogramContainer', () => {
const component = mountWithIntl(
);
- await new Promise((resolve) => setTimeout(resolve, 0));
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
expect(component.update().isEmptyRender()).toBe(false);
});
@@ -80,6 +82,7 @@ describe('UnifiedHistogramContainer', () => {
});
}
});
+ await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
expect(api?.initialized).toBe(true);
});
});
diff --git a/src/plugins/unified_histogram/public/container/container.tsx b/src/plugins/unified_histogram/public/container/container.tsx
index f079b38d179ff0..3bfc7c5d69a578 100644
--- a/src/plugins/unified_histogram/public/container/container.tsx
+++ b/src/plugins/unified_histogram/public/container/container.tsx
@@ -9,6 +9,7 @@
import React, { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
import { Subject } from 'rxjs';
import { pick } from 'lodash';
+import { LensSuggestionsApi } from '@kbn/lens-plugin/public';
import { UnifiedHistogramLayout, UnifiedHistogramLayoutProps } from '../layout';
import type { UnifiedHistogramInputMessage } from '../types';
import {
@@ -19,6 +20,8 @@ import {
import { useStateProps } from './hooks/use_state_props';
import { useStateSelector } from './utils/use_state_selector';
import {
+ columnsSelector,
+ currentSuggestionSelector,
dataViewSelector,
filtersSelector,
querySelector,
@@ -81,6 +84,7 @@ export type UnifiedHistogramInitializedApi = {
| 'setChartHidden'
| 'setTopPanelHeight'
| 'setBreakdownField'
+ | 'setColumns'
| 'setTimeInterval'
| 'setRequestParams'
| 'setTotalHits'
@@ -98,6 +102,7 @@ export const UnifiedHistogramContainer = forwardRef<
const [initialized, setInitialized] = useState(false);
const [layoutProps, setLayoutProps] = useState();
const [stateService, setStateService] = useState();
+ const [lensSuggestionsApi, setLensSuggestionsApi] = useState();
const [input$] = useState(() => new Subject());
const api = useMemo(
() => ({
@@ -111,6 +116,12 @@ export const UnifiedHistogramContainer = forwardRef<
getRelativeTimeRange,
} = options;
+ // API helpers are loaded async from Lens
+ (async () => {
+ const apiHelper = await services.lens.stateHelperApi();
+ setLensSuggestionsApi(() => apiHelper.suggestions);
+ })();
+
setLayoutProps({
services,
disableAutoFetching,
@@ -130,6 +141,7 @@ export const UnifiedHistogramContainer = forwardRef<
'setChartHidden',
'setTopPanelHeight',
'setBreakdownField',
+ 'setColumns',
'setTimeInterval',
'setRequestParams',
'setTotalHits'
@@ -146,10 +158,12 @@ export const UnifiedHistogramContainer = forwardRef<
const query = useStateSelector(stateService?.state$, querySelector);
const filters = useStateSelector(stateService?.state$, filtersSelector);
const timeRange = useStateSelector(stateService?.state$, timeRangeSelector);
+ const columns = useStateSelector(stateService?.state$, columnsSelector);
+ const currentSuggestion = useStateSelector(stateService?.state$, currentSuggestionSelector);
const topPanelHeight = useStateSelector(stateService?.state$, topPanelHeightSelector);
// Don't render anything until the container is initialized
- if (!layoutProps || !dataView) {
+ if (!layoutProps || !dataView || !lensSuggestionsApi) {
return null;
}
@@ -162,8 +176,11 @@ export const UnifiedHistogramContainer = forwardRef<
query={query}
filters={filters}
timeRange={timeRange}
+ columns={columns}
+ currentSuggestion={currentSuggestion}
topPanelHeight={topPanelHeight}
input$={input$}
+ lensSuggestionsApi={lensSuggestionsApi}
/>
);
});
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 c36e5239bfe29d..fea0813119136f 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
@@ -8,11 +8,13 @@
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/common';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
+import { Suggestion } from '@kbn/lens-plugin/public';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-test-renderer';
import { UnifiedHistogramFetchStatus } from '../../types';
import { dataViewMock } from '../../__mocks__/data_view';
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
+import { currentSuggestionMock } from '../../__mocks__/suggestions';
import { unifiedHistogramServicesMock } from '../../__mocks__/services';
import {
createStateService,
@@ -36,6 +38,8 @@ describe('useStateProps', () => {
topPanelHeight: 100,
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
totalHitsResult: undefined,
+ columns: [],
+ currentSuggestion: undefined,
};
const getStateService = (options: Omit) => {
@@ -50,6 +54,7 @@ describe('useStateProps', () => {
jest.spyOn(stateService, 'setRequestParams');
jest.spyOn(stateService, 'setLensRequestAdapter');
jest.spyOn(stateService, 'setTotalHits');
+ jest.spyOn(stateService, 'setCurrentSuggestion');
return stateService;
};
@@ -76,9 +81,11 @@ describe('useStateProps', () => {
"status": "uninitialized",
"total": undefined,
},
+ "isPlainRecord": false,
"onBreakdownFieldChange": [Function],
"onChartHiddenChange": [Function],
"onChartLoad": [Function],
+ "onSuggestionChange": [Function],
"onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function],
@@ -104,11 +111,19 @@ describe('useStateProps', () => {
expect(result.current).toMatchInlineSnapshot(`
Object {
"breakdown": undefined,
- "chart": undefined,
- "hits": undefined,
+ "chart": Object {
+ "hidden": false,
+ "timeInterval": "auto",
+ },
+ "hits": Object {
+ "status": "uninitialized",
+ "total": undefined,
+ },
+ "isPlainRecord": true,
"onBreakdownFieldChange": [Function],
"onChartHiddenChange": [Function],
"onChartLoad": [Function],
+ "onSuggestionChange": [Function],
"onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function],
@@ -126,6 +141,20 @@ describe('useStateProps', () => {
`);
});
+ it('should return the correct props when a text based language is used', () => {
+ const stateService = getStateService({
+ initialState: {
+ ...initialState,
+ query: { sql: 'SELECT * FROM index' },
+ currentSuggestion: currentSuggestionMock,
+ },
+ });
+ const { result } = renderHook(() => useStateProps(stateService));
+ expect(result.current.chart).toStrictEqual({ hidden: false, timeInterval: 'auto' });
+ expect(result.current.breakdown).toBe(undefined);
+ expect(result.current.isPlainRecord).toBe(true);
+ });
+
it('should return the correct props when a rollup data view is used', () => {
const stateService = getStateService({
initialState: {
@@ -145,9 +174,11 @@ describe('useStateProps', () => {
"status": "uninitialized",
"total": undefined,
},
+ "isPlainRecord": false,
"onBreakdownFieldChange": [Function],
"onChartHiddenChange": [Function],
"onChartLoad": [Function],
+ "onSuggestionChange": [Function],
"onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function],
@@ -178,9 +209,11 @@ describe('useStateProps', () => {
"status": "uninitialized",
"total": undefined,
},
+ "isPlainRecord": false,
"onBreakdownFieldChange": [Function],
"onChartHiddenChange": [Function],
"onChartLoad": [Function],
+ "onSuggestionChange": [Function],
"onTimeIntervalChange": [Function],
"onTopPanelHeightChange": [Function],
"onTotalHitsChange": [Function],
@@ -208,6 +241,7 @@ describe('useStateProps', () => {
onChartHiddenChange,
onChartLoad,
onBreakdownFieldChange,
+ onSuggestionChange,
} = result.current;
act(() => {
onTopPanelHeightChange(200);
@@ -237,6 +271,11 @@ describe('useStateProps', () => {
onBreakdownFieldChange({ name: 'field' } as DataViewField);
});
expect(stateService.setBreakdownField).toHaveBeenLastCalledWith('field');
+
+ act(() => {
+ onSuggestionChange({ title: 'Stacked Bar' } as Suggestion);
+ });
+ expect(stateService.setCurrentSuggestion).toHaveBeenLastCalledWith({ title: 'Stacked Bar' });
});
it('should clear lensRequestAdapter when chart is hidden', () => {
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 56b2da38ac2fa9..8b63b6d91dd62d 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
@@ -40,7 +40,11 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
*/
const isPlainRecord = useMemo(() => {
- return query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql';
+ return (
+ query &&
+ isOfAggregateQueryType(query) &&
+ ['sql', 'esql'].some((mode) => mode === getAggregateQueryMode(query))
+ );
}, [query]);
const isTimeBased = useMemo(() => {
@@ -48,7 +52,7 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
}, [dataView]);
const hits = useMemo(() => {
- if (isPlainRecord || totalHitsResult instanceof Error) {
+ if (totalHitsResult instanceof Error) {
return undefined;
}
@@ -56,10 +60,10 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
status: totalHitsStatus,
total: totalHitsResult,
};
- }, [isPlainRecord, totalHitsResult, totalHitsStatus]);
+ }, [totalHitsResult, totalHitsStatus]);
const chart = useMemo(() => {
- if (isPlainRecord || !isTimeBased) {
+ if (!isTimeBased && !isPlainRecord) {
return undefined;
}
@@ -136,6 +140,13 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
[stateService]
);
+ const onSuggestionChange = useCallback(
+ (suggestion) => {
+ stateService?.setCurrentSuggestion(suggestion);
+ },
+ [stateService]
+ );
+
/**
* Effects
*/
@@ -152,11 +163,13 @@ export const useStateProps = (stateService: UnifiedHistogramStateService | undef
chart,
breakdown,
request,
+ isPlainRecord,
onTopPanelHeightChange,
onTimeIntervalChange,
onTotalHitsChange,
onChartHiddenChange,
onChartLoad,
onBreakdownFieldChange,
+ onSuggestionChange,
};
};
diff --git a/src/plugins/unified_histogram/public/container/services/state_service.test.ts b/src/plugins/unified_histogram/public/container/services/state_service.test.ts
index a663672fb8356c..82553ce27951b3 100644
--- a/src/plugins/unified_histogram/public/container/services/state_service.test.ts
+++ b/src/plugins/unified_histogram/public/container/services/state_service.test.ts
@@ -59,6 +59,8 @@ describe('UnifiedHistogramStateService', () => {
topPanelHeight: 100,
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
totalHitsResult: undefined,
+ columns: [],
+ currentSuggestion: undefined,
};
it('should initialize state with default values', () => {
@@ -84,6 +86,9 @@ describe('UnifiedHistogramStateService', () => {
topPanelHeight: undefined,
totalHitsResult: undefined,
totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized,
+ columns: [],
+ currentSuggestion: undefined,
+ allSuggestions: undefined,
});
});
diff --git a/src/plugins/unified_histogram/public/container/services/state_service.ts b/src/plugins/unified_histogram/public/container/services/state_service.ts
index 5e8ad3b8e8b663..36f50ea8bd36e9 100644
--- a/src/plugins/unified_histogram/public/container/services/state_service.ts
+++ b/src/plugins/unified_histogram/public/container/services/state_service.ts
@@ -9,6 +9,7 @@
import type { DataView } from '@kbn/data-views-plugin/common';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import type { RequestAdapter } from '@kbn/inspector-plugin/common';
+import type { Suggestion } from '@kbn/lens-plugin/public';
import { BehaviorSubject, Observable } from 'rxjs';
import { UnifiedHistogramFetchStatus } from '../..';
import type { UnifiedHistogramServices } from '../../types';
@@ -29,6 +30,14 @@ export interface UnifiedHistogramState {
* The current field used for the breakdown
*/
breakdownField: string | undefined;
+ /**
+ * The current selected columns
+ */
+ columns: string[] | undefined;
+ /**
+ * The current Lens suggestion
+ */
+ currentSuggestion: Suggestion | undefined;
/**
* Whether or not the chart is hidden
*/
@@ -109,6 +118,14 @@ export interface UnifiedHistogramStateService {
* Sets the current chart hidden state
*/
setChartHidden: (chartHidden: boolean) => void;
+ /**
+ * Sets current Lens suggestion
+ */
+ setCurrentSuggestion: (suggestion: Suggestion | undefined) => void;
+ /**
+ * Sets columns
+ */
+ setColumns: (columns: string[] | undefined) => void;
/**
* Sets the current top panel height
*/
@@ -163,7 +180,9 @@ export const createStateService = (
const state$ = new BehaviorSubject({
breakdownField: initialBreakdownField,
chartHidden: initialChartHidden,
+ columns: [],
filters: [],
+ currentSuggestion: undefined,
lensRequestAdapter: undefined,
query: services.data.query.queryString.getDefaultQuery(),
requestAdapter: undefined,
@@ -210,6 +229,14 @@ export const createStateService = (
updateState({ breakdownField });
},
+ setCurrentSuggestion: (suggestion: Suggestion | undefined) => {
+ updateState({ currentSuggestion: suggestion });
+ },
+
+ setColumns: (columns: string[] | undefined) => {
+ updateState({ columns });
+ },
+
setTimeInterval: (timeInterval: string) => {
updateState({ timeInterval });
},
diff --git a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts
index 87f425ae64b452..52980e39182950 100644
--- a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts
+++ b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts
@@ -9,6 +9,7 @@
import type { UnifiedHistogramState } from '../services/state_service';
export const breakdownFieldSelector = (state: UnifiedHistogramState) => state.breakdownField;
+export const columnsSelector = (state: UnifiedHistogramState) => state.columns;
export const chartHiddenSelector = (state: UnifiedHistogramState) => state.chartHidden;
export const dataViewSelector = (state: UnifiedHistogramState) => state.dataView;
export const filtersSelector = (state: UnifiedHistogramState) => state.filters;
@@ -20,3 +21,4 @@ export const timeRangeSelector = (state: UnifiedHistogramState) => state.timeRan
export const topPanelHeightSelector = (state: UnifiedHistogramState) => state.topPanelHeight;
export const totalHitsResultSelector = (state: UnifiedHistogramState) => state.totalHitsResult;
export const totalHitsStatusSelector = (state: UnifiedHistogramState) => state.totalHitsStatus;
+export const currentSuggestionSelector = (state: UnifiedHistogramState) => state.currentSuggestion;
diff --git a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts
new file mode 100644
index 00000000000000..2884118872a2d5
--- /dev/null
+++ b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { DataView } from '@kbn/data-views-plugin/common';
+import { AggregateQuery, isOfAggregateQueryType, Query } from '@kbn/es-query';
+import { LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public';
+import { isEqual } from 'lodash';
+import { useEffect, useMemo, useRef, useState } from 'react';
+
+export const useLensSuggestions = ({
+ dataView,
+ query,
+ originalSuggestion,
+ isPlainRecord,
+ columns,
+ lensSuggestionsApi,
+ onSuggestionChange,
+}: {
+ dataView: DataView;
+ query?: Query | AggregateQuery;
+ originalSuggestion?: Suggestion;
+ isPlainRecord?: boolean;
+ columns?: string[];
+ lensSuggestionsApi: LensSuggestionsApi;
+ onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
+}) => {
+ const suggestions = useMemo(() => {
+ const context = {
+ dataViewSpec: dataView?.toSpec(),
+ fieldName: '',
+ contextualFields: columns,
+ query: query && isOfAggregateQueryType(query) ? query : undefined,
+ };
+ const lensSuggestions = isPlainRecord ? lensSuggestionsApi(context, dataView) : undefined;
+ const firstSuggestion = lensSuggestions?.length ? lensSuggestions[0] : undefined;
+ const restSuggestions = lensSuggestions?.filter((sug) => {
+ return !sug.hide && sug.visualizationId !== 'lnsLegacyMetric';
+ });
+ const firstSuggestionExists = restSuggestions?.find(
+ (sug) => sug.title === firstSuggestion?.title
+ );
+ if (firstSuggestion && !firstSuggestionExists) {
+ restSuggestions?.push(firstSuggestion);
+ }
+ return { firstSuggestion, restSuggestions };
+ }, [columns, dataView, isPlainRecord, lensSuggestionsApi, query]);
+
+ const [allSuggestions, setAllSuggestions] = useState(suggestions.restSuggestions);
+ const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion;
+ const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns }));
+
+ useEffect(() => {
+ const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns });
+
+ if (!isEqual(suggestionDeps.current, newSuggestionsDeps)) {
+ setAllSuggestions(suggestions.restSuggestions);
+ onSuggestionChange?.(suggestions.firstSuggestion);
+
+ suggestionDeps.current = newSuggestionsDeps;
+ }
+ }, [
+ columns,
+ dataView,
+ onSuggestionChange,
+ query,
+ suggestions.firstSuggestion,
+ suggestions.restSuggestions,
+ ]);
+
+ return {
+ allSuggestions,
+ currentSuggestion,
+ suggestionUnsupported:
+ isPlainRecord &&
+ (!currentSuggestion || currentSuggestion?.visualizationId === 'lnsDatatable'),
+ };
+};
+
+const getSuggestionDeps = ({
+ dataView,
+ query,
+ columns,
+}: {
+ dataView: DataView;
+ query?: Query | AggregateQuery;
+ columns?: string[];
+}) => [dataView.id, columns, query];
diff --git a/src/plugins/unified_histogram/public/layout/layout.test.tsx b/src/plugins/unified_histogram/public/layout/layout.test.tsx
index aee62e3c7d3b89..ee51ad99b40536 100644
--- a/src/plugins/unified_histogram/public/layout/layout.test.tsx
+++ b/src/plugins/unified_histogram/public/layout/layout.test.tsx
@@ -76,6 +76,7 @@ describe('Layout', () => {
from: '2020-05-14T11:05:13.590',
to: '2020-05-14T11:20:13.590',
}}
+ lensSuggestionsApi={jest.fn()}
{...rest}
/>
);
diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx
index 5167bbf63f8307..89059a320fcf20 100644
--- a/src/plugins/unified_histogram/public/layout/layout.tsx
+++ b/src/plugins/unified_histogram/public/layout/layout.tsx
@@ -7,13 +7,13 @@
*/
import { EuiSpacer, useEuiTheme, useIsWithinBreakpoints } from '@elastic/eui';
-import type { PropsWithChildren, ReactElement, RefObject } from 'react';
+import { PropsWithChildren, ReactElement, RefObject } from 'react';
import React, { useMemo } from 'react';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { css } from '@emotion/css';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
-import type { LensEmbeddableInput } from '@kbn/lens-plugin/public';
-import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
+import type { LensEmbeddableInput, LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public';
+import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { Chart } from '../chart';
import { Panels, PANELS_MODE } from '../panels';
import type {
@@ -26,6 +26,7 @@ import type {
UnifiedHistogramChartLoadEvent,
UnifiedHistogramInput$,
} from '../types';
+import { useLensSuggestions } from './hooks/use_lens_suggestions';
export interface UnifiedHistogramLayoutProps extends PropsWithChildren {
/**
@@ -48,10 +49,22 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren
* The current filters
*/
filters?: Filter[];
+ /**
+ * The current Lens suggestion
+ */
+ currentSuggestion?: Suggestion;
+ /**
+ * Flag that indicates that a text based language is used
+ */
+ isPlainRecord?: boolean;
/**
* The current time range
*/
timeRange?: TimeRange;
+ /**
+ * The current columns
+ */
+ columns?: string[];
/**
* Context object for requests made by Unified Histogram components -- optional
*/
@@ -96,6 +109,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren
* Input observable
*/
input$?: UnifiedHistogramInput$;
+ /**
+ * The Lens suggestions API
+ */
+ lensSuggestionsApi: LensSuggestionsApi;
/**
* Callback to get the relative time range, useful when passing an absolute time range (e.g. for edit visualization button)
*/
@@ -116,6 +133,10 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren
* Callback to update the breakdown field -- should set {@link UnifiedHistogramBreakdownContext.field} to breakdownField
*/
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
+ /**
+ * Callback to update the suggested chart
+ */
+ onSuggestionChange?: (suggestion: Suggestion | undefined) => void;
/**
* Callback to update the total hits -- should set {@link UnifiedHistogramHitsContext.status} to status
* and {@link UnifiedHistogramHitsContext.total} to result
@@ -141,10 +162,13 @@ export const UnifiedHistogramLayout = ({
dataView,
query,
filters,
+ currentSuggestion: originalSuggestion,
+ isPlainRecord,
timeRange,
+ columns,
request,
hits,
- chart,
+ chart: originalChart,
breakdown,
resizeRef,
topPanelHeight,
@@ -152,18 +176,32 @@ export const UnifiedHistogramLayout = ({
disableAutoFetching,
disableTriggers,
disabledActions,
+ lensSuggestionsApi,
input$,
getRelativeTimeRange,
onTopPanelHeightChange,
onChartHiddenChange,
onTimeIntervalChange,
onBreakdownFieldChange,
+ onSuggestionChange,
onTotalHitsChange,
onChartLoad,
onFilter,
onBrushEnd,
children,
}: UnifiedHistogramLayoutProps) => {
+ const { allSuggestions, currentSuggestion, suggestionUnsupported } = useLensSuggestions({
+ dataView,
+ query,
+ originalSuggestion,
+ isPlainRecord,
+ columns,
+ lensSuggestionsApi,
+ onSuggestionChange,
+ });
+
+ const chart = suggestionUnsupported ? undefined : originalChart;
+
const topPanelNode = useMemo(
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
[]
@@ -214,6 +252,9 @@ export const UnifiedHistogramLayout = ({
timeRange={timeRange}
request={request}
hits={hits}
+ currentSuggestion={currentSuggestion}
+ allSuggestions={allSuggestions}
+ isPlainRecord={isPlainRecord}
chart={chart}
breakdown={breakdown}
appendHitsCounter={appendHitsCounter}
@@ -227,6 +268,7 @@ export const UnifiedHistogramLayout = ({
onChartHiddenChange={onChartHiddenChange}
onTimeIntervalChange={onTimeIntervalChange}
onBreakdownFieldChange={onBreakdownFieldChange}
+ onSuggestionChange={onSuggestionChange}
onTotalHitsChange={onTotalHitsChange}
onChartLoad={onChartLoad}
onFilter={onFilter}
diff --git a/src/plugins/unified_histogram/public/mocks.ts b/src/plugins/unified_histogram/public/mocks.ts
index 0258b9900bf97e..9eb6463d34d1c5 100644
--- a/src/plugins/unified_histogram/public/mocks.ts
+++ b/src/plugins/unified_histogram/public/mocks.ts
@@ -24,6 +24,7 @@ export const createMockUnifiedHistogramApi = (
setChartHidden: jest.fn(),
setTopPanelHeight: jest.fn(),
setBreakdownField: jest.fn(),
+ setColumns: jest.fn(),
setTimeInterval: jest.fn(),
setRequestParams: jest.fn(),
setTotalHits: jest.fn(),
diff --git a/src/plugins/unified_histogram/public/types.ts b/src/plugins/unified_histogram/public/types.ts
index d7eacdf9d5c446..8113fd552553de 100644
--- a/src/plugins/unified_histogram/public/types.ts
+++ b/src/plugins/unified_histogram/public/types.ts
@@ -17,6 +17,7 @@ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import type { Subject } from 'rxjs';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
+import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
/**
* The fetch status of a Unified Histogram request
@@ -40,6 +41,7 @@ export interface UnifiedHistogramServices {
fieldFormats: FieldFormatsStart;
lens: LensPublicStart;
storage: Storage;
+ expressions: ExpressionsStart;
}
/**
diff --git a/test/functional/apps/discover/group2/_sql_view.ts b/test/functional/apps/discover/group2/_sql_view.ts
index 1f567ec67ddff1..0cc3882bcb00de 100644
--- a/test/functional/apps/discover/group2/_sql_view.ts
+++ b/test/functional/apps/discover/group2/_sql_view.ts
@@ -66,8 +66,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await testSubjects.exists('showQueryBarMenu')).to.be(false);
expect(await testSubjects.exists('addFilter')).to.be(false);
expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false);
+ // here Lens suggests a table so the chart is not rendered
expect(await testSubjects.exists('unifiedHistogramChart')).to.be(false);
- expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(false);
+ expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true);
expect(await testSubjects.exists('discoverAlertsButton')).to.be(false);
expect(await testSubjects.exists('shareTopNavButton')).to.be(false);
expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(false);
@@ -87,7 +88,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await monacoEditor.setCodeEditorValue(testQuery);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
-
+ // here Lens suggests a heatmap so it is rendered
+ expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true);
+ expect(await testSubjects.exists('heatmapChart')).to.be(true);
const cell = await dataGrid.getCellElement(0, 3);
expect(await cell.getVisibleText()).to.be('2269');
});
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 36bd655fd893eb..0187825a031307 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -210,6 +210,10 @@ export class DiscoverPageObject extends FtrService {
await this.comboBox.set('unifiedHistogramBreakdownFieldSelector', field);
}
+ public async chooseLensChart(chart: string) {
+ await this.comboBox.set('unifiedHistogramSuggestionSelector', chart);
+ }
+
public async getHistogramLegendList() {
const unifiedHistogram = await this.testSubjects.find('unifiedHistogramChart');
const list = await unifiedHistogram.findAllByClassName('echLegendItem__label');
diff --git a/x-pack/examples/third_party_vis_lens_example/public/visualization.tsx b/x-pack/examples/third_party_vis_lens_example/public/visualization.tsx
index e5185be2241441..83813a590f6c69 100644
--- a/x-pack/examples/third_party_vis_lens_example/public/visualization.tsx
+++ b/x-pack/examples/third_party_vis_lens_example/public/visualization.tsx
@@ -93,7 +93,7 @@ export const getRotatingNumberVisualization = ({
{
previewIcon: 'refresh',
score: 0.5,
- title: `Rotating ${table.label}` || 'Rotating number',
+ title: table.label ? `Rotating ${table.label}` : 'Rotating number',
state: {
layerId: table.layerId,
color: state?.color || DEFAULT_COLOR,
diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts
index 9aee46b25208b7..c9efec1f572d6d 100644
--- a/x-pack/plugins/lens/public/async_services.ts
+++ b/x-pack/plugins/lens/public/async_services.ts
@@ -32,6 +32,7 @@ export * from './visualizations/gauge';
export * from './datasources/form_based/form_based';
export { getTextBasedDatasource } from './datasources/text_based/text_based_languages';
export { createFormulaPublicApi } from './datasources/form_based/operations/definitions/formula/formula_public_api';
+export * from './lens_suggestions_api';
export * from './datasources/text_based';
export * from './datasources/form_based';
diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
index 60edc6e613451b..bc173bab4aca4e 100644
--- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
+++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts
@@ -441,7 +441,7 @@ describe('Textbased Data Source', () => {
},
},
],
- index: 'foo',
+ index: '1',
query: {
sql: 'SELECT * FROM "foo"',
},
diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
index a240afd54ddda3..1d20433893a61f 100644
--- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
+++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx
@@ -116,7 +116,7 @@ export function getTextBasedDatasource({
};
});
- const index = context.dataViewSpec.title;
+ const index = context.dataViewSpec.id ?? context.dataViewSpec.title;
const query = context.query;
const updatedState = {
...state,
@@ -127,6 +127,7 @@ export function getTextBasedDatasource({
query,
columns: newColumns ?? [],
allColumns: newColumns ?? [],
+ timeField: context.dataViewSpec.timeFieldName,
},
},
};
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index 9bda68f5193abb..c240623e706c50 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -30,6 +30,7 @@ export type {
TableSuggestion,
Visualization,
VisualizationSuggestion,
+ Suggestion,
} from './types';
export type {
LegacyMetricState as MetricState,
@@ -109,6 +110,6 @@ export type { ChartInfo } from './chart_info_api';
export { layerTypes } from '../common/layer_types';
export { LENS_EMBEDDABLE_TYPE } from '../common/constants';
-export type { LensPublicStart, LensPublicSetup } from './plugin';
+export type { LensPublicStart, LensPublicSetup, LensSuggestionsApi } from './plugin';
export const plugin = () => new LensPlugin();
diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.test.ts b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts
new file mode 100644
index 00000000000000..7e5f8a7b1b50fe
--- /dev/null
+++ b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
+import type { DataView } from '@kbn/data-views-plugin/public';
+import { createMockVisualization, DatasourceMock, createMockDatasource } from './mocks';
+import { DatasourceSuggestion } from './types';
+import { suggestionsApi } from './lens_suggestions_api';
+
+const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
+ state,
+ table: {
+ columns: [],
+ isMultiRow: false,
+ layerId,
+ changeType: 'unchanged',
+ },
+ keptLayerIds: [layerId],
+});
+
+describe('suggestionsApi', () => {
+ let datasourceMap: Record;
+ const mockVis = createMockVisualization();
+ beforeEach(() => {
+ datasourceMap = {
+ textBased: createMockDatasource('textBased'),
+ };
+ });
+ test('call the getDatasourceSuggestionsForVisualizeField for the text based query', async () => {
+ const dataView = { id: 'index1' } as unknown as DataView;
+ const visualizationMap = {
+ testVis: {
+ ...mockVis,
+ },
+ };
+ datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
+ generateSuggestion(),
+ ]);
+ const context = {
+ dataViewSpec: {
+ id: 'index1',
+ title: 'index1',
+ name: 'DataView',
+ },
+ fieldName: '',
+ contextualFields: ['field1', 'field2'],
+ query: {
+ sql: 'SELECT field1, field2 FROM "index1"',
+ },
+ };
+ const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
+ expect(datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith(
+ { layers: {}, fieldList: [], indexPatternRefs: [], initialContext: context },
+ 'index1',
+ '',
+ { index1: { id: 'index1' } }
+ );
+ expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled();
+ expect(suggestions?.length).toEqual(0);
+ });
+
+ test('returns the visualizations suggestions', async () => {
+ const dataView = { id: 'index1' } as unknown as DataView;
+ const visualizationMap = {
+ testVis: {
+ ...mockVis,
+ getSuggestions: () => [
+ {
+ score: 0.2,
+ title: 'Test',
+ state: {},
+ previewIcon: 'empty',
+ },
+ {
+ score: 0.8,
+ title: 'Test2',
+ state: {},
+ previewIcon: 'empty',
+ },
+ ],
+ },
+ };
+ datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
+ generateSuggestion(),
+ ]);
+ const context = {
+ dataViewSpec: {
+ id: 'index1',
+ title: 'index1',
+ name: 'DataView',
+ },
+ fieldName: '',
+ contextualFields: ['field1', 'field2'],
+ query: {
+ sql: 'SELECT field1, field2 FROM "index1"',
+ },
+ };
+ const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
+ expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
+ expect(suggestions?.length).toEqual(1);
+ });
+});
diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.ts b/x-pack/plugins/lens/public/lens_suggestions_api.ts
new file mode 100644
index 00000000000000..0e5901d861b1be
--- /dev/null
+++ b/x-pack/plugins/lens/public/lens_suggestions_api.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
+import type { DataView } from '@kbn/data-views-plugin/public';
+import { getSuggestions } from './editor_frame_service/editor_frame/suggestion_helpers';
+import type { DatasourceMap, VisualizationMap, VisualizeEditorContext } from './types';
+import type { DataViewsState } from './state_management';
+
+interface SuggestionsApi {
+ context: VisualizeFieldContext | VisualizeEditorContext;
+ dataView: DataView;
+ visualizationMap?: VisualizationMap;
+ datasourceMap?: DatasourceMap;
+}
+
+export const suggestionsApi = ({
+ context,
+ dataView,
+ datasourceMap,
+ visualizationMap,
+}: SuggestionsApi) => {
+ if (!datasourceMap || !visualizationMap || !dataView.id) return undefined;
+ const datasourceStates = {
+ formBased: {
+ isLoading: false,
+ state: {
+ layers: {},
+ },
+ },
+ textBased: {
+ isLoading: false,
+ state: {
+ layers: {},
+ fieldList: [],
+ indexPatternRefs: [],
+ initialContext: context,
+ },
+ },
+ };
+ const currentDataViewId = dataView.id;
+ const dataViews = {
+ indexPatterns: {
+ [currentDataViewId]: dataView,
+ },
+ indexPatternRefs: [],
+ } as unknown as DataViewsState;
+
+ const initialVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null;
+
+ // find the active visualizations from the context
+ const suggestions = getSuggestions({
+ datasourceMap,
+ datasourceStates,
+ visualizationMap,
+ activeVisualization: initialVisualization,
+ visualizationState: undefined,
+ visualizeTriggerFieldContext: context,
+ dataViews,
+ });
+ if (!suggestions.length) return [];
+ const activeVisualization = suggestions[0];
+ // compute the rest suggestions depending on the active one
+ const newSuggestions = getSuggestions({
+ datasourceMap,
+ datasourceStates: {
+ textBased: {
+ isLoading: false,
+ state: activeVisualization.datasourceState,
+ },
+ },
+ visualizationMap,
+ activeVisualization: visualizationMap[activeVisualization.visualizationId],
+ visualizationState: activeVisualization.visualizationState,
+ dataViews,
+ });
+
+ return [activeVisualization, ...newSuggestions];
+};
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 2e269f37e7f8c4..1eb23895910157 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -16,7 +16,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
-import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
+import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type {
@@ -44,6 +44,7 @@ import {
UiActionsStart,
ACTION_VISUALIZE_FIELD,
VISUALIZE_FIELD_TRIGGER,
+ VisualizeFieldContext,
} from '@kbn/ui-actions-plugin/public';
import {
VISUALIZE_EDITOR_TRIGGER,
@@ -89,6 +90,8 @@ import type {
VisualizationType,
EditorFrameSetup,
LensTopNavMenuEntryGenerator,
+ VisualizeEditorContext,
+ Suggestion,
} from './types';
import { getLensAliasConfig } from './vis_type_alias';
import { createOpenInDiscoverAction } from './trigger_actions/open_in_discover_action';
@@ -232,9 +235,15 @@ export interface LensPublicStart {
stateHelperApi: () => Promise<{
formula: FormulaPublicApi;
chartInfo: ChartInfoApi;
+ suggestions: LensSuggestionsApi;
}>;
}
+export type LensSuggestionsApi = (
+ context: VisualizeFieldContext | VisualizeEditorContext,
+ dataViews: DataView
+) => Suggestion[] | undefined;
+
export class LensPlugin {
private datatableVisualization: DatatableVisualizationType | undefined;
private editorFrameService: EditorFrameServiceType | undefined;
@@ -585,13 +594,30 @@ export class LensPlugin {
},
stateHelperApi: async () => {
- const { createFormulaPublicApi, createChartInfoApi } = await import('./async_services');
+ const { createFormulaPublicApi, createChartInfoApi, suggestionsApi } = await import(
+ './async_services'
+ );
if (!this.editorFrameService) {
await this.initDependenciesForApi();
}
+ const [visualizationMap, datasourceMap] = await Promise.all([
+ this.editorFrameService!.loadVisualizations(),
+ this.editorFrameService!.loadDatasources(),
+ ]);
return {
formula: createFormulaPublicApi(),
chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService),
+ suggestions: (
+ context: VisualizeFieldContext | VisualizeEditorContext,
+ dataView: DataView
+ ) => {
+ return suggestionsApi({
+ datasourceMap,
+ visualizationMap,
+ context,
+ dataView,
+ });
+ },
};
},
};
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
index 5b3eb512426bde..485289cda75a7d 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
@@ -6,6 +6,7 @@
*/
import { getSuggestions } from './suggestions';
+import { IconChartVerticalBullet, IconChartHorizontalBullet } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { GaugeShapes } from '@kbn/expression-gauge-plugin/common';
import { GaugeVisualizationState } from './constants';
@@ -157,12 +158,12 @@ describe('shows suggestions', () => {
},
title: 'Gauge',
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartHorizontalBullet,
score: 0.5,
},
{
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartVerticalBullet,
title: 'Gauge',
score: 0.5,
state: {
@@ -204,7 +205,7 @@ describe('shows suggestions', () => {
ticksPosition: 'auto',
layerId: 'first',
},
- previewIcon: 'empty',
+ previewIcon: IconChartVerticalBullet,
title: 'Gauge',
hide: false, // shows suggestion when current is gauge
score: 0.5,
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
index 424accee1572c2..fc8e5b18957961 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
@@ -12,6 +12,7 @@ import {
GaugeTicksPositions,
GaugeLabelMajorModes,
} from '@kbn/expression-gauge-plugin/common';
+import { IconChartHorizontalBullet, IconChartVerticalBullet } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import type { TableSuggestion, Visualization } from '../../types';
import type { GaugeVisualizationState } from './constants';
@@ -64,7 +65,8 @@ export const getSuggestions: Visualization['getSuggesti
title: i18n.translate('xpack.lens.gauge.gaugeLabel', {
defaultMessage: 'Gauge',
}),
- previewIcon: 'empty',
+ previewIcon:
+ shape === GaugeShapes.VERTICAL_BULLET ? IconChartVerticalBullet : IconChartHorizontalBullet,
score: 0.5,
hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta
};
@@ -73,6 +75,10 @@ export const getSuggestions: Visualization['getSuggesti
? [
{
...baseSuggestion,
+ previewIcon:
+ state?.shape === GaugeShapes.VERTICAL_BULLET
+ ? IconChartHorizontalBullet
+ : IconChartVerticalBullet,
state: {
...baseSuggestion.state,
...state,
@@ -93,6 +99,10 @@ export const getSuggestions: Visualization['getSuggesti
},
{
...baseSuggestion,
+ previewIcon:
+ state?.shape === GaugeShapes.VERTICAL_BULLET
+ ? IconChartHorizontalBullet
+ : IconChartVerticalBullet,
state: {
...baseSuggestion.state,
metricAccessor: table.columns[0].columnId,
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
index 0fc9c6fc306705..5913d61ebb3cbd 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
@@ -6,6 +6,7 @@
*/
import { Position } from '@elastic/charts';
+import { IconChartHeatmap } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { getSuggestions } from './suggestions';
import type { HeatmapVisualizationState } from './types';
@@ -298,7 +299,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.3,
},
]);
@@ -351,7 +352,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0,
},
]);
@@ -404,7 +405,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.3,
},
]);
@@ -468,7 +469,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.3,
},
]);
@@ -534,7 +535,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.6,
},
]);
@@ -608,7 +609,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.3,
},
]);
@@ -682,7 +683,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: 0.9,
},
]);
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
index aca5f4dfe689c8..21e276215acd17 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
@@ -8,6 +8,7 @@
import { partition } from 'lodash';
import { Position } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
+import { IconChartHeatmap } from '@kbn/chart-icons';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import type { Visualization } from '../../types';
import type { HeatmapVisualizationState } from './types';
@@ -127,7 +128,7 @@ export const getSuggestions: Visualization['getSugges
defaultMessage: 'Heat map',
}),
hide,
- previewIcon: 'empty',
+ previewIcon: IconChartHeatmap,
score: Number(score.toFixed(1)),
},
];
diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
index 95f35f6f21cc2b..d37d91fee0b776 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
@@ -1040,7 +1040,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
- "previewIcon": "bullseye",
+ "previewIcon": [Function],
"score": 0.61,
"state": Object {
"layers": Array [
@@ -1069,7 +1069,7 @@ describe('suggestions', () => {
"palette": undefined,
"shape": "mosaic",
},
- "title": "As Mosaic",
+ "title": "Mosaic",
},
]
`);
@@ -1149,7 +1149,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
- "previewIcon": "bullseye",
+ "previewIcon": [Function],
"score": 0.46,
"state": Object {
"layers": Array [
@@ -1175,7 +1175,7 @@ describe('suggestions', () => {
"palette": undefined,
"shape": "waffle",
},
- "title": "As Waffle",
+ "title": "Waffle",
},
]
`);
diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
index 05def4e0a2937f..2399b691333a17 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
@@ -120,7 +120,7 @@ export function suggestions({
const newShape = getNewShape(groups, subVisualizationId as PieVisualizationState['shape']);
const baseSuggestion: VisualizationSuggestion = {
title: i18n.translate('xpack.lens.pie.suggestionLabel', {
- defaultMessage: 'As {chartName}',
+ defaultMessage: '{chartName}',
values: { chartName: PartitionChartsMeta[newShape].label },
description: 'chartName is already translated',
}),
@@ -149,7 +149,7 @@ export function suggestions({
},
],
},
- previewIcon: 'bullseye',
+ previewIcon: PartitionChartsMeta[newShape].icon,
// dont show suggestions for same type
hide:
table.changeType === 'reduced' ||
@@ -161,7 +161,7 @@ export function suggestions({
results.push({
...baseSuggestion,
title: i18n.translate('xpack.lens.pie.suggestionLabel', {
- defaultMessage: 'As {chartName}',
+ defaultMessage: '{chartName}',
values: {
chartName:
PartitionChartsMeta[
@@ -185,7 +185,7 @@ export function suggestions({
) {
results.push({
title: i18n.translate('xpack.lens.pie.treemapSuggestionLabel', {
- defaultMessage: 'As Treemap',
+ defaultMessage: 'Treemap',
}),
// Use a higher score when currently active, to prevent chart type switching
// on the user unintentionally
@@ -218,7 +218,7 @@ export function suggestions({
},
],
},
- previewIcon: 'bullseye',
+ previewIcon: PartitionChartsMeta.treemap.icon,
// hide treemap suggestions from bottom bar, but keep them for chart switcher
hide:
table.changeType === 'reduced' ||
@@ -234,7 +234,7 @@ export function suggestions({
) {
results.push({
title: i18n.translate('xpack.lens.pie.mosaicSuggestionLabel', {
- defaultMessage: 'As Mosaic',
+ defaultMessage: 'Mosaic',
}),
score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5,
state: {
@@ -266,7 +266,7 @@ export function suggestions({
},
],
},
- previewIcon: 'bullseye',
+ previewIcon: PartitionChartsMeta.mosaic.icon,
hide:
groups.length !== 2 ||
table.changeType === 'reduced' ||
@@ -281,7 +281,7 @@ export function suggestions({
) {
results.push({
title: i18n.translate('xpack.lens.pie.waffleSuggestionLabel', {
- defaultMessage: 'As Waffle',
+ defaultMessage: 'Waffle',
}),
score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.4,
state: {
@@ -310,7 +310,7 @@ export function suggestions({
},
],
},
- previewIcon: 'bullseye',
+ previewIcon: PartitionChartsMeta.waffle.icon,
hide:
groups.length !== 1 ||
table.changeType === 'reduced' ||
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index ddd0547b5f425d..9c6c440d214fe8 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -2467,7 +2467,6 @@
"discover.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.",
"discover.sourceViewer.errorMessageTitle": "Une erreur s'est produite.",
"discover.sourceViewer.refresh": "Actualiser",
- "discover.textBasedLanguages.visualize.label": "Visualiser dans Lens",
"discover.toggleSidebarAriaLabel": "Activer/Désactiver la barre latérale",
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Saviez-vous que Discover possède un nouvel Explorateur de documents avec un meilleur tri des données, des colonnes redimensionnables et une vue en plein écran ? Vous pouvez modifier le mode d'affichage dans les Paramètres avancés.",
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "Vous pouvez revenir à l'affichage Discover classique dans les Paramètres avancés.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 6acb9a7d693e61..aa17ed9c8ea7be 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2467,7 +2467,6 @@
"discover.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。",
"discover.sourceViewer.errorMessageTitle": "エラーが発生しました",
"discover.sourceViewer.refresh": "更新",
- "discover.textBasedLanguages.visualize.label": "Lensで可視化",
"discover.toggleSidebarAriaLabel": "サイドバーを切り替える",
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "Discoverの新しいドキュメントエクスプローラーでは、データの並べ替え、列のサイズ変更、全画面表示といった優れた機能をご利用いただけます。高度な設定で表示モードを変更できます。",
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "高度な設定でクラシックDiscoverビューに戻すことができます。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1fdf857d663607..80c7c04c1c8743 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2467,7 +2467,6 @@
"discover.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。",
"discover.sourceViewer.errorMessageTitle": "发生错误",
"discover.sourceViewer.refresh": "刷新",
- "discover.textBasedLanguages.visualize.label": "在 Lens 中可视化",
"discover.toggleSidebarAriaLabel": "切换侧边栏",
"discover.topNav.openOptionsPopover.documentExplorerDisabledHint": "您知道吗?Discover 有一种新的 Document Explorer,可提供更好的数据排序、可调整大小的列及全屏视图。您可以在高级设置中更改视图模式。",
"discover.topNav.openOptionsPopover.documentExplorerEnabledHint": "您可以在高级设置中切换回经典 Discover 视图。",
diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts
index e90d777d2dd9e4..99a85c4ba1be8f 100644
--- a/x-pack/test/functional/apps/discover/visualize_field.ts
+++ b/x-pack/test/functional/apps/discover/visualize_field.ts
@@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
- it('should visualize correctly text based language queries', async () => {
+ it('should visualize correctly text based language queries in Discover', async () => {
await PageObjects.discover.selectTextBaseLang('SQL');
await PageObjects.header.waitUntilLoadingHasFinished();
await monacoEditor.setCodeEditorValue(
@@ -138,8 +138,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true);
+ expect(await testSubjects.exists('heatmapChart')).to.be(true);
- await testSubjects.click('textBased-visualize');
+ await PageObjects.discover.chooseLensChart('Donut');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await testSubjects.exists('partitionVisChart')).to.be(true);
+ });
+
+ it('should visualize correctly text based language queries in Lens', async () => {
+ await PageObjects.discover.selectTextBaseLang('SQL');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await monacoEditor.setCodeEditorValue(
+ 'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension'
+ );
+ await testSubjects.click('querySubmitButton');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await testSubjects.click('unifiedTextLangEditor-expand');
+ await testSubjects.click('unifiedHistogramEditVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -157,8 +173,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
-
- await testSubjects.click('textBased-visualize');
+ await testSubjects.click('unifiedTextLangEditor-expand');
+ await testSubjects.click('unifiedHistogramEditVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
diff --git a/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts
index 9d89c91d2dc4c6..53a51b89f2bccf 100644
--- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts
+++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts
@@ -315,7 +315,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
await PageObjects.lens.save('twolayerchart');
- await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion');
+ await testSubjects.click('lnsSuggestion-donut > lnsSuggestion');
expect(await PageObjects.lens.getLayerCount()).to.eql(1);
expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql(