diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index ba6ffbaf149fe..43192a22858fa 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -69,6 +69,7 @@ export interface ChartProps { disabledActions?: LensEmbeddableInput['disabledActions']; input$?: UnifiedHistogramInput$; lensTablesAdapter?: Record; + isOnHistogramMode?: boolean; onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; @@ -104,6 +105,7 @@ export function Chart({ disabledActions, input$: originalInput$, lensTablesAdapter, + isOnHistogramMode, onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, @@ -425,7 +427,7 @@ export function Chart({ disableTriggers={disableTriggers} disabledActions={disabledActions} onTotalHitsChange={onTotalHitsChange} - hasLensSuggestions={Boolean(currentSuggestion)} + hasLensSuggestions={!Boolean(isOnHistogramMode)} onChartLoad={onChartLoad} onFilter={onFilter} onBrushEnd={onBrushEnd} diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 761e701e8f9a6..92608bc544f79 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -10,7 +10,7 @@ import { useEuiTheme, useResizeObserver } from '@elastic/eui'; import { css } from '@emotion/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 { DefaultInspectorAdapters, Datatable } from '@kbn/expressions-plugin/common'; import type { IKibanaSearchResponse } from '@kbn/data-plugin/public'; import type { estypes } from '@elastic/elasticsearch'; import type { TimeRange } from '@kbn/es-query'; @@ -52,6 +52,28 @@ export interface HistogramProps { onBrushEnd?: LensEmbeddableInput['onBrushEnd']; } +const computeTotalHits = ( + hasLensSuggestions: boolean, + adapterTables: + | { + [key: string]: Datatable; + } + | undefined, + isPlainRecord?: boolean +) => { + if (isPlainRecord && hasLensSuggestions) { + return Object.values(adapterTables ?? {})?.[0]?.rows?.length; + } else if (isPlainRecord && !hasLensSuggestions) { + let rowsCount = 0; + Object.values(adapterTables ?? {})?.[0]?.rows.forEach((r) => { + rowsCount += r.rows; + }); + return rowsCount; + } else { + return adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount; + } +}; + export function Histogram({ services: { data, lens, uiSettings }, dataView, @@ -111,10 +133,7 @@ export function Histogram({ } const adapterTables = adapters?.tables?.tables; - const totalHits = - isPlainRecord && hasLensSuggestions - ? Object.values(adapterTables ?? {})?.[0]?.rows?.length - : adapterTables?.unifiedHistogram?.meta?.statistics?.totalCount; + const totalHits = computeTotalHits(hasLensSuggestions, adapterTables, isPlainRecord); onTotalHitsChange?.( isLoading ? UnifiedHistogramFetchStatus.loading : UnifiedHistogramFetchStatus.complete, 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 index e4936b9345a6d..a7de347b3d54a 100644 --- a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts +++ b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts @@ -5,20 +5,75 @@ * 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 type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { + AggregateQuery, + isOfAggregateQueryType, + Query, + TimeRange, + getAggregateQueryMode, +} from '@kbn/es-query'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public'; import { isEqual } from 'lodash'; import { useEffect, useMemo, useRef, useState } from 'react'; +const roundInterval = (interval: number) => { + { + switch (true) { + case interval <= 500: // <= 0.5s + return '100 millisecond'; + case interval <= 5000: // <= 5s + return '1 second'; + case interval <= 7500: // <= 7.5s + return '5 second'; + case interval <= 15000: // <= 15s + return '10 second'; + case interval <= 45000: // <= 45s + return '30 second'; + case interval <= 180000: // <= 3m + return '1 minute'; + case interval <= 450000: // <= 9m + return '5 minute'; + case interval <= 1200000: // <= 20m + return '10 minute'; + case interval <= 2700000: // <= 45m + return '30 minute'; + case interval <= 7200000: // <= 2h + return '1 hour'; + case interval <= 21600000: // <= 6h + return '3 hour'; + case interval <= 86400000: // <= 24h + return '12 hour'; + case interval <= 604800000: // <= 1w + return '24 hour'; + case interval <= 1814400000: // <= 3w + return '1 week'; + case interval < 3628800000: // < 2y + return '30 day'; + default: + return '1 year'; + } + } +}; + +const computeInterval = (timeRange: TimeRange, data: DataPublicPluginStart): string => { + const bounds = data.query.timefilter.timefilter.calculateBounds(timeRange!); + const min = bounds.min!.valueOf(); + const max = bounds.max!.valueOf(); + const interval = (max - min) / 50; + return roundInterval(interval); +}; + export const useLensSuggestions = ({ dataView, query, originalSuggestion, isPlainRecord, columns, + timeRange, + data, lensSuggestionsApi, onSuggestionChange, }: { @@ -27,6 +82,8 @@ export const useLensSuggestions = ({ originalSuggestion?: Suggestion; isPlainRecord?: boolean; columns?: DatatableColumn[]; + timeRange?: TimeRange; + data: DataPublicPluginStart; lensSuggestionsApi: LensSuggestionsApi; onSuggestionChange?: (suggestion: Suggestion | undefined) => void; }) => { @@ -47,8 +104,51 @@ export const useLensSuggestions = ({ }, [dataView, isPlainRecord, lensSuggestionsApi, query, columns]); const [allSuggestions, setAllSuggestions] = useState(suggestions.allSuggestions); - const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion; + let currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion; const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns })); + let isOnHistogramMode = false; + + if ( + !currentSuggestion && + dataView.isTimeBased() && + query && + isOfAggregateQueryType(query) && + getAggregateQueryMode(query) === 'esql' && + timeRange + ) { + const language = getAggregateQueryMode(query); + const interval = computeInterval(timeRange, data); + const histogramQuery = `${query[language]} | eval uniqueName = 1 + | EVAL timestamp=DATE_TRUNC(${dataView.timeFieldName}, ${interval}) | stats rows = count(uniqueName) by timestamp | rename timestamp as \`${dataView.timeFieldName} every ${interval}\``; + const context = { + dataViewSpec: dataView?.toSpec(), + fieldName: '', + textBasedColumns: [ + { + id: `${dataView.timeFieldName} every ${interval}`, + name: `${dataView.timeFieldName} every ${interval}`, + meta: { + type: 'date', + }, + }, + { + id: 'rows', + name: 'rows', + meta: { + type: 'number', + }, + }, + ] as DatatableColumn[], + query: { + esql: histogramQuery, + }, + }; + const sug = isPlainRecord ? lensSuggestionsApi(context, dataView, ['lnsDatatable']) ?? [] : []; + if (sug.length) { + currentSuggestion = sug[0]; + isOnHistogramMode = true; + } + } useEffect(() => { const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns }); @@ -71,7 +171,8 @@ export const useLensSuggestions = ({ return { allSuggestions, currentSuggestion, - suggestionUnsupported: !currentSuggestion && !dataView.isTimeBased(), + suggestionUnsupported: isPlainRecord && !currentSuggestion, + isOnHistogramMode, }; }; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 4d0605c4f72b3..bdf1b876b5afa 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -193,15 +193,18 @@ export const UnifiedHistogramLayout = ({ onBrushEnd, children, }: UnifiedHistogramLayoutProps) => { - const { allSuggestions, currentSuggestion, suggestionUnsupported } = useLensSuggestions({ - dataView, - query, - originalSuggestion, - isPlainRecord, - columns, - lensSuggestionsApi, - onSuggestionChange, - }); + const { allSuggestions, currentSuggestion, suggestionUnsupported, isOnHistogramMode } = + useLensSuggestions({ + dataView, + query, + originalSuggestion, + isPlainRecord, + columns, + timeRange, + data: services.data, + lensSuggestionsApi, + onSuggestionChange, + }); const chart = suggestionUnsupported ? undefined : originalChart; @@ -277,6 +280,7 @@ export const UnifiedHistogramLayout = ({ onFilter={onFilter} onBrushEnd={onBrushEnd} lensTablesAdapter={lensTablesAdapter} + isOnHistogramMode={isOnHistogramMode} /> {children} diff --git a/test/functional/apps/discover/group2/_sql_view.ts b/test/functional/apps/discover/group2/_sql_view.ts index 3e19b8664517f..aa8c0f5b5faa6 100644 --- a/test/functional/apps/discover/group2/_sql_view.ts +++ b/test/functional/apps/discover/group2/_sql_view.ts @@ -72,8 +72,8 @@ 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); - // when Lens suggests a table, we render the histogram - expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); + // when Lens suggests a table, we render an ESQL based histogram + expect(await testSubjects.exists('unifiedHistogramChart')).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(true); diff --git a/test/functional/apps/discover/group3/_request_counts.ts b/test/functional/apps/discover/group3/_request_counts.ts index bfa7726acff15..4614866e1a09c 100644 --- a/test/functional/apps/discover/group3/_request_counts.ts +++ b/test/functional/apps/discover/group3/_request_counts.ts @@ -218,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('SQL mode', () => { + describe.skip('SQL mode', () => { const type = 'sql'; beforeEach(async () => { diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts index 12e65647c350b..af8cb6d7756da 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts @@ -131,6 +131,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(searchesCountBeforeRestore).to.be(searchesCountAfterRestore); // no new searches started during restore }); + + it('should should clean the search session when navigating to SQL mode, and reinitialize when navigating back', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await searchSessions.exists()).to.be(true); + await PageObjects.discover.selectTextBaseLang('SQL'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.missingOrFail(); + await browser.goBack(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await searchSessions.exists()).to.be(true); + }); }); async function getSearchSessionId(): Promise {