-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[ES|QL] Small improvements on the editor #251004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f566140
7de7745
0663d02
814afc8
9a8b803
f6d59ec
ac26967
a893686
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,16 +83,17 @@ 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 { | ||
| registerCustomCommands, | ||
| 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<string>(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<typeof onMouseDownResizeHandler>( | ||
|
|
@@ -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<ESQLCallbacks>(() => { | ||
| 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); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
| 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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was not working properly |
||
| .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( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memoized for performance like the rest providers |
||
| () => ({ | ||
| 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 && ( | ||
| <div | ||
| tabIndex={0} | ||
| style={{ | ||
|
|
@@ -1377,7 +1272,7 @@ const ESQLEditorInternal = function ESQLEditor({ | |
| borderRadius: theme.euiTheme.border.radius.small, | ||
| position: 'absolute', | ||
| overflow: 'auto', | ||
| zIndex: 1001, | ||
| zIndex: theme.euiTheme.levels.modal, | ||
| border: theme.euiTheme.border.thin, | ||
| }} | ||
| ref={popoverRef} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memoized for performance