Integrate painless autocomplete in runtime fields editor (#84943)

This commit is contained in:
Alison Goryachev 2020-12-07 12:55:53 -05:00 committed by GitHub
parent d19b558c88
commit 079a7e82ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 82 additions and 24 deletions

View file

@ -22,7 +22,7 @@ import './register_globals';
export { monaco } from './monaco_imports';
export { XJsonLang } from './xjson';
export { PainlessLang, PainlessContext } from './painless';
export { PainlessLang, PainlessContext, PainlessAutocompleteField } from './painless';
/* eslint-disable-next-line @kbn/eslint/module_migration */
import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api';

View file

@ -23,4 +23,4 @@ import { getSuggestionProvider } from './language';
export const PainlessLang = { ID, getSuggestionProvider, lexerRules };
export { PainlessContext } from './types';
export { PainlessContext, PainlessAutocompleteField } from './types';

View file

@ -21,7 +21,7 @@ import { monaco } from '../monaco_imports';
import { WorkerProxyService, EditorStateService } from './services';
import { ID } from './constants';
import { PainlessContext, Field } from './types';
import { PainlessContext, PainlessAutocompleteField } from './types';
import { PainlessWorker } from './worker';
import { PainlessCompletionAdapter } from './completion_adapter';
@ -38,7 +38,10 @@ monaco.languages.onLanguage(ID, async () => {
workerProxyService.setup();
});
export const getSuggestionProvider = (context: PainlessContext, fields?: Field[]) => {
export const getSuggestionProvider = (
context: PainlessContext,
fields?: PainlessAutocompleteField[]
) => {
editorStateService.setup(context, fields);
return new PainlessCompletionAdapter(worker, editorStateService);

View file

@ -17,16 +17,16 @@
* under the License.
*/
import { PainlessContext, Field } from '../types';
import { PainlessContext, PainlessAutocompleteField } from '../types';
export interface EditorState {
context: PainlessContext;
fields?: Field[];
fields?: PainlessAutocompleteField[];
}
export class EditorStateService {
context: PainlessContext = 'painless_test';
fields: Field[] = [];
fields: PainlessAutocompleteField[] = [];
public getState(): EditorState {
return {
@ -35,7 +35,7 @@ export class EditorStateService {
};
}
public setup(context: PainlessContext, fields?: Field[]) {
public setup(context: PainlessContext, fields?: PainlessAutocompleteField[]) {
this.context = context;
if (fields) {

View file

@ -51,7 +51,7 @@ export interface PainlessCompletionResult {
suggestions: PainlessCompletionItem[];
}
export interface Field {
export interface PainlessAutocompleteField {
name: string;
type: string;
}

View file

@ -23,7 +23,7 @@ import {
PainlessCompletionResult,
PainlessCompletionItem,
PainlessContext,
Field,
PainlessAutocompleteField,
} from '../../types';
import {
@ -124,7 +124,9 @@ export const getClassMemberSuggestions = (
};
};
export const getFieldSuggestions = (fields: Field[]): PainlessCompletionResult => {
export const getFieldSuggestions = (
fields: PainlessAutocompleteField[]
): PainlessCompletionResult => {
const suggestions: PainlessCompletionItem[] = fields.map(({ name }) => {
return {
label: name,
@ -168,7 +170,7 @@ export const getConstructorSuggestions = (suggestions: Suggestion[]): PainlessCo
export const getAutocompleteSuggestions = (
painlessContext: PainlessContext,
words: string[],
fields?: Field[]
fields?: PainlessAutocompleteField[]
): PainlessCompletionResult => {
const suggestions = mapContextToData[painlessContext].suggestions;
// What the user is currently typing

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { PainlessCompletionResult, PainlessContext, Field } from '../types';
import { PainlessCompletionResult, PainlessContext, PainlessAutocompleteField } from '../types';
import { getAutocompleteSuggestions } from './lib';
@ -25,7 +25,7 @@ export class PainlessWorker {
public provideAutocompleteSuggestions(
currentLineChars: string,
context: PainlessContext,
fields?: Field[]
fields?: PainlessAutocompleteField[]
): PainlessCompletionResult {
// Array of the active line words, e.g., [boolean, isTrue, =, true]
const words = currentLineChars.replace('\t', '').split(' ');

View file

@ -78,7 +78,10 @@ export const RuntimeFieldsList = () => {
docLinks: docLinks!,
ctx: {
namesNotAllowed: Object.values(runtimeFields).map((field) => field.source.name),
existingConcreteFields: Object.values(fields.byId).map((field) => field.source.name),
existingConcreteFields: Object.values(fields.byId).map((field) => ({
name: field.source.name,
type: field.source.type,
})),
},
},
flyoutProps: {

View file

@ -90,8 +90,12 @@ interface Context {
* An array of existing concrete fields. If the user gives a name to the runtime
* field that matches one of the concrete fields, a callout will be displayed
* to indicate that this runtime field will shadow the concrete field.
* This array is also used to provide the list of field autocomplete suggestions to the code editor
*/
existingConcreteFields?: string[];
existingConcreteFields?: Array<{
name: string;
type: string;
}>;
}
```

View file

@ -78,7 +78,7 @@ describe('Runtime field editor', () => {
});
test('should accept a list of existing concrete fields and display a callout when shadowing one of the fields', async () => {
const existingConcreteFields = ['myConcreteField'];
const existingConcreteFields = [{ name: 'myConcreteField', type: 'keyword' }];
testBed = setup({ onChange, docLinks, ctx: { existingConcreteFields } });
@ -87,7 +87,7 @@ describe('Runtime field editor', () => {
expect(exists('shadowingFieldCallout')).toBe(false);
await act(async () => {
form.setInputValue('nameField.input', existingConcreteFields[0]);
form.setInputValue('nameField.input', existingConcreteFields[0].name);
});
component.update();

View file

@ -3,9 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { PainlessLang } from '@kbn/monaco';
import { PainlessLang, PainlessContext } from '@kbn/monaco';
import {
EuiFlexGroup,
EuiFlexItem,
@ -28,7 +28,7 @@ import {
ValidationFunc,
FieldConfig,
} from '../../shared_imports';
import { RuntimeField } from '../../types';
import { RuntimeField, RuntimeType } from '../../types';
import { RUNTIME_FIELD_OPTIONS } from '../../constants';
import { schema } from './schema';
@ -38,6 +38,11 @@ export interface FormState {
submit: FormHook<RuntimeField>['submit'];
}
interface Field {
name: string;
type: string;
}
export interface Props {
links: {
runtimePainless: string;
@ -54,8 +59,9 @@ export interface Props {
* An array of existing concrete fields. If the user gives a name to the runtime
* field that matches one of the concrete fields, a callout will be displayed
* to indicate that this runtime field will shadow the concrete field.
* It is also used to provide the list of field autocomplete suggestions to the code editor.
*/
existingConcreteFields?: string[];
existingConcreteFields?: Field[];
};
}
@ -105,18 +111,51 @@ const getNameFieldConfig = (
};
};
const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => {
switch (runtimeType) {
case 'keyword':
return 'string_script_field_script_field';
case 'long':
return 'long_script_field_script_field';
case 'double':
return 'double_script_field_script_field';
case 'date':
return 'date_script_field';
case 'ip':
return 'ip_script_field_script_field';
case 'boolean':
return 'boolean_script_field_script_field';
default:
return 'string_script_field_script_field';
}
};
const RuntimeFieldFormComp = ({
defaultValue,
onChange,
links,
ctx: { namesNotAllowed, existingConcreteFields = [] } = {},
}: Props) => {
const typeFieldConfig = schema.type as FieldConfig<RuntimeType, RuntimeField>;
const [painlessContext, setPainlessContext] = useState<PainlessContext>(
mapReturnTypeToPainlessContext(typeFieldConfig!.defaultValue!)
);
const { form } = useForm<RuntimeField>({ defaultValue, schema });
const { submit, isValid: isFormValid, isSubmitted } = form;
const [{ name }] = useFormData<RuntimeField>({ form, watch: 'name' });
const nameFieldConfig = getNameFieldConfig(namesNotAllowed, defaultValue);
const onTypeChange = useCallback((newType: Array<EuiComboBoxOptionOption<RuntimeType>>) => {
setPainlessContext(mapReturnTypeToPainlessContext(newType[0]!.value!));
}, []);
const suggestionProvider = PainlessLang.getSuggestionProvider(
painlessContext,
existingConcreteFields
);
useEffect(() => {
if (onChange) {
onChange({ isValid: isFormValid, isSubmitted, submit });
@ -145,7 +184,10 @@ const RuntimeFieldFormComp = ({
{/* Return type */}
<EuiFlexItem>
<UseField<EuiComboBoxOptionOption[]> path="type">
<UseField<Array<EuiComboBoxOptionOption<RuntimeType>>>
path="type"
onChange={onTypeChange}
>
{({ label, value, setValue }) => {
if (value === undefined) {
return null;
@ -185,7 +227,7 @@ const RuntimeFieldFormComp = ({
</EuiFlexItem>
</EuiFlexGroup>
{existingConcreteFields.includes(name) && (
{existingConcreteFields.find((field) => field.name === name) && (
<>
<EuiSpacer />
<EuiCallOut
@ -237,6 +279,7 @@ const RuntimeFieldFormComp = ({
>
<CodeEditor
languageId={PainlessLang.ID}
suggestionProvider={suggestionProvider}
width="100%"
height="300px"
value={value}
@ -250,6 +293,9 @@ const RuntimeFieldFormComp = ({
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
suggest: {
snippetsPreventQuickSuggestions: false,
},
}}
data-test-subj="scriptField"
aria-label={i18n.translate('xpack.runtimeFields.form.scriptEditorAriaLabel', {