From 5a397a1cb4956f769c5bfe86f62d2ba5a608acb1 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 7 Dec 2020 12:55:53 -0500 Subject: [PATCH] Integrate painless autocomplete in runtime fields editor (#84943) --- packages/kbn-monaco/src/index.ts | 2 +- packages/kbn-monaco/src/painless/index.ts | 2 +- packages/kbn-monaco/src/painless/language.ts | 7 ++- .../src/painless/services/editor_state.ts | 8 +-- packages/kbn-monaco/src/painless/types.ts | 2 +- .../src/painless/worker/lib/autocomplete.ts | 8 ++- .../src/painless/worker/painless_worker.ts | 4 +- .../runtime_fields/runtime_fields_list.tsx | 5 +- x-pack/plugins/runtime_fields/README.md | 6 +- .../runtime_field_editor.test.tsx | 4 +- .../runtime_field_form/runtime_field_form.tsx | 58 +++++++++++++++++-- 11 files changed, 82 insertions(+), 24 deletions(-) diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts index dcfcb5fbfc63f..41600d96ff7c9 100644 --- a/packages/kbn-monaco/src/index.ts +++ b/packages/kbn-monaco/src/index.ts @@ -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'; diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 3c81f265f9b0d..4693fa2418b66 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -23,4 +23,4 @@ import { getSuggestionProvider } from './language'; export const PainlessLang = { ID, getSuggestionProvider, lexerRules }; -export { PainlessContext } from './types'; +export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index f64094dbb482e..b38dac2c7baf7 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -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); diff --git a/packages/kbn-monaco/src/painless/services/editor_state.ts b/packages/kbn-monaco/src/painless/services/editor_state.ts index b54744152e34d..3003f266dca62 100644 --- a/packages/kbn-monaco/src/painless/services/editor_state.ts +++ b/packages/kbn-monaco/src/painless/services/editor_state.ts @@ -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) { diff --git a/packages/kbn-monaco/src/painless/types.ts b/packages/kbn-monaco/src/painless/types.ts index 8afc3dc7ddd88..a56ca4f9b695a 100644 --- a/packages/kbn-monaco/src/painless/types.ts +++ b/packages/kbn-monaco/src/painless/types.ts @@ -51,7 +51,7 @@ export interface PainlessCompletionResult { suggestions: PainlessCompletionItem[]; } -export interface Field { +export interface PainlessAutocompleteField { name: string; type: string; } diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts index 5536da828be42..e8e795e99b259 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts @@ -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 diff --git a/packages/kbn-monaco/src/painless/worker/painless_worker.ts b/packages/kbn-monaco/src/painless/worker/painless_worker.ts index 357d81354ac43..9c39659519163 100644 --- a/packages/kbn-monaco/src/painless/worker/painless_worker.ts +++ b/packages/kbn-monaco/src/painless/worker/painless_worker.ts @@ -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(' '); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx index dce5ad1657d38..4033c0f2fe456 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx @@ -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: { diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md index e682d77f7a884..eb7b31e6e1154 100644 --- a/x-pack/plugins/runtime_fields/README.md +++ b/x-pack/plugins/runtime_fields/README.md @@ -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; + }>; } ``` diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx index a8f90810a1212..89f795633e9d1 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx @@ -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(); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index 2ed6df537a6fe..f64bdaacd7ff2 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -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['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; + + const [painlessContext, setPainlessContext] = useState( + mapReturnTypeToPainlessContext(typeFieldConfig!.defaultValue!) + ); const { form } = useForm({ defaultValue, schema }); const { submit, isValid: isFormValid, isSubmitted } = form; const [{ name }] = useFormData({ form, watch: 'name' }); const nameFieldConfig = getNameFieldConfig(namesNotAllowed, defaultValue); + const onTypeChange = useCallback((newType: Array>) => { + 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 */} - path="type"> + >> + path="type" + onChange={onTypeChange} + > {({ label, value, setValue }) => { if (value === undefined) { return null; @@ -185,7 +227,7 @@ const RuntimeFieldFormComp = ({ - {existingConcreteFields.includes(name) && ( + {existingConcreteFields.find((field) => field.name === name) && ( <>