mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ES|QL] Integrating the editor with the time picker (#187047)
Part of #189010 ## Summary It displays the date picker for date fields. It is currently only possible for where and eval commands (and not in stats bucker) exactly as the earliest and latest params mostly because the bucket command is not well defined and we need to fix the autocomplete first before we integrate the latest / earliest params and the timepicker  ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Drew Tate <andrew.tate@elastic.co>
This commit is contained in:
parent
f5d9b9db35
commit
dd25429d8e
4 changed files with 170 additions and 36 deletions
|
@ -15,7 +15,7 @@ import { timeUnitsToSuggest } from '../../definitions/literals';
|
|||
import { groupingFunctionDefinitions } from '../../definitions/grouping';
|
||||
import * as autocomplete from '../autocomplete';
|
||||
import type { ESQLCallbacks } from '../../shared/types';
|
||||
import type { EditorContext } from '../types';
|
||||
import type { EditorContext, SuggestionRawDefinition } from '../types';
|
||||
import { TIME_SYSTEM_PARAMS } from '../factories';
|
||||
|
||||
export interface Integration {
|
||||
|
@ -28,6 +28,13 @@ export interface Integration {
|
|||
}>;
|
||||
}
|
||||
|
||||
export type PartialSuggestionWithText = Partial<SuggestionRawDefinition> & { text: string };
|
||||
|
||||
export const TIME_PICKER_SUGGESTION: PartialSuggestionWithText = {
|
||||
text: '',
|
||||
label: 'Choose from the time picker',
|
||||
};
|
||||
|
||||
export const triggerCharacters = [',', '(', '=', ' '];
|
||||
|
||||
export const fields: Array<{ name: string; type: string; suggestedAs?: string }> = [
|
||||
|
@ -224,7 +231,7 @@ export function getLiteralsByType(_type: string | string[]) {
|
|||
|
||||
export function getDateLiteralsByFieldType(_requestedType: string | string[]) {
|
||||
const requestedType = Array.isArray(_requestedType) ? _requestedType : [_requestedType];
|
||||
return requestedType.includes('date') ? TIME_SYSTEM_PARAMS : [];
|
||||
return requestedType.includes('date') ? [TIME_PICKER_SUGGESTION, ...TIME_SYSTEM_PARAMS] : [];
|
||||
}
|
||||
|
||||
export function createCustomCallbackMocks(
|
||||
|
|
|
@ -25,12 +25,14 @@ import {
|
|||
createCustomCallbackMocks,
|
||||
createCompletionContext,
|
||||
getPolicyFields,
|
||||
PartialSuggestionWithText,
|
||||
TIME_PICKER_SUGGESTION,
|
||||
} from './__tests__/helpers';
|
||||
|
||||
describe('autocomplete', () => {
|
||||
type TestArgs = [
|
||||
string,
|
||||
string[],
|
||||
Array<string | PartialSuggestionWithText>,
|
||||
string?,
|
||||
number?,
|
||||
Parameters<typeof createCustomCallbackMocks>?
|
||||
|
@ -39,7 +41,7 @@ describe('autocomplete', () => {
|
|||
const _testSuggestionsFn = (
|
||||
{ only, skip }: { only?: boolean; skip?: boolean } = {},
|
||||
statement: string,
|
||||
expected: string[],
|
||||
expected: Array<string | PartialSuggestionWithText>,
|
||||
triggerCharacter?: string,
|
||||
_offset?: number,
|
||||
customCallbacksArgs: Parameters<typeof createCustomCallbackMocks> = [
|
||||
|
@ -66,10 +68,20 @@ describe('autocomplete', () => {
|
|||
callbackMocks
|
||||
);
|
||||
|
||||
const sortedSuggestions = suggestions.map((suggestion) => suggestion.text).sort();
|
||||
const sortedExpected = expected.sort();
|
||||
const sortedSuggestionTexts = suggestions.map((suggestion) => suggestion.text).sort();
|
||||
const sortedExpectedTexts = expected
|
||||
.map((suggestion) => (typeof suggestion === 'string' ? suggestion : suggestion.text ?? ''))
|
||||
.sort();
|
||||
|
||||
expect(sortedSuggestions).toEqual(sortedExpected);
|
||||
expect(sortedSuggestionTexts).toEqual(sortedExpectedTexts);
|
||||
const expectedNonStringSuggestions = expected.filter(
|
||||
(suggestion) => typeof suggestion !== 'string'
|
||||
) as PartialSuggestionWithText[];
|
||||
|
||||
for (const expectedSuggestion of expectedNonStringSuggestions) {
|
||||
const suggestion = suggestions.find((s) => s.text === expectedSuggestion.text);
|
||||
expect(suggestion).toEqual(expect.objectContaining(expectedSuggestion));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -755,28 +767,30 @@ describe('autocomplete', () => {
|
|||
|
||||
const suggestedConstants = param.literalSuggestions || param.literalOptions;
|
||||
|
||||
const addCommaIfRequired = (s: string | PartialSuggestionWithText) => {
|
||||
// don't add commas to the empty string or if there are no more required args
|
||||
if (!requiresMoreArgs || s === '' || (typeof s === 'object' && s.text === '')) {
|
||||
return s;
|
||||
}
|
||||
return typeof s === 'string' ? `${s},` : { ...s, text: `${s.text},` };
|
||||
};
|
||||
|
||||
testSuggestions(
|
||||
`from a | eval ${fn.name}(${Array(i).fill('field').join(', ')}${i ? ',' : ''} )`,
|
||||
suggestedConstants?.length
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)
|
||||
: [
|
||||
...getDateLiteralsByFieldType(
|
||||
getTypesFromParamDefs(acceptsFieldParamDefs)
|
||||
).map((l) => (requiresMoreArgs ? `${l},` : l)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)).map(
|
||||
(f) => (requiresMoreArgs ? `${f},` : f)
|
||||
),
|
||||
...getDateLiteralsByFieldType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
getTypesFromParamDefs(acceptsFieldParamDefs),
|
||||
{ evalMath: true },
|
||||
undefined,
|
||||
[fn.name]
|
||||
).map((l) => (requiresMoreArgs ? `${l},` : l)),
|
||||
...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)).map((d) =>
|
||||
requiresMoreArgs ? `${d},` : d
|
||||
),
|
||||
],
|
||||
...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)),
|
||||
].map(addCommaIfRequired),
|
||||
' '
|
||||
);
|
||||
testSuggestions(
|
||||
|
@ -786,23 +800,17 @@ describe('autocomplete', () => {
|
|||
suggestedConstants?.length
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)
|
||||
: [
|
||||
...getDateLiteralsByFieldType(
|
||||
getTypesFromParamDefs(acceptsFieldParamDefs)
|
||||
).map((l) => (requiresMoreArgs ? `${l},` : l)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)).map(
|
||||
(f) => (requiresMoreArgs ? `${f},` : f)
|
||||
),
|
||||
...getDateLiteralsByFieldType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
getTypesFromParamDefs(acceptsFieldParamDefs),
|
||||
{ evalMath: true },
|
||||
undefined,
|
||||
[fn.name]
|
||||
).map((l) => (requiresMoreArgs ? `${l},` : l)),
|
||||
...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)).map((d) =>
|
||||
requiresMoreArgs ? `${d},` : d
|
||||
),
|
||||
],
|
||||
...getLiteralsByType(getTypesFromParamDefs(constantOnlyParamDefs)),
|
||||
].map(addCommaIfRequired),
|
||||
' '
|
||||
);
|
||||
}
|
||||
|
@ -860,12 +868,13 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | eval var0=date_trunc()',
|
||||
[
|
||||
...TIME_SYSTEM_PARAMS.map((t) => `${t},`),
|
||||
...[...TIME_SYSTEM_PARAMS].map((t) => `${t},`),
|
||||
...getLiteralsByType('time_literal').map((t) => `${t},`),
|
||||
...getFunctionSignaturesByReturnType('eval', 'date', { evalMath: true }, undefined, [
|
||||
'date_trunc',
|
||||
]).map((t) => `${t},`),
|
||||
...getFieldNamesByType('date').map((t) => `${t},`),
|
||||
TIME_PICKER_SUGGESTION,
|
||||
],
|
||||
'('
|
||||
);
|
||||
|
|
|
@ -394,11 +394,39 @@ export function getCompatibleLiterals(commandName: string, types: string[], name
|
|||
}
|
||||
|
||||
export function getDateLiterals() {
|
||||
return buildConstantsDefinitions(
|
||||
TIME_SYSTEM_PARAMS,
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.namedParamDefinition', {
|
||||
defaultMessage: 'Named parameter',
|
||||
}),
|
||||
'1A'
|
||||
);
|
||||
return [
|
||||
...buildConstantsDefinitions(
|
||||
TIME_SYSTEM_PARAMS,
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.namedParamDefinition', {
|
||||
defaultMessage: 'Named parameter',
|
||||
}),
|
||||
'1A'
|
||||
),
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.chooseFromTimePickerLabel',
|
||||
{
|
||||
defaultMessage: 'Choose from the time picker',
|
||||
}
|
||||
),
|
||||
text: '',
|
||||
kind: 'Issue',
|
||||
detail: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.chooseFromTimePicker',
|
||||
{
|
||||
defaultMessage: 'Click to choose',
|
||||
}
|
||||
),
|
||||
sortText: '1A',
|
||||
command: {
|
||||
id: 'esql.timepicker.choose',
|
||||
title: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.chooseFromTimePicker',
|
||||
{
|
||||
defaultMessage: 'Click to choose',
|
||||
}
|
||||
),
|
||||
},
|
||||
} as SuggestionRawDefinition,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ import {
|
|||
EuiOutsideClickDetector,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
EuiDatePicker,
|
||||
} from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { CodeEditor, CodeEditorProps } from '@kbn/code-editor';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
|
@ -33,6 +35,7 @@ import { ESQLLang, ESQL_LANG_ID, ESQL_THEME_ID, monaco, type ESQLCallbacks } fro
|
|||
import classNames from 'classnames';
|
||||
import memoize from 'lodash/memoize';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { css } from '@emotion/react';
|
||||
import { EditorFooter } from './editor_footer';
|
||||
import { ErrorsWarningsCompactViewPopover } from './errors_warnings_popover';
|
||||
|
@ -143,6 +146,7 @@ let clickedOutside = false;
|
|||
let initialRender = true;
|
||||
let updateLinesFromModel = false;
|
||||
let lines = 1;
|
||||
let isDatePickerOpen = false;
|
||||
|
||||
export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
||||
query,
|
||||
|
@ -166,6 +170,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
hideQueryHistory,
|
||||
hideHeaderWhenExpanded,
|
||||
}: TextBasedLanguagesEditorProps) {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const language = getAggregateQueryMode(query);
|
||||
const queryString: string = query[language] ?? '';
|
||||
|
@ -187,7 +192,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
const [editorHeight, setEditorHeight] = useState(
|
||||
isCodeEditorExpanded ? EDITOR_INITIAL_HEIGHT_EXPANDED : EDITOR_INITIAL_HEIGHT
|
||||
);
|
||||
|
||||
const [popoverPosition, setPopoverPosition] = useState<{ top?: number; left?: number }>({});
|
||||
const [timePickerDate, setTimePickerDate] = useState(moment());
|
||||
const [measuredEditorWidth, setMeasuredEditorWidth] = useState(0);
|
||||
const [measuredContentWidth, setMeasuredContentWidth] = useState(0);
|
||||
|
||||
|
@ -291,6 +297,24 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
setIsHistoryOpen(status);
|
||||
}, []);
|
||||
|
||||
const openTimePickerPopover = useCallback(() => {
|
||||
const currentCursorPosition = editor1.current?.getPosition();
|
||||
const editorCoords = editor1.current?.getDomNode()!.getBoundingClientRect();
|
||||
if (currentCursorPosition && editorCoords) {
|
||||
const editorPosition = editor1.current!.getScrolledVisiblePosition(currentCursorPosition);
|
||||
const editorTop = editorCoords.top;
|
||||
const editorLeft = editorCoords.left;
|
||||
|
||||
// Calculate the absolute position of the popover
|
||||
const absoluteTop = editorTop + (editorPosition?.top ?? 0) + 20;
|
||||
const absoluteLeft = editorLeft + (editorPosition?.left ?? 0);
|
||||
|
||||
setPopoverPosition({ top: absoluteTop, left: absoluteLeft });
|
||||
isDatePickerOpen = true;
|
||||
popoverRef.current?.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Registers a command to redirect users to the index management page
|
||||
// to create a new policy. The command is called by the buildNoPoliciesAvailableDefinition
|
||||
monaco.editor.registerCommand('esql.policies.create', (...args) => {
|
||||
|
@ -300,6 +324,10 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
});
|
||||
});
|
||||
|
||||
monaco.editor.registerCommand('esql.timepicker.choose', (...args) => {
|
||||
openTimePickerPopover();
|
||||
});
|
||||
|
||||
const styles = textBasedLanguageEditorStyles(
|
||||
euiTheme,
|
||||
isCompactFocused,
|
||||
|
@ -933,6 +961,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
setTimeout(() => {
|
||||
editor.focus();
|
||||
}, 100);
|
||||
if (isDatePickerOpen) {
|
||||
setPopoverPosition({});
|
||||
}
|
||||
});
|
||||
|
||||
editor.onDidFocusEditorText(() => {
|
||||
|
@ -1107,6 +1138,65 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
editorIsInline={editorIsInline}
|
||||
/>
|
||||
)}
|
||||
|
||||
{createPortal(
|
||||
Object.keys(popoverPosition).length !== 0 && popoverPosition.constructor === Object && (
|
||||
<div
|
||||
tabIndex={0}
|
||||
style={{
|
||||
...popoverPosition,
|
||||
backgroundColor: euiTheme.colors.emptyShade,
|
||||
borderRadius: euiTheme.border.radius.small,
|
||||
position: 'absolute',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
ref={popoverRef}
|
||||
data-test-subj="TextBasedLangEditor-timepicker-popover"
|
||||
>
|
||||
<EuiDatePicker
|
||||
selected={timePickerDate}
|
||||
autoFocus
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
setTimePickerDate(date);
|
||||
}
|
||||
}}
|
||||
onSelect={(date, event) => {
|
||||
if (date && event) {
|
||||
const currentCursorPosition = editor1.current?.getPosition();
|
||||
const lineContent = editorModel.current?.getLineContent(
|
||||
currentCursorPosition?.lineNumber ?? 0
|
||||
);
|
||||
const contentAfterCursor = lineContent?.substring(
|
||||
(currentCursorPosition?.column ?? 0) - 1,
|
||||
lineContent.length + 1
|
||||
);
|
||||
|
||||
const addition = `"${date.toISOString()}"${contentAfterCursor}`;
|
||||
editor1.current?.executeEdits('time', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: currentCursorPosition?.lineNumber ?? 0,
|
||||
startColumn: currentCursorPosition?.column ?? 0,
|
||||
endLineNumber: currentCursorPosition?.lineNumber ?? 0,
|
||||
endColumn: (currentCursorPosition?.column ?? 0) + addition.length + 1,
|
||||
},
|
||||
text: addition,
|
||||
forceMoveMarkers: true,
|
||||
},
|
||||
]);
|
||||
setPopoverPosition({});
|
||||
isDatePickerOpen = false;
|
||||
}
|
||||
}}
|
||||
inline
|
||||
showTimeSelect={true}
|
||||
shadow={true}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue