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(