Skip to content
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

Integrate painless autocomplete in runtime fields editor #84943

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
2 changes: 1 addition & 1 deletion packages/kbn-monaco/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-monaco/src/painless/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ import { getSuggestionProvider } from './language';

export const PainlessLang = { ID, getSuggestionProvider, lexerRules };

export { PainlessContext } from './types';
export { PainlessContext, PainlessAutocompleteField } from './types';
7 changes: 5 additions & 2 deletions packages/kbn-monaco/src/painless/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions packages/kbn-monaco/src/painless/services/editor_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -35,7 +35,7 @@ export class EditorStateService {
};
}

public setup(context: PainlessContext, fields?: Field[]) {
public setup(context: PainlessContext, fields?: PainlessAutocompleteField[]) {
this.context = context;

if (fields) {
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-monaco/src/painless/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export interface PainlessCompletionResult {
suggestions: PainlessCompletionItem[];
}

export interface Field {
export interface PainlessAutocompleteField {
name: string;
type: string;
}
8 changes: 5 additions & 3 deletions packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
PainlessCompletionResult,
PainlessCompletionItem,
PainlessContext,
Field,
PainlessAutocompleteField,
} from '../../types';

import {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/kbn-monaco/src/painless/worker/painless_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
* under the License.
*/

import { PainlessCompletionResult, PainlessContext, Field } from '../types';
import { PainlessCompletionResult, PainlessContext, PainlessAutocompleteField } from '../types';

import { getAutocompleteSuggestions } from './lib';

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(' ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/runtime_fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}>;
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });

Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand All @@ -38,6 +38,11 @@ export interface FormState {
submit: FormHook<RuntimeField>['submit'];
}

interface Field {
name: string;
type: string;
}

export interface Props {
links: {
runtimePainless: string;
Expand All @@ -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[];
};
}

Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -185,7 +227,7 @@ const RuntimeFieldFormComp = ({
</EuiFlexItem>
</EuiFlexGroup>

{existingConcreteFields.includes(name) && (
{existingConcreteFields.find((field) => field.name === name) && (
<>
<EuiSpacer />
<EuiCallOut
Expand Down Expand Up @@ -237,6 +279,7 @@ const RuntimeFieldFormComp = ({
>
<CodeEditor
languageId={PainlessLang.ID}
suggestionProvider={suggestionProvider}
width="100%"
height="300px"
value={value}
Expand All @@ -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', {
Expand Down