diff --git a/src/platform/packages/private/kbn-esql-editor/moon.yml b/src/platform/packages/private/kbn-esql-editor/moon.yml index 979b68fa6f4c1..9b08d72c8a4a4 100644 --- a/src/platform/packages/private/kbn-esql-editor/moon.yml +++ b/src/platform/packages/private/kbn-esql-editor/moon.yml @@ -46,6 +46,7 @@ dependsOn: - '@kbn/kql' - '@kbn/ebt-tools' - '@kbn/shared-ux-utility' + - '@kbn/search-types' tags: - shared-browser - package diff --git a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx index fad6b8d372f2b..87f8b5107f3e1 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx +++ b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx @@ -26,15 +26,10 @@ import { getIndexPatternFromESQLQuery, getESQLSources, getEsqlColumns, - getEsqlPolicies, getJoinIndices, - getTimeseriesIndices, - getInferenceEndpoints, - getEditorExtensions, fixESQLQueryWithVariables, prettifyQuery, hasOnlySourceCommand, - getESQLAdHocDataview, } from '@kbn/esql-utils'; import type { CodeEditorProps } from '@kbn/code-editor'; import { CodeEditor } from '@kbn/code-editor'; @@ -46,8 +41,8 @@ import type { ESQLCallbacks, TelemetryQuerySubmittedProps, } from '@kbn/esql-types'; -import { KQL_TYPE_TO_KIND_MAP } from '@kbn/esql-types'; import { FavoritesClient } from '@kbn/content-management-favorites-public'; +import type { ISearchGeneric } from '@kbn/search-types'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { ILicense } from '@kbn/licensing-types'; import { ESQLLang, ESQL_LANG_ID, monaco } from '@kbn/monaco'; @@ -59,7 +54,6 @@ import { createPortal } from 'react-dom'; import useObservable from 'react-use/lib/useObservable'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { QuerySource } from '@kbn/esql-types'; -import type { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; import { useCanCreateLookupIndex, useLookupIndexCommand } from './lookup_join'; import { EditorFooter } from './editor_footer'; import { QuickSearchVisor } from './editor_visor'; @@ -72,7 +66,6 @@ import { } from './esql_editor.styles'; import { ESQLEditorTelemetryService } from './telemetry/telemetry_service'; import { - clearCacheWhenOld, filterDataErrors, filterDuplicatedWarnings, filterOutWarningsOverlappingWithErrors, @@ -90,9 +83,9 @@ import { useValidationLatencyTracking, } from './use_latency_tracking'; import { addQueriesToCache } from './history_local_storage'; +import type { getHistoryItems } from './history_local_storage'; import { ResizableButton } from './resizable_button'; import { useRestorableRef, useRestorableState, withRestorableState } from './restorable_state'; -import { getHistoryItems } from './history_local_storage'; import type { StarredQueryMetadata } from './editor_footer/esql_starred_queries_service'; import type { ESQLEditorDeps, ESQLEditorProps as ESQLEditorPropsInternal } from './types'; import { @@ -100,6 +93,7 @@ import { addEditorKeyBindings, addTabKeybindingRules, } from './custom_editor_commands'; +import { useEsqlCallbacks } from './use_esql_callbacks'; // for editor width smaller than this value we want to start hiding some text const BREAKPOINT_WIDTH = 540; @@ -178,7 +172,8 @@ const ESQLEditorInternal = function ESQLEditor({ [esqlVariables, query.esql] ); - const variablesService = kibana.services?.esql?.variablesService; + const esqlService = kibana.services?.esql; + const variablesService = esqlService?.variablesService; const histogramBarTarget = uiSettings?.get('histogram:barTarget') ?? 50; const [code, setCode] = useState(fixedQuery ?? ''); // To make server side errors less "sticky", register the state of the code when submitting @@ -202,7 +197,7 @@ const ESQLEditorInternal = function ESQLEditor({ const [isHistoryOpen, setIsHistoryOpen] = useRestorableState('isHistoryOpen', false); const [isLanguageComponentOpen, setIsLanguageComponentOpen] = useState(false); const [isQueryLoading, setIsQueryLoading] = useState(true); - const [abortController, setAbortController] = useState(new AbortController()); + const abortControllerRef = useRef(new AbortController()); const [isVisorOpen, setIsVisorOpen] = useRestorableState('isVisorOpen', false); const [hasUserDismissedVisorAutoOpen, setHasUserDismissedVisorAutoOpen] = useLocalStorage( VISOR_AUTO_OPEN_DISMISSED_KEY, @@ -290,12 +285,12 @@ const ESQLEditorInternal = function ESQLEditor({ const onQuerySubmit = useCallback( (source: TelemetryQuerySubmittedProps['source']) => { if (isQueryLoading && isLoading && allowQueryCancellation) { - abortController?.abort(); + abortControllerRef.current.abort(); setIsQueryLoading(false); } else { setIsQueryLoading(true); const abc = new AbortController(); - setAbortController(abc); + abortControllerRef.current = abc; const currentValue = editorRef.current?.getValue(); if (currentValue != null) { @@ -311,14 +306,7 @@ const ESQLEditorInternal = function ESQLEditor({ onTextLangQuerySubmit({ esql: currentValue } as AggregateQuery, abc); } }, - [ - isQueryLoading, - isLoading, - allowQueryCancellation, - abortController, - onTextLangQuerySubmit, - telemetryService, - ] + [isQueryLoading, isLoading, allowQueryCancellation, onTextLangQuerySubmit, telemetryService] ); const onUpdateAndSubmitQuery = useCallback( @@ -474,13 +462,24 @@ const ESQLEditorInternal = function ESQLEditor({ } }, []); - const styles = esqlEditorStyles( - theme.euiTheme, - editorHeight, - Boolean(editorMessages.errors.length), - Boolean(editorMessages.warnings.length), - Boolean(editorIsInline), - Boolean(hasOutline) + const styles = useMemo( + () => + esqlEditorStyles( + theme.euiTheme, + editorHeight, + Boolean(editorMessages.errors.length), + Boolean(editorMessages.warnings.length), + Boolean(editorIsInline), + Boolean(hasOutline) + ), + [ + theme.euiTheme, + editorHeight, + editorMessages.errors.length, + editorMessages.warnings.length, + editorIsInline, + hasOutline, + ] ); const onMouseDownResize = useCallback( @@ -541,7 +540,7 @@ const ESQLEditorInternal = function ESQLEditor({ ...args: [ { esqlQuery: string; - search: any; + search: ISearchGeneric; timeRange: TimeRange; signal?: AbortSignal; dropNullColumns?: boolean; @@ -645,136 +644,26 @@ const ESQLEditorInternal = function ESQLEditor({ [telemetryService, setIsHistoryOpen] ); - const esqlCallbacks = useMemo(() => { - const callbacks: ESQLCallbacks = { - getSources: async () => { - clearCacheWhenOld(dataSourcesCache, minimalQueryRef.current); - const getLicense = kibana.services?.esql?.getLicense; - const sources = await memoizedSources(core, getLicense).result; - return sources; - }, - getColumnsFor: async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { - if (queryToExecute) { - // Check if there's a stale entry and clear it - clearCacheWhenOld(esqlFieldsCache, `${queryToExecute} | limit 0`); - const timeRange = data.query.timefilter.timefilter.getTime(); - return ( - (await memoizedFieldsFromESQL({ - esqlQuery: queryToExecute, - search: data.search.search, - timeRange, - signal: abortController.signal, - variables: variablesService?.esqlVariables, - dropNullColumns: true, - }).result) || [] - ); - } - return []; - }, - getPolicies: async () => getEsqlPolicies(core.http), - getPreferences: async () => { - return { - histogramBarTarget, - }; - }, - // @ts-expect-error To prevent circular type import, type defined here is partial of full client - getFieldsMetadata: fieldsMetadata?.getClient(), - getVariables: () => { - return variablesService?.esqlVariables; - }, - canSuggestVariables: () => { - return variablesService?.isCreateControlSuggestionEnabled ?? false; - }, - getJoinIndices: getJoinIndicesCallback, - getTimeseriesIndices: async () => { - return (await getTimeseriesIndices(core.http)) || []; - }, - getEditorExtensions: async (queryString: string) => { - // Only fetch recommendations if there's an active solutionId and a non-empty query - // Otherwise the route will return an error - if (activeSolutionId && queryString.trim() !== '') { - return await getEditorExtensions(core.http, queryString, activeSolutionId); - } - return { - recommendedQueries: [], - recommendedFields: [], - }; - }, - getInferenceEndpoints: async (taskType: InferenceTaskType) => { - return (await getInferenceEndpoints(core.http, taskType)) || []; - }, - getLicense: async () => { - const ls = await kibana.services?.esql?.getLicense(); - - if (!ls) { - return undefined; - } - - return { - ...ls, - hasAtLeast: ls.hasAtLeast.bind(ls), - }; - }, - getActiveProduct: () => core.pricing.getActiveProduct(), - getHistoryStarredItems: async () => { - clearCacheWhenOld(historyStarredItemsCache, 'historyStarredItems'); - return await memoizedHistoryStarredItems(getHistoryItems, favoritesClient).result; - }, - canCreateLookupIndex, - isServerless: Boolean(kibana.services?.esql?.isServerless), - getKqlSuggestions: async (kqlQuery: string, cursorPositionInKql: number) => { - const hasQuerySuggestions = kql?.autocomplete?.hasQuerySuggestions('kuery'); - if (!hasQuerySuggestions) { - return undefined; - } - const dataView = await getESQLAdHocDataview({ - dataViewsService: data.dataViews, - query: minimalQueryRef.current, - }); - const suggestions = await kql?.autocomplete.getQuerySuggestions({ - language: 'kuery', - query: kqlQuery, - selectionStart: cursorPositionInKql, - selectionEnd: cursorPositionInKql, - indexPatterns: [dataView], - }); - return ( - suggestions?.map((suggestion) => { - return { - text: suggestion.text, - label: suggestion.text, - detail: - typeof suggestion.description === 'string' ? suggestion.description : undefined, - kind: KQL_TYPE_TO_KIND_MAP[suggestion.type] ?? 'Value', - }; - }) ?? [] - ); - }, - }; - return callbacks; - }, [ + const esqlCallbacks = useEsqlCallbacks({ + core, + data, + kql, fieldsMetadata, - getJoinIndicesCallback, + esqlService, + histogramBarTarget, + activeSolutionId: activeSolutionId ?? undefined, canCreateLookupIndex, - kibana.services?.esql, - kql?.autocomplete, + minimalQueryRef, + abortControllerRef, dataSourcesCache, memoizedSources, - core, esqlFieldsCache, - data.query.timefilter.timefilter, - data.search.search, - data.dataViews, memoizedFieldsFromESQL, - abortController.signal, - variablesService?.esqlVariables, - variablesService?.isCreateControlSuggestionEnabled, - histogramBarTarget, - activeSolutionId, historyStarredItemsCache, memoizedHistoryStarredItems, favoritesClient, - ]); + getJoinIndicesCallback, + }); const queryRunButtonProperties = useMemo(() => { if (allowQueryCancellation && isLoading) { @@ -872,10 +761,10 @@ const ESQLEditorInternal = function ESQLEditor({ allWarnings = [...parserWarnings, ...externalErrorsParsedWarnings]; } - const unerlinedWarnings = allWarnings.filter((warning) => warning.underlinedWarning); + const underlinedWarnings = allWarnings.filter((warning) => warning.underlinedWarning); const nonOverlappingWarnings = filterOutWarningsOverlappingWithErrors( allErrors, - unerlinedWarnings + underlinedWarnings ); const underlinedMessages = [...allErrors, ...nonOverlappingWarnings]; @@ -966,14 +855,16 @@ const ESQLEditorInternal = function ESQLEditor({ parsedErrors.length ? parsedErrors : [] ); return; - } else { - queryValidation(subscription).catch(() => {}); } - return () => (subscription.active = false); + queryValidation(subscription) + .catch(() => {}) + .finally(() => { + subscription.active = false; + }); }, { skipFirstRender: false }, 256, - [serverErrors, serverWarning, code, queryValidation] + [serverErrors, serverWarning, code, codeWhenSubmitted, queryValidation] ); const suggestionProvider = useMemo( @@ -998,6 +889,17 @@ const ESQLEditorInternal = function ESQLEditor({ return ESQLLang.getInlineCompletionsProvider?.(esqlCallbacks); }, [esqlCallbacks]); + const codeEditorHoverProvider = useMemo( + () => ({ + provideHover: ( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken + ) => hoverProvider?.provideHover?.(model, position, token) ?? { contents: [] }, + }), + [hoverProvider] + ); + const onErrorClick = useCallback(({ startLineNumber, startColumn }: MonacoMessage) => { if (!editorRef.current) { return; @@ -1197,14 +1099,7 @@ const ESQLEditorInternal = function ESQLEditor({ options={codeEditorOptions} width="100%" suggestionProvider={suggestionProvider} - hoverProvider={{ - provideHover: (model, position, token) => { - if (!hoverProvider?.provideHover) { - return { contents: [] }; - } - return hoverProvider?.provideHover(model, position, token); - }, - }} + hoverProvider={codeEditorHoverProvider} signatureProvider={signatureProvider} inlineCompletionsProvider={inlineCompletionsProvider} onChange={onQueryUpdate} @@ -1368,7 +1263,7 @@ const ESQLEditorInternal = function ESQLEditor({ onErrorClick={onErrorClick} /> {createPortal( - Object.keys(popoverPosition).length !== 0 && popoverPosition.constructor === Object && ( + Object.keys(popoverPosition).length > 0 && (
{ const isFirstRender = useRef(true); - const newDeps = [...(deps || []), isFirstRender]; + const newDeps = deps || []; return useDebounce( () => { @@ -62,50 +62,45 @@ const maxWarningLength = 1000; export const parseWarning = (warning: string): MonacoMessage[] => { // we limit the length to reduce ReDoS risks const truncatedWarning = warning.substring(0, maxWarningLength); - if (quotedWarningMessageRegexp.test(truncatedWarning)) { - const matches = truncatedWarning.match(quotedWarningMessageRegexp); - if (matches) { - return matches.map((message) => { - // replaces the quotes only if they are not escaped, - let warningMessage = message.replace(/(? { + // replaces the quotes only if they are not escaped, + let warningMessage = message.replace(/(?) => { line-height: 1.5rem; border-radius: ${theme.euiTheme.border.radius.medium} !important; box-shadow: ${theme.euiTheme.shadows.l.down} !important; - z-index: 100; + z-index: ${theme.euiTheme.levels.flyout}; } // Fixes inline suggestions hover styles and only @@ -322,7 +317,7 @@ export const getEditorOverwrites = (theme: UseEuiTheme<{}>) => { border-radius: ${theme.euiTheme.border.radius.medium}; ${euiShadow(theme, 'l')} // Suggestions must be rendered above flyouts - z-index: 1100 !important; + z-index: ${theme.euiTheme.levels.toast} !important; } .suggest-details-container { diff --git a/src/platform/packages/private/kbn-esql-editor/src/history_local_storage.ts b/src/platform/packages/private/kbn-esql-editor/src/history_local_storage.ts index 8c642e8630d05..79e80b3b51048 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/history_local_storage.ts +++ b/src/platform/packages/private/kbn-esql-editor/src/history_local_storage.ts @@ -99,22 +99,14 @@ export const addQueriesToCache = (itemToAddOrUpdate: QueryHistoryItem) => { let allQueries = [...getCachedQueries()]; - // Check storage size and trim if needed - const checkAndTrimBySize = (queryList: QueryHistoryItem[]): QueryHistoryItem[] => { - const storageString = JSON.stringify(queryList); - const storageSizeKB = new Blob([storageString]).size / 1024; - - if (storageSizeKB > MAX_STORAGE_SIZE_KB && queryList.length > 10) { - // Remove oldest queries until under size limit or minimum count - const sortedByDate = queryList.sort((a, b) => sortDates(b.timeRan, a.timeRan)); - return checkAndTrimBySize(sortedByDate.slice(0, -1)); - } - - return queryList; - }; + const getStorageSizeKB = (queryList: QueryHistoryItem[]) => + new Blob([JSON.stringify(queryList)]).size / 1024; - // Apply storage-based trimming - allQueries = checkAndTrimBySize(allQueries); + // Apply storage-based trimming (sort once, then trim iteratively) + allQueries = allQueries.sort((a, b) => sortDates(b.timeRan, a.timeRan)); + while (getStorageSizeKB(allQueries) > MAX_STORAGE_SIZE_KB && allQueries.length > 10) { + allQueries.pop(); + } // Update cache with final query list cachedQueries.clear(); diff --git a/src/platform/packages/private/kbn-esql-editor/src/use_esql_callbacks.ts b/src/platform/packages/private/kbn-esql-editor/src/use_esql_callbacks.ts new file mode 100644 index 0000000000000..ce67c9ee923c1 --- /dev/null +++ b/src/platform/packages/private/kbn-esql-editor/src/use_esql_callbacks.ts @@ -0,0 +1,280 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useCallback, useMemo, type MutableRefObject } from 'react'; +import type { CoreStart } from '@kbn/core/public'; +import type { TimeRange } from '@kbn/es-query'; +import type { ESQLCallbacks, ESQLControlVariable } from '@kbn/esql-types'; +import { KQL_TYPE_TO_KIND_MAP } from '@kbn/esql-types'; +import type { ISearchGeneric } from '@kbn/search-types'; +import type { ILicense } from '@kbn/licensing-types'; +import type { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; +import type { MapCache } from 'lodash'; +import type { FavoritesClient } from '@kbn/content-management-favorites-public'; +import { + getESQLAdHocDataview, + getEditorExtensions, + getEsqlPolicies, + getInferenceEndpoints, + getTimeseriesIndices, +} from '@kbn/esql-utils'; +import type { getEsqlColumns, getESQLSources } from '@kbn/esql-utils'; +import { clearCacheWhenOld } from './helpers'; +import { getHistoryItems } from './history_local_storage'; +import type { ESQLEditorDeps } from './types'; +import type { StarredQueryMetadata } from './editor_footer/esql_starred_queries_service'; + +type MemoizedFn = (...args: TArgs) => { + timestamp: number; + result: TResult; +}; + +type MemoizedFieldsFromESQL = MemoizedFn< + [ + { + esqlQuery: string; + search: ISearchGeneric; + timeRange: TimeRange; + signal?: AbortSignal; + dropNullColumns?: boolean; + variables?: ESQLControlVariable[]; + } + ], + ReturnType +>; + +type MemoizedSources = MemoizedFn< + [CoreStart, (() => Promise) | undefined], + ReturnType +>; + +type MemoizedHistoryStarredItems = MemoizedFn< + [typeof getHistoryItems, FavoritesClient], + Promise +>; + +interface UseEsqlCallbacksParams { + core: CoreStart; + data: ESQLEditorDeps['data']; + kql?: ESQLEditorDeps['kql']; + fieldsMetadata?: ESQLEditorDeps['fieldsMetadata']; + esqlService?: ESQLEditorDeps['esql']; + histogramBarTarget: number; + activeSolutionId?: Parameters[2]; + canCreateLookupIndex: ESQLCallbacks['canCreateLookupIndex']; + minimalQueryRef: MutableRefObject; + abortControllerRef: MutableRefObject; + dataSourcesCache: MapCache; + memoizedSources: MemoizedSources; + esqlFieldsCache: MapCache; + memoizedFieldsFromESQL: MemoizedFieldsFromESQL; + historyStarredItemsCache: MapCache; + memoizedHistoryStarredItems: MemoizedHistoryStarredItems; + favoritesClient: FavoritesClient; + getJoinIndicesCallback: Required['getJoinIndices']; +} + +export const useEsqlCallbacks = ({ + core, + data, + kql, + fieldsMetadata, + esqlService, + histogramBarTarget, + activeSolutionId, + canCreateLookupIndex, + minimalQueryRef, + abortControllerRef, + dataSourcesCache, + memoizedSources, + esqlFieldsCache, + memoizedFieldsFromESQL, + historyStarredItemsCache, + memoizedHistoryStarredItems, + favoritesClient, + getJoinIndicesCallback, +}: UseEsqlCallbacksParams): ESQLCallbacks => { + const getSources = useCallback(async () => { + clearCacheWhenOld(dataSourcesCache, minimalQueryRef.current); + const getLicense = esqlService?.getLicense; + const sources = await memoizedSources(core, getLicense).result; + return sources; + }, [dataSourcesCache, minimalQueryRef, memoizedSources, core, esqlService]); + + const getColumnsFor = useCallback( + async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { + if (queryToExecute) { + // Check if there's a stale entry and clear it + clearCacheWhenOld(esqlFieldsCache, `${queryToExecute} | limit 0`); + const timeRange = data.query.timefilter.timefilter.getTime(); + return ( + (await memoizedFieldsFromESQL({ + esqlQuery: queryToExecute, + search: data.search.search, + timeRange, + signal: abortControllerRef.current.signal, + variables: esqlService?.variablesService?.esqlVariables, + dropNullColumns: true, + }).result) || [] + ); + } + return []; + }, + [ + data.query.timefilter.timefilter, + data.search.search, + esqlFieldsCache, + memoizedFieldsFromESQL, + abortControllerRef, + esqlService, + ] + ); + + const getPolicies = useCallback(async () => getEsqlPolicies(core.http), [core.http]); + + const getPreferences = useCallback( + async () => ({ + histogramBarTarget, + }), + [histogramBarTarget] + ); + + const fieldsMetadataClient = useMemo(() => fieldsMetadata?.getClient(), [fieldsMetadata]); + + const getVariables = useCallback( + () => esqlService?.variablesService?.esqlVariables, + [esqlService] + ); + + const canSuggestVariables = useCallback( + () => esqlService?.variablesService?.isCreateControlSuggestionEnabled ?? false, + [esqlService] + ); + + const getTimeseriesIndicesCallback = useCallback(async () => { + return (await getTimeseriesIndices(core.http)) || []; + }, [core.http]); + + const getEditorExtensionsCallback = useCallback( + async (queryString: string) => { + // Only fetch recommendations if there's an active solutionId and a non-empty query + // Otherwise the route will return an error + if (activeSolutionId && queryString.trim() !== '') { + return await getEditorExtensions(core.http, queryString, activeSolutionId); + } + return { + recommendedQueries: [], + recommendedFields: [], + }; + }, + [activeSolutionId, core.http] + ); + + const getInferenceEndpointsCallback = useCallback( + async (taskType: InferenceTaskType) => { + return (await getInferenceEndpoints(core.http, taskType)) || []; + }, + [core.http] + ); + + const getLicense = useCallback(async () => { + const ls = await esqlService?.getLicense(); + + if (!ls) { + return undefined; + } + + return { + ...ls, + hasAtLeast: ls.hasAtLeast.bind(ls), + }; + }, [esqlService]); + + const getActiveProduct = useCallback(() => core.pricing.getActiveProduct(), [core.pricing]); + + const getHistoryStarredItems = useCallback(async () => { + clearCacheWhenOld(historyStarredItemsCache, 'historyStarredItems'); + return await memoizedHistoryStarredItems(getHistoryItems, favoritesClient).result; + }, [historyStarredItemsCache, memoizedHistoryStarredItems, favoritesClient]); + + const isServerless = Boolean(esqlService?.isServerless); + + const getKqlSuggestions = useCallback( + async (kqlQuery: string, cursorPositionInKql: number) => { + const hasQuerySuggestions = kql?.autocomplete?.hasQuerySuggestions('kuery'); + if (!hasQuerySuggestions) { + return undefined; + } + const dataView = await getESQLAdHocDataview({ + dataViewsService: data.dataViews, + query: minimalQueryRef.current, + }); + const suggestions = await kql?.autocomplete.getQuerySuggestions({ + language: 'kuery', + query: kqlQuery, + selectionStart: cursorPositionInKql, + selectionEnd: cursorPositionInKql, + indexPatterns: [dataView], + }); + return ( + suggestions?.map((suggestion) => { + return { + text: suggestion.text, + label: suggestion.text, + detail: typeof suggestion.description === 'string' ? suggestion.description : undefined, + kind: KQL_TYPE_TO_KIND_MAP[suggestion.type] ?? 'Value', + }; + }) ?? [] + ); + }, + [data.dataViews, kql?.autocomplete, minimalQueryRef] + ); + + return useMemo( + () => ({ + getSources, + getColumnsFor, + getPolicies, + getPreferences, + // @ts-expect-error To prevent circular type import, type defined here is partial of full client + getFieldsMetadata: fieldsMetadataClient, + getVariables, + canSuggestVariables, + getJoinIndices: getJoinIndicesCallback, + getTimeseriesIndices: getTimeseriesIndicesCallback, + getEditorExtensions: getEditorExtensionsCallback, + getInferenceEndpoints: getInferenceEndpointsCallback, + getLicense, + getActiveProduct, + getHistoryStarredItems, + canCreateLookupIndex, + isServerless, + getKqlSuggestions, + }), + [ + getSources, + getColumnsFor, + getPolicies, + getPreferences, + fieldsMetadataClient, + getVariables, + canSuggestVariables, + getJoinIndicesCallback, + getTimeseriesIndicesCallback, + getEditorExtensionsCallback, + getInferenceEndpointsCallback, + getLicense, + getActiveProduct, + getHistoryStarredItems, + canCreateLookupIndex, + isServerless, + getKqlSuggestions, + ] + ); +}; diff --git a/src/platform/packages/private/kbn-esql-editor/tsconfig.json b/src/platform/packages/private/kbn-esql-editor/tsconfig.json index 44736281f179a..ab542cf3c107c 100644 --- a/src/platform/packages/private/kbn-esql-editor/tsconfig.json +++ b/src/platform/packages/private/kbn-esql-editor/tsconfig.json @@ -40,7 +40,8 @@ "@kbn/calculate-width-from-char-count", "@kbn/kql", "@kbn/ebt-tools", - "@kbn/shared-ux-utility" + "@kbn/shared-ux-utility", + "@kbn/search-types" ], "exclude": [ "target/**/*",