Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/platform/packages/private/kbn-esql-editor/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependsOn:
- '@kbn/kql'
- '@kbn/ebt-tools'
- '@kbn/shared-ux-utility'
- '@kbn/search-types'
tags:
- shared-browser
- package
Expand Down
227 changes: 61 additions & 166 deletions src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -72,7 +66,6 @@ import {
} from './esql_editor.styles';
import { ESQLEditorTelemetryService } from './telemetry/telemetry_service';
import {
clearCacheWhenOld,
filterDataErrors,
filterDuplicatedWarnings,
filterOutWarningsOverlappingWithErrors,
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoized for performance

() =>
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>(
Expand Down Expand Up @@ -541,7 +540,7 @@ const ESQLEditorInternal = function ESQLEditor({
...args: [
{
esqlQuery: string;
search: any;
search: ISearchGeneric;
timeRange: TimeRange;
signal?: AbortSignal;
dropNullColumns?: boolean;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

const nonOverlappingWarnings = filterOutWarningsOverlappingWithErrors(
allErrors,
unerlinedWarnings
underlinedWarnings
);

const underlinedMessages = [...allErrors, ...nonOverlappingWarnings];
Expand Down Expand Up @@ -966,14 +855,16 @@ const ESQLEditorInternal = function ESQLEditor({
parsedErrors.length ? parsedErrors : []
);
return;
} else {
queryValidation(subscription).catch(() => {});
}
return () => (subscription.active = false);
queryValidation(subscription)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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(
Expand All @@ -998,6 +889,17 @@ const ESQLEditorInternal = function ESQLEditor({
return ESQLLang.getInlineCompletionsProvider?.(esqlCallbacks);
}, [esqlCallbacks]);

const codeEditorHoverProvider = useMemo(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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={{
Expand All @@ -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}
Expand Down
Loading
Loading