diff --git a/x-pack/platform/plugins/shared/lens/moon.yml b/x-pack/platform/plugins/shared/lens/moon.yml index cad24b5a95572..2df82321bd542 100644 --- a/x-pack/platform/plugins/shared/lens/moon.yml +++ b/x-pack/platform/plugins/shared/lens/moon.yml @@ -153,6 +153,7 @@ dependsOn: - '@kbn/as-code-shared-schemas' - '@kbn/as-code-shared-telemetry' - '@kbn/core-http-browser' + - '@kbn/use-observable' tags: - plugin - prod diff --git a/x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/config_panel/esql_editor.tsx b/x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/config_panel/esql_editor.tsx index dd417c6130f80..4c623161ddd42 100644 --- a/x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/config_panel/esql_editor.tsx +++ b/x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/config_panel/esql_editor.tsx @@ -9,18 +9,19 @@ import { css } from '@emotion/react'; import { EuiFlexItem, useEuiTheme } from '@elastic/eui'; import type { AggregateQuery, Query } from '@kbn/es-query'; import { isOfAggregateQueryType } from '@kbn/es-query'; -import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common'; import { useFetchContext } from '@kbn/presentation-publishing'; import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { isEqual } from 'lodash'; import type { MutableRefObject } from 'react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ESQLLangEditor, useESQLQueryStats } from '@kbn/esql/public'; import { type ESQLControlVariable, type ESQLQueryStats } from '@kbn/esql-types'; import { i18n } from '@kbn/i18n'; import React from 'react'; import type { DataViewSpec } from '@kbn/data-views-plugin/common'; import type { Simplify } from '@kbn/chart-expressions-common'; +import { useObservable } from '@kbn/use-observable'; +import { EMPTY } from 'rxjs'; import { useCurrentAttributes } from '../../../app_plugin/shared/edit_on_the_fly/use_current_attributes'; import { useESQLEditorContext } from './esql_editor_context'; import { getActiveDataFromDatatable } from '../../../state_management/shared_logic'; @@ -29,6 +30,7 @@ import { useLensDispatch, useLensSelector, selectCanEditTextBasedQuery, + selectSearchSessionId, } from '../../../state_management'; import type { ESQLDataGridAttrs } from '../../../app_plugin/shared/edit_on_the_fly/helpers'; import { getSuggestions } from '../../../app_plugin/shared/edit_on_the_fly/helpers'; @@ -96,6 +98,10 @@ export function ESQLEditor({ const { visualizationMap, datasourceMap } = useEditorFrameService(); const { visualization } = useLensSelector((state) => state.lens); const canEditTextBasedQuery = useLensSelector(selectCanEditTextBasedQuery); + // Updated when the workspace kicks off a new search (manual refresh, auto-refresh, + // or when chart requests run under a new session). Used as an effect dependency to + // re-fetch the ES|QL results grid for the last submitted query. + const searchSessionId = useLensSelector(selectSearchSessionId); const [errors, setErrors] = useState([]); const [submittedQuery, setSubmittedQuery] = useState( @@ -119,42 +125,48 @@ export function ESQLEditor({ const currentAttributesRef = useRef(currentAttributes); currentAttributesRef.current = currentAttributes; - const adHocDataViews = - attributes && attributes.state.adHocDataViews - ? Object.values(attributes.state.adHocDataViews) - : Object.values(framePublicAPI.dataViews.indexPatterns).map((index) => index.spec); + const adHocDataViews = useMemo(() => { + if (attributes && attributes.state.adHocDataViews) { + return Object.values(attributes.state.adHocDataViews); + } + return Object.values(framePublicAPI.dataViews.indexPatterns).map((index) => index.spec); + }, [attributes, framePublicAPI.dataViews.indexPatterns]); - const previousAdapters = useRef | undefined>(lensAdapters); + const lensAdaptersRef = useRef(lensAdapters); + lensAdaptersRef.current = lensAdapters; + + // Avoids duplicating the first grid load + const isInitialRenderRef = useRef(true); + + const submittedQueryRef = useRef(submittedQuery); + submittedQueryRef.current = submittedQuery; const { esqlVariables } = useFetchContext({ uuid: panelId, parentApi }); const esqlQueryStats = useESQLQueryStats(isTextBasedLanguage, lensAdapters?.requests); const dispatch = useLensDispatch(); + // Update activeData and column limit indicator when chart data finishes loading + const isDataLoading = useObservable(dataLoading$ ?? EMPTY); + useEffect(() => { - const s = dataLoading$?.subscribe((isDataLoading) => { - // go thru only when the loading is complete - if (isDataLoading) { - return; - } - const activeData = getActiveDataFromDatatable( - layerId, - previousAdapters.current?.tables?.tables - ); - const table = activeData?.[layerId]; + if (isDataLoading !== false) { + return; + } - if (table) { - // there are cases where a query can return a big amount of columns - // at this case we don't suggest all columns in a table but the first `MAX_NUM_OF_COLUMNS` - setSuggestsLimitedColumns(table.columns.length >= MAX_NUM_OF_COLUMNS); - } + const activeData = getActiveDataFromDatatable(layerId, lensAdaptersRef.current?.tables?.tables); - if (Object.keys(activeData).length > 0) { - dispatch(onActiveDataChange({ activeData })); - } - }); - return () => s?.unsubscribe(); - }, [dataLoading$, dispatch, layerId]); + const table = activeData?.[layerId]; + if (table) { + // there are cases where a query can return a big amount of columns + // at this case we don't suggest all columns in a table but the first `MAX_NUM_OF_COLUMNS` + setSuggestsLimitedColumns(table.columns.length >= MAX_NUM_OF_COLUMNS); + } + + if (Object.keys(activeData).length > 0) { + dispatch(onActiveDataChange({ activeData })); + } + }, [isDataLoading, dispatch, layerId]); const runQuery = useCallback( async (q: AggregateQuery, abortController?: AbortController, shouldUpdateAttrs?: boolean) => { @@ -215,6 +227,54 @@ export function ESQLEditor({ }); }, [query, submittedQuery, errors.length, onTextBasedQueryStateChange]); + // Refresh the ES|QL results table for the last submitted query when inputs to the preview + // request change without the user submitting again. + useEffect(() => { + // Skip the initial render, the grid is populated by useInitializeChart → runQuery + if (isInitialRenderRef.current) { + isInitialRenderRef.current = false; + return; + } + + const lastSubmittedQuery = submittedQueryRef.current; + if (!isOfAggregateQueryType(lastSubmittedQuery)) { + return; + } + + const abortController = new AbortController(); + + getSuggestions( + lastSubmittedQuery, + data, + http, + uiSettings, + datasourceMap, + visualizationMap, + adHocDataViews, + undefined, + abortController, + setDataGridAttrs, + esqlVariables, + false, + currentAttributesRef.current + ).catch(() => { + // The chart itself will surface query errors via its own error handling path + }); + + return () => { + abortController.abort(); + }; + }, [ + searchSessionId, + esqlVariables, + data, + http, + uiSettings, + datasourceMap, + visualizationMap, + adHocDataViews, + ]); + // Early exit if it's not in TextBased mode or the editor should be hidden if (!isTextBasedLanguage || !canEditTextBasedQuery || !isOfAggregateQueryType(query)) { return null; diff --git a/x-pack/platform/plugins/shared/lens/tsconfig.json b/x-pack/platform/plugins/shared/lens/tsconfig.json index d86303faf9d58..436def6640f2e 100644 --- a/x-pack/platform/plugins/shared/lens/tsconfig.json +++ b/x-pack/platform/plugins/shared/lens/tsconfig.json @@ -147,7 +147,8 @@ "@kbn/esql-language", "@kbn/as-code-shared-schemas", "@kbn/as-code-shared-telemetry", - "@kbn/core-http-browser" + "@kbn/core-http-browser", + "@kbn/use-observable" ], "exclude": ["target/**/*"] }