From 4dd7ba0b2115473a5421cf3d2e05527c43c9f674 Mon Sep 17 00:00:00 2001 From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:34:00 +0500 Subject: [PATCH] [Console] Fix condition auto-completion for templates (#126881) (#130972) * Fix condition autocompletion for templates * Added block level matching logic * Fix lint * Fixed types * Resolved comments * Added a custom type guard * Minor refactor * Add type to type imports * Add functional tests and comments Co-authored-by: Muhammad Ibragimov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 64479235912eecd8364a7dfd4231931839c61cf9) --- .../public/lib/autocomplete/autocomplete.ts | 78 ++++++++++++++++++- .../console/public/lib/autocomplete/types.ts | 9 +++ test/functional/apps/console/_autocomplete.ts | 44 +++++++++++ test/functional/page_objects/console_page.ts | 5 +- 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 63d43a849a681..a17dbde450973 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -11,22 +11,22 @@ import { i18n } from '@kbn/i18n'; // TODO: All of these imports need to be moved to the core editor so that it can inject components from there. import { - getTopLevelUrlCompleteComponents, getEndpointBodyCompleteComponents, getGlobalAutocompleteComponents, + getTopLevelUrlCompleteComponents, getUnmatchedEndpointComponents, // @ts-ignore } from '../kb/kb'; import { createTokenIterator } from '../../application/factories'; -import { Position, Token, Range, CoreEditor } from '../../types'; +import type { CoreEditor, Position, Range, Token } from '../../types'; import type RowParser from '../row_parser'; import * as utils from '../utils'; // @ts-ignore import { populateContext } from './engine'; -import { AutoCompleteContext, ResultTerm } from './types'; +import type { AutoCompleteContext, DataAutoCompleteRulesOneOf, ResultTerm } from './types'; // @ts-ignore import { URL_PATH_END_MARKER } from './components/index'; @@ -349,14 +349,84 @@ export default function ({ }); } + /** + * Get a different set of templates based on the value configured in the request. + * For example, when creating a snapshot repository of different types (`fs`, `url` etc), + * different properties are inserted in the textarea based on the type. + * E.g. https://github.com/elastic/kibana/blob/main/src/plugins/console/server/lib/spec_definitions/json/overrides/snapshot.create_repository.json + */ + function getConditionalTemplate( + name: string, + autocompleteRules: Record | null | undefined + ) { + const obj = autocompleteRules && autocompleteRules[name]; + + if (obj) { + const currentLineNumber = editor.getCurrentPosition().lineNumber; + + if (hasOneOfIn(obj)) { + // Get the line number of value that should provide different templates based on that + const startLine = getStartLineNumber(currentLineNumber, obj.__one_of); + // Join line values from start to current line + const lines = editor.getLines(startLine, currentLineNumber).join('\n'); + // Get the correct template by comparing the autocomplete rules against the lines + const prop = getProperty(lines, obj.__one_of); + if (prop && prop.__template) { + return prop.__template; + } + } + } + } + + /** + * Check if object has a property of '__one_of' + */ + function hasOneOfIn(value: unknown): value is { __one_of: DataAutoCompleteRulesOneOf[] } { + return typeof value === 'object' && value !== null && '__one_of' in value; + } + + /** + * Get the start line of value that matches the autocomplete rules condition + */ + function getStartLineNumber(currentLine: number, rules: DataAutoCompleteRulesOneOf[]): number { + if (currentLine === 1) { + return currentLine; + } + const value = editor.getLineValue(currentLine); + const prop = getProperty(value, rules); + if (prop) { + return currentLine; + } + return getStartLineNumber(currentLine - 1, rules); + } + + /** + * Get the matching property based on the given condition + */ + function getProperty(condition: string, rules: DataAutoCompleteRulesOneOf[]) { + return rules.find((rule) => { + if (rule.__condition && rule.__condition.lines_regex) { + return new RegExp(rule.__condition.lines_regex, 'm').test(condition); + } + return false; + }); + } + function applyTerm(term: { value?: string; context?: AutoCompleteContext; - template?: { __raw: boolean; value: string }; + template?: { __raw?: boolean; value?: string; [key: string]: unknown }; insertValue?: string; }) { const context = term.context!; + if (context?.endpoint && term.value) { + const { data_autocomplete_rules: autocompleteRules } = context.endpoint; + const template = getConditionalTemplate(term.value, autocompleteRules); + if (template) { + term.template = template; + } + } // make sure we get up to date replacement info. addReplacementInfoToContext(context, editor.getCurrentPosition(), term.insertValue); diff --git a/src/plugins/console/public/lib/autocomplete/types.ts b/src/plugins/console/public/lib/autocomplete/types.ts index 33c543f43be9e..15d32e6426a6c 100644 --- a/src/plugins/console/public/lib/autocomplete/types.ts +++ b/src/plugins/console/public/lib/autocomplete/types.ts @@ -15,6 +15,14 @@ export interface ResultTerm { value?: string; } +export interface DataAutoCompleteRulesOneOf { + __condition?: { + lines_regex: string; + }; + __template: Record; + [key: string]: unknown; +} + export interface AutoCompleteContext { autoCompleteSet?: null | ResultTerm[]; endpoint?: null | { @@ -24,6 +32,7 @@ export interface AutoCompleteContext { bodyAutocompleteRootComponents: unknown; id?: string; documentation?: string; + data_autocomplete_rules?: Record | null; }; urlPath?: null | unknown; urlParamsTokenPath?: Array> | null; diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 4b424b2a79c66..57c59793f69f6 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -7,10 +7,12 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); + const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'console']); describe('console autocomplete feature', function describeIndexTests() { @@ -62,5 +64,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(lastChar).to.be.eql(','); }); }); + + describe('with conditional templates', async () => { + const CONDITIONAL_TEMPLATES = [ + { + type: 'fs', + template: `"location": "path"`, + }, + { + type: 'url', + template: `"url": ""`, + }, + { type: 's3', template: `"bucket": ""` }, + { + type: 'azure', + template: `"path": ""`, + }, + ]; + + beforeEach(async () => { + await PageObjects.console.clearTextArea(); + await PageObjects.console.enterRequest('\n POST _snapshot/test_repo'); + }); + + await asyncForEach(CONDITIONAL_TEMPLATES, async ({ type, template }) => { + it('should insert different templates depending on the value of type', async () => { + await PageObjects.console.enterText(`{\n\t"type": "${type}"`); + await PageObjects.console.pressEnter(); + // Prompt autocomplete for 'settings' + await PageObjects.console.promptAutocomplete('s'); + + await retry.waitFor('autocomplete to be visible', () => + PageObjects.console.isAutocompleteVisible() + ); + await PageObjects.console.pressEnter(); + await retry.try(async () => { + const request = await PageObjects.console.getRequest(); + log.debug(request); + expect(request).to.contain(`${template}`); + }); + }); + }); + }); }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 32c859cc1aed9..281c49a789acf 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -83,10 +83,11 @@ export class ConsolePageObject extends FtrService { } } - public async promptAutocomplete() { + // Prompt autocomplete window and provide a initial letter of properties to narrow down the results. E.g. 'b' = 'bool' + public async promptAutocomplete(letter = 'b') { const textArea = await this.testSubjects.find('console-textarea'); await textArea.clickMouseButton(); - await textArea.type('b'); + await textArea.type(letter); await this.retry.waitFor('autocomplete to be visible', () => this.isAutocompleteVisible()); }