diff --git a/package.json b/package.json index 473e7613b321c9..ced46d2578656b 100644 --- a/package.json +++ b/package.json @@ -1097,7 +1097,7 @@ "pretty-ms": "6.0.0", "prop-types": "^15.8.1", "proxy-from-env": "1.0.0", - "puppeteer": "22.3.0", + "puppeteer": "22.8.1", "query-string": "^6.13.2", "rbush": "^3.0.1", "re-resizable": "^6.9.9", diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index 52027ffe908396..c2669c19c32548 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -109,6 +109,7 @@ export { export { apiPublishesPanelDescription, apiPublishesWritablePanelDescription, + getPanelDescription, type PublishesPanelDescription, type PublishesWritablePanelDescription, } from './interfaces/titles/publishes_panel_description'; diff --git a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.test.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.test.ts new file mode 100644 index 00000000000000..2fde4feed021e9 --- /dev/null +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { BehaviorSubject } from 'rxjs'; +import { getPanelDescription } from './publishes_panel_description'; + +describe('getPanelDescription', () => { + test('should return default description when description is undefined', () => { + const api = { + panelDescription: new BehaviorSubject(undefined), + defaultPanelDescription: new BehaviorSubject('default description'), + }; + expect(getPanelDescription(api)).toBe('default description'); + }); + + test('should return empty description when description is empty string', () => { + const api = { + panelDescription: new BehaviorSubject(''), + defaultPanelDescription: new BehaviorSubject('default description'), + }; + expect(getPanelDescription(api)).toBe(''); + }); + + test('should return description when description is provided', () => { + const api = { + panelDescription: new BehaviorSubject('custom description'), + defaultPanelDescription: new BehaviorSubject('default description'), + }; + expect(getPanelDescription(api)).toBe('custom description'); + }); +}); diff --git a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts index 7cf0f06a8a9fdf..f7275ea364cca1 100644 --- a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_description.ts @@ -13,6 +13,10 @@ export interface PublishesPanelDescription { defaultPanelDescription?: PublishingSubject; } +export function getPanelDescription(api: Partial): string | undefined { + return api.panelDescription?.value ?? api.defaultPanelDescription?.value; +} + export type PublishesWritablePanelDescription = PublishesPanelDescription & { setPanelDescription: (newTitle: string | undefined) => void; }; diff --git a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts index df8d1a0d9a9163..e0724af40d5be3 100644 --- a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.test.ts @@ -18,12 +18,12 @@ describe('getPanelTitle', () => { expect(getPanelTitle(api)).toBe('default title'); }); - test('should return default title when title is empty string', () => { + test('should return empty title when title is empty string', () => { const api = { panelTitle: new BehaviorSubject(''), defaultPanelTitle: new BehaviorSubject('default title'), }; - expect(getPanelTitle(api)).toBe('default title'); + expect(getPanelTitle(api)).toBe(''); }); test('should return title when title is provided', () => { diff --git a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts index 56cc8a55268262..c91704da80f6a0 100644 --- a/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts +++ b/packages/presentation/presentation_publishing/interfaces/titles/publishes_panel_title.ts @@ -15,7 +15,7 @@ export interface PublishesPanelTitle { } export function getPanelTitle(api: Partial): string | undefined { - return api.panelTitle?.value || api.defaultPanelTitle?.value; + return api.panelTitle?.value ?? api.defaultPanelTitle?.value; } export type PublishesWritablePanelTitle = PublishesPanelTitle & { diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/index.ts b/src/plugins/console/public/application/containers/editor/monaco/hooks/index.ts new file mode 100644 index 00000000000000..e0a56ebf655d01 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/hooks/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useResizeCheckerUtils } from './use_resize_checker_utils'; +export { useSetInitialValue } from './use_set_initial_value'; +export { useSetupAutocompletePolling } from './use_setup_autocomplete_polling'; +export { useSetupAutosave } from './use_setup_autosave'; diff --git a/src/plugins/console/public/application/containers/editor/monaco/use_resize_checker_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_resize_checker_utils.ts similarity index 100% rename from src/plugins/console/public/application/containers/editor/monaco/use_resize_checker_utils.ts rename to src/plugins/console/public/application/containers/editor/monaco/hooks/use_resize_checker_utils.ts diff --git a/src/plugins/console/public/application/containers/editor/monaco/use_set_initial_value.ts b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts similarity index 98% rename from src/plugins/console/public/application/containers/editor/monaco/use_set_initial_value.ts rename to src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts index 9d963bb2cf00f5..ccd45a3a98106d 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/use_set_initial_value.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts @@ -12,7 +12,7 @@ import { IToasts } from '@kbn/core-notifications-browser'; import { decompressFromEncodedURIComponent } from 'lz-string'; import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; -import { DEFAULT_INPUT_VALUE } from '../../../../../common/constants'; +import { DEFAULT_INPUT_VALUE } from '../../../../../../common/constants'; interface QueryParams { load_from: string; diff --git a/src/plugins/console/public/application/containers/editor/monaco/use_setup_autocomplete_polling.ts b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts similarity index 94% rename from src/plugins/console/public/application/containers/editor/monaco/use_setup_autocomplete_polling.ts rename to src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts index c1bf0640ba8dbb..9ec3f17ec6b3f2 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/use_setup_autocomplete_polling.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts @@ -7,7 +7,7 @@ */ import { useEffect } from 'react'; -import { AutocompleteInfo, Settings } from '../../../../services'; +import { AutocompleteInfo, Settings } from '../../../../../services'; interface SetupAutocompletePollingParams { /** The Console autocomplete service. */ diff --git a/src/plugins/console/public/application/containers/editor/monaco/use_setup_autosave.ts b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts similarity index 96% rename from src/plugins/console/public/application/containers/editor/monaco/use_setup_autosave.ts rename to src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts index 323279045fa6ba..0076711b97163f 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/use_setup_autosave.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts @@ -7,7 +7,7 @@ */ import { useEffect, useRef } from 'react'; -import { useSaveCurrentTextObject } from '../../../hooks'; +import { useSaveCurrentTextObject } from '../../../../hooks'; import { readLoadFromParam } from './use_set_initial_value'; interface SetupAutosaveParams { diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 4bbf13c006353c..86a567fe70e36d 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -18,12 +18,14 @@ import { useEditorReadContext, useRequestActionContext, } from '../../../contexts'; -import { useSetInitialValue } from './use_set_initial_value'; +import { + useSetInitialValue, + useSetupAutocompletePolling, + useSetupAutosave, + useResizeCheckerUtils, +} from './hooks'; import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider'; -import { useSetupAutocompletePolling } from './use_setup_autocomplete_polling'; -import { useSetupAutosave } from './use_setup_autosave'; import { getSuggestionProvider } from './monaco_editor_suggestion_provider'; -import { useResizeCheckerUtils } from './use_resize_checker_utils'; export interface EditorProps { initialTextValue: string; diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts index 486074f9ddf9d0..edb4ed93cf71ee 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -8,24 +8,22 @@ import { CSSProperties, Dispatch } from 'react'; import { debounce } from 'lodash'; -import { - ConsoleParsedRequestsProvider, - getParsedRequestsProvider, - monaco, - ParsedRequest, -} from '@kbn/monaco'; +import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, monaco } from '@kbn/monaco'; import { IToasts } from '@kbn/core-notifications-browser'; import { i18n } from '@kbn/i18n'; import type { HttpSetup } from '@kbn/core-http-browser'; import { DEFAULT_VARIABLES } from '../../../../../common/constants'; import { getStorage, StorageKeys } from '../../../../services'; -import { sendRequest } from '../../../hooks/use_send_current_request/send_request'; +import { sendRequest } from '../../../hooks'; import { MetricsTracker } from '../../../../types'; import { Actions } from '../../../stores/request'; + import { + AutocompleteType, containsUrlParams, + getBodyCompletionItems, getCurlRequest, - getDocumentationLink, + getDocumentationLinkFromAutocomplete, getLineTokens, getMethodCompletionItems, getRequestEndLineNumber, @@ -33,29 +31,13 @@ import { getUrlParamsCompletionItems, getUrlPathCompletionItems, replaceRequestVariables, + SELECTED_REQUESTS_CLASSNAME, stringifyRequest, trackSentRequests, getAutoIndentedRequests, } from './utils'; -const selectedRequestsClass = 'console__monaco_editor__selectedRequests'; - -export interface EditorRequest { - method: string; - url: string; - data: string[]; -} - -export interface AdjustedParsedRequest extends ParsedRequest { - startLineNumber: number; - endLineNumber: number; -} -enum AutocompleteType { - PATH = 'path', - URL_PARAMS = 'url_params', - METHOD = 'method', - BODY = 'body', -} +import type { AdjustedParsedRequest } from './types'; export class MonacoEditorActionsProvider { private parsedRequestsProvider: ConsoleParsedRequestsProvider; @@ -126,7 +108,7 @@ export class MonacoEditorActionsProvider { range: selectedRange, options: { isWholeLine: true, - className: selectedRequestsClass, + className: SELECTED_REQUESTS_CLASSNAME, }, }, ]); @@ -256,7 +238,7 @@ export class MonacoEditorActionsProvider { } const request = requests[0]; - return getDocumentationLink(request, docLinkVersion); + return getDocumentationLinkFromAutocomplete(request, docLinkVersion); } private async getAutocompleteType( @@ -303,7 +285,11 @@ export class MonacoEditorActionsProvider { return AutocompleteType.BODY; } - private async getSuggestions(model: monaco.editor.ITextModel, position: monaco.Position) { + private async getSuggestions( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.CompletionContext + ) { // determine autocomplete type const autocompleteType = await this.getAutocompleteType(model, position); if (!autocompleteType) { @@ -329,6 +315,23 @@ export class MonacoEditorActionsProvider { }; } + if (autocompleteType === AutocompleteType.BODY) { + // suggestions only when triggered by " or keyboard + if (context.triggerCharacter && context.triggerCharacter !== '"') { + return { suggestions: [] }; + } + const requests = await this.getRequestsBetweenLines( + model, + position.lineNumber, + position.lineNumber + ); + const requestStartLineNumber = requests[0].startLineNumber; + const suggestions = getBodyCompletionItems(model, position, requestStartLineNumber); + return { + suggestions, + }; + } + return { suggestions: [], }; @@ -339,7 +342,7 @@ export class MonacoEditorActionsProvider { context: monaco.languages.CompletionContext, token: monaco.CancellationToken ): monaco.languages.ProviderResult { - return this.getSuggestions(model, position); + return this.getSuggestions(model, position, context); } /* diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx index f11ea7479d461e..4480538544d9bc 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx @@ -22,7 +22,7 @@ import { safeExpandLiteralStrings, languageForContentType, } from '../utilities'; -import { useResizeCheckerUtils } from './use_resize_checker_utils'; +import { useResizeCheckerUtils } from './hooks'; export const MonacoEditorOutput: FunctionComponent = () => { const { settings: readOnlySettings } = useEditorReadContext(); diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_suggestion_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_suggestion_provider.ts index 7dab08f855b300..95b9e98faaff7a 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_suggestion_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_suggestion_provider.ts @@ -15,7 +15,7 @@ export const getSuggestionProvider = ( ): monaco.languages.CompletionItemProvider => { return { // force suggestions when these characters are used - triggerCharacters: ['/', '.', '_', ',', '?', '=', '&'], + triggerCharacters: ['/', '.', '_', ',', '?', '=', '&', '"'], provideCompletionItems: (...args) => { if (actionsProvider.current) { return actionsProvider.current?.provideCompletionItems(...args); diff --git a/src/plugins/console/public/application/containers/editor/monaco/types.ts b/src/plugins/console/public/application/containers/editor/monaco/types.ts new file mode 100644 index 00000000000000..07e823aed8e089 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ParsedRequest } from '@kbn/monaco'; + +export interface EditorRequest { + method: string; + url: string; + data: string[]; +} + +export interface AdjustedParsedRequest extends ParsedRequest { + startLineNumber: number; + endLineNumber: number; +} diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils.ts deleted file mode 100644 index c08f5c80e03def..00000000000000 --- a/src/plugins/console/public/application/containers/editor/monaco/utils.ts +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { monaco, ParsedRequest } from '@kbn/monaco'; -import { i18n } from '@kbn/i18n'; -import { getTopLevelUrlCompleteComponents } from '../../../../lib/kb'; -import { AutoCompleteContext } from '../../../../lib/autocomplete/types'; -import { constructUrl } from '../../../../lib/es'; -import type { DevToolsVariable } from '../../../components'; -import { EditorRequest, AdjustedParsedRequest } from './monaco_editor_actions_provider'; -import { MetricsTracker } from '../../../../types'; -import { populateContext } from '../../../../lib/autocomplete/engine'; - -/* - * Helper constants - */ -const whitespacesRegex = /\s+/; -const slashRegex = /\//; -const ampersandRegex = /&/; -const equalsSignRegex = /=/; -const variableTemplateRegex = /\${(\w+)}/g; -const endOfUrlToken = '__url_path_end__'; - -/* - * Helper interfaces - */ -export interface ParsedLineTokens { - method: string; - urlPathTokens: string[]; - urlParamsTokens: string[][]; -} - -/* - * i18n for autocomplete labels - */ -const methodDetailLabel = i18n.translate('console.autocompleteSuggestions.methodLabel', { - defaultMessage: 'method', -}); -const endpointDetailLabel = i18n.translate('console.autocompleteSuggestions.endpointLabel', { - defaultMessage: 'endpoint', -}); -const paramDetailLabel = i18n.translate('console.autocompleteSuggestions.paramLabel', { - defaultMessage: 'param', -}); - -/* - * This functions removes any trailing inline comments, for example - * "_search // comment" -> "_search" - * Ideally the parser would do that, but currently they are included in url. - */ -export const removeTrailingWhitespaces = (url: string): string => { - return url.trim().split(whitespacesRegex)[0]; -}; - -export const stringifyRequest = (parsedRequest: ParsedRequest): EditorRequest => { - const url = removeTrailingWhitespaces(parsedRequest.url); - const method = parsedRequest.method.toUpperCase(); - const data = parsedRequest.data?.map((parsedData) => JSON.stringify(parsedData, null, 2)); - return { url, method, data: data ?? [] }; -}; - -const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { - if (variableTemplateRegex.test(text)) { - text = text.replaceAll(variableTemplateRegex, (match, key) => { - const variable = variables.find(({ name }) => name === key); - - return variable?.value ?? match; - }); - } - return text; -}; - -export const replaceRequestVariables = ( - { method, url, data }: EditorRequest, - variables: DevToolsVariable[] -): EditorRequest => { - return { - method, - url: replaceVariables(url, variables), - data: data.map((dataObject) => replaceVariables(dataObject, variables)), - }; -}; - -export const getCurlRequest = ( - { method, url, data }: EditorRequest, - elasticsearchBaseUrl: string -): string => { - const curlUrl = constructUrl(elasticsearchBaseUrl, url); - let curlRequest = `curl -X${method} "${curlUrl}" -H "kbn-xsrf: reporting"`; - if (data.length > 0) { - curlRequest += ` -H "Content-Type: application/json" -d'\n`; - curlRequest += data.join('\n'); - curlRequest += "'"; - } - return curlRequest; -}; - -export const trackSentRequests = ( - requests: EditorRequest[], - trackUiMetric: MetricsTracker -): void => { - requests.map(({ method, url }) => { - const eventName = `${method}_${url}`; - trackUiMetric.count(eventName); - }); -}; - -/* - * This function initializes the autocomplete context for the request - * and returns a documentation link from the endpoint object - * with the branch in the url replaced by the current version "docLinkVersion" - */ -export const getDocumentationLink = (request: EditorRequest, docLinkVersion: string) => { - // get the url parts from the request url - const { urlPathTokens } = parseUrlTokens(request.url); - // remove the last token, if it's empty - if (!urlPathTokens[urlPathTokens.length - 1]) { - urlPathTokens.pop(); - } - // add the end of url token - urlPathTokens.push(endOfUrlToken); - const { endpoint } = populateContextForMethodAndUrl(request.method, urlPathTokens); - if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { - return endpoint.documentation - .replace('/master/', `/${docLinkVersion}/`) - .replace('/current/', `/${docLinkVersion}/`) - .replace('/{branch}/', `/${docLinkVersion}/`); - } - return null; -}; - -const containsComments = (text: string) => { - return text.indexOf('//') >= 0 || text.indexOf('/*') >= 0; -}; - -/** - * This function takes a string containing unformatted Console requests and - * returns a text in which the requests are auto-indented. - * @param requests The list of {@link AdjustedParsedRequest} that are in the selected text in the editor. - * @param selectedText The selected text in the editor. - * @param allText The whole text input in the editor. - */ -export const getAutoIndentedRequests = ( - requests: AdjustedParsedRequest[], - selectedText: string, - allText: string -): string => { - const selectedTextLines = selectedText.split(`\n`); - const allTextLines = allText.split(`\n`); - const formattedTextLines: string[] = []; - - let currentLineIndex = 0; - let currentRequestIndex = 0; - - while (currentLineIndex < selectedTextLines.length) { - const request = requests[currentRequestIndex]; - // Check if the current line is the start of the next request - if ( - request && - selectedTextLines[currentLineIndex] === allTextLines[request.startLineNumber - 1] - ) { - // Start of a request - const requestLines = allTextLines.slice(request.startLineNumber - 1, request.endLineNumber); - - if (requestLines.some((line) => containsComments(line))) { - // If request has comments, add it as it is - without formatting - // TODO: Format requests with comments - formattedTextLines.push(...requestLines); - } else { - // If no comments, add stringified parsed request - const stringifiedRequest = stringifyRequest(request); - const firstLine = stringifiedRequest.method + ' ' + stringifiedRequest.url; - formattedTextLines.push(firstLine); - - if (stringifiedRequest.data && stringifiedRequest.data.length > 0) { - formattedTextLines.push(...stringifiedRequest.data); - } - } - - currentLineIndex = currentLineIndex + requestLines.length; - currentRequestIndex++; - } else { - // Current line is a comment or whitespaces - // Trim white spaces and add it to the formatted text - formattedTextLines.push(selectedTextLines[currentLineIndex].trim()); - currentLineIndex++; - } - } - - return formattedTextLines.join('\n'); -}; - -/* - * This function converts the start offset value of the parsed request to a line number in the model - */ -export const getRequestStartLineNumber = ( - parsedRequest: ParsedRequest, - model: monaco.editor.ITextModel -): number => { - return model.getPositionAt(parsedRequest.startOffset).lineNumber; -}; - -/* - * This function converts the end offset value of the parsed request to a line number in the model. - * If there is no end offset (the parser was not able to parse this request completely), - * then the last non-empty line is returned or the line before the next request. - */ -export const getRequestEndLineNumber = ( - parsedRequest: ParsedRequest, - model: monaco.editor.ITextModel, - index: number, - parsedRequests: ParsedRequest[] -): number => { - let endLineNumber: number; - if (parsedRequest.endOffset) { - // if the parser set an end offset for this request, then find the line number for it - endLineNumber = model.getPositionAt(parsedRequest.endOffset).lineNumber; - } else { - // if no end offset, try to find the line before the next request starts - const nextRequest = parsedRequests.at(index + 1); - if (nextRequest) { - const nextRequestStartLine = model.getPositionAt(nextRequest.startOffset).lineNumber; - endLineNumber = nextRequestStartLine - 1; - } else { - // if there is no next request, take the last line of the model - endLineNumber = model.getLineCount(); - } - } - // if the end line is empty, go up to find the first non-empty line - let lineContent = model.getLineContent(endLineNumber).trim(); - while (!lineContent) { - endLineNumber = endLineNumber - 1; - lineContent = model.getLineContent(endLineNumber).trim(); - } - return endLineNumber; -}; - -/* - * This function returns an array of completion items for the request method - */ -const autocompleteMethods = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH']; -export const getMethodCompletionItems = ( - model: monaco.editor.ITextModel, - position: monaco.Position -): monaco.languages.CompletionItem[] => { - // get the word before suggestions to replace when selecting a suggestion from the list - const wordUntilPosition = model.getWordUntilPosition(position); - return autocompleteMethods.map((method) => ({ - label: method, - insertText: method, - detail: methodDetailLabel, - // only used to configure the icon - kind: monaco.languages.CompletionItemKind.Constant, - range: { - // replace the whole word with the suggestion - startColumn: wordUntilPosition.startColumn, - startLineNumber: position.lineNumber, - endColumn: position.column, - endLineNumber: position.lineNumber, - }, - })); -}; - -/* - * This function splits a string on whitespaces and returns its parts as an array - */ -export const getLineTokens = (lineContent: string): string[] => { - return lineContent.split(whitespacesRegex); -}; - -/* - * This function checks if the url contains url params - */ -const questionMarkRegex = /\?/; -export const containsUrlParams = (lineContent: string): boolean => { - return questionMarkRegex.test(lineContent); -}; - -/* - * This function initializes the autocomplete context for the provided method and url token path. - */ -const populateContextForMethodAndUrl = (method: string, urlTokenPath: string[]) => { - // get autocomplete components for the request method - const components = getTopLevelUrlCompleteComponents(method); - // this object will contain the information later, it needs to be initialized with some data - // similar to the old ace editor context - const context: AutoCompleteContext = { - method, - urlTokenPath, - }; - - // mutate the context object and put the autocomplete information there - populateContext(urlTokenPath, context, undefined, true, components); - - return context; -}; - -/* - * This function returns an array of completion items for the request method and the url path - */ -export const getUrlPathCompletionItems = ( - model: monaco.editor.ITextModel, - position: monaco.Position -): monaco.languages.CompletionItem[] => { - const { lineNumber, column } = position; - // get the content of the line up until the current position - const lineContent = model.getValueInRange({ - startLineNumber: lineNumber, - startColumn: 1, - endLineNumber: lineNumber, - endColumn: column, - }); - - // get the method and previous url parts for context - const { method, urlPathTokens } = parseLineContent(lineContent); - // remove the last token that is either empty if the url has like "_search/" as the last char - // or it's a word that need to be replaced with autocomplete suggestions like "_search/s" - urlPathTokens.pop(); - const { autoCompleteSet } = populateContextForMethodAndUrl(method, urlPathTokens); - - const wordUntilPosition = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - // replace the whole word with the suggestion - startColumn: lineContent.endsWith('.') - ? // if there is a dot at the end of the content, it's ignored in the wordUntilPosition - wordUntilPosition.startColumn - 1 - : wordUntilPosition.startColumn, - endLineNumber: position.lineNumber, - endColumn: position.column, - }; - if (autoCompleteSet && autoCompleteSet.length > 0) { - return ( - autoCompleteSet - // filter autocomplete items without a name - .filter(({ name }) => Boolean(name)) - // map autocomplete items to completion items - .map((item) => { - return { - label: item.name!, - insertText: item.name!, - detail: item.meta ?? endpointDetailLabel, - // the kind is only used to configure the icon - kind: monaco.languages.CompletionItemKind.Constant, - range, - }; - }) - ); - } - return []; -}; - -/* - * This function returns an array of completion items for the url params - */ -export const getUrlParamsCompletionItems = ( - model: monaco.editor.ITextModel, - position: monaco.Position -): monaco.languages.CompletionItem[] => { - const { lineNumber, column } = position; - // get the content of the line up until the current position - const lineContent = model.getValueInRange({ - startLineNumber: lineNumber, - startColumn: 1, - endLineNumber: lineNumber, - endColumn: column, - }); - - // get the method and previous url parts for context - const { method, urlPathTokens, urlParamsTokens } = parseLineContent(lineContent); - urlPathTokens.push(endOfUrlToken); - const context = populateContextForMethodAndUrl(method, urlPathTokens); - - const urlParamsComponents = context.endpoint?.paramsAutocomplete.getTopLevelComponents(method); - - const currentUrlParamToken = urlParamsTokens.pop(); - // check if we are at the param name or the param value - const urlParamTokenPath = []; - // if there are 2 tokens in the current url param, then we have the name and the value of the param - if (currentUrlParamToken && currentUrlParamToken.length > 1) { - urlParamTokenPath.push(currentUrlParamToken![0]); - } - - populateContext(urlParamTokenPath, context, undefined, true, urlParamsComponents); - - if (context.autoCompleteSet && context.autoCompleteSet.length > 0) { - const wordUntilPosition = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - // replace the whole word with the suggestion - startColumn: wordUntilPosition.startColumn, - endLineNumber: position.lineNumber, - endColumn: position.column, - }; - return ( - context.autoCompleteSet - // filter autocomplete items without a name - .filter(({ name }) => Boolean(name)) - // map autocomplete items to completion items - .map((item) => { - return { - label: item.name!, - insertText: item.name!, - detail: item.meta ?? paramDetailLabel, - // the kind is only used to configure the icon - kind: monaco.languages.CompletionItemKind.Constant, - range, - }; - }) - ); - } - return []; -}; - -const parseLineContent = (lineContent: string): ParsedLineTokens => { - // try to parse into method and url (split on whitespace) - const parts = lineContent.split(whitespacesRegex); - // 1st part is the method - const method = parts[0]; - // 2nd part is the url - const url = parts[1]; - // try to parse into url path and url params (split on question mark) - const { urlPathTokens, urlParamsTokens } = parseUrlTokens(url); - return { method, urlPathTokens, urlParamsTokens }; -}; - -const parseUrlTokens = ( - url: string -): { - urlPathTokens: ParsedLineTokens['urlPathTokens']; - urlParamsTokens: ParsedLineTokens['urlParamsTokens']; -} => { - let urlPathTokens: ParsedLineTokens['urlPathTokens'] = []; - let urlParamsTokens: ParsedLineTokens['urlParamsTokens'] = []; - const urlParts = url.split(questionMarkRegex); - // 1st part is the url path - const urlPath = urlParts[0]; - // try to parse into url path tokens (split on slash) - if (urlPath) { - urlPathTokens = urlPath.split(slashRegex); - } - // 2nd part is the url params - const urlParams = urlParts[1]; - // try to parse into url param tokens - if (urlParams) { - urlParamsTokens = urlParams.split(ampersandRegex).map((urlParamsPart) => { - return urlParamsPart.split(equalsSignRegex); - }); - } - return { urlPathTokens, urlParamsTokens }; -}; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.test.ts new file mode 100644 index 00000000000000..9755bf4e8f442c --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +/* + * Mock the function "populateContext" that accesses the autocomplete definitions + */ +const mockPopulateContext = jest.fn(); + +jest.mock('../../../../../lib/autocomplete/engine', () => { + return { + populateContext: (...args: any) => { + mockPopulateContext(args); + }, + }; +}); +import { AutoCompleteContext } from '../../../../../lib/autocomplete/types'; +import { getDocumentationLinkFromAutocomplete } from './autocomplete_utils'; + +describe('autocomplete_utils', () => { + describe('getDocumentationLinkFromAutocomplete', () => { + const mockRequest = { method: 'GET', url: '_search', data: [] }; + const version = '8.13'; + const expectedLink = 'http://elastic.co/8.13/_search'; + + it('correctly replaces {branch} with the version', () => { + const endpoint = { + documentation: 'http://elastic.co/{branch}/_search', + } as AutoCompleteContext['endpoint']; + // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object + mockPopulateContext.mockImplementation((...args) => { + const context = args[0][1]; + context.endpoint = endpoint; + }); + const link = getDocumentationLinkFromAutocomplete(mockRequest, version); + expect(link).toBe(expectedLink); + }); + + it('correctly replaces /master/ with the version', () => { + const endpoint = { + documentation: 'http://elastic.co/master/_search', + } as AutoCompleteContext['endpoint']; + // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object + mockPopulateContext.mockImplementation((...args) => { + const context = args[0][1]; + context.endpoint = endpoint; + }); + const link = getDocumentationLinkFromAutocomplete(mockRequest, version); + expect(link).toBe(expectedLink); + }); + + it('correctly replaces /current/ with the version', () => { + const endpoint = { + documentation: 'http://elastic.co/current/_search', + } as AutoCompleteContext['endpoint']; + // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object + mockPopulateContext.mockImplementation((...args) => { + const context = args[0][1]; + context.endpoint = endpoint; + }); + const link = getDocumentationLinkFromAutocomplete(mockRequest, version); + expect(link).toBe(expectedLink); + }); + }); +}); diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.ts new file mode 100644 index 00000000000000..75a46590143c49 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/autocomplete_utils.ts @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { monaco } from '@kbn/monaco'; +import { + getEndpointBodyCompleteComponents, + getGlobalAutocompleteComponents, + getTopLevelUrlCompleteComponents, + getUnmatchedEndpointComponents, +} from '../../../../../lib/kb'; +import { AutoCompleteContext, ResultTerm } from '../../../../../lib/autocomplete/types'; +import { populateContext } from '../../../../../lib/autocomplete/engine'; +import type { EditorRequest } from '../types'; +import { parseBody, parseLine, parseUrl } from './tokens_utils'; +import { END_OF_URL_TOKEN, i18nTexts, newLineRegex } from './constants'; + +/* + * This function initializes the autocomplete context for the request + * and returns a documentation link from the endpoint object + * with the branch in the url replaced by the current version "docLinkVersion" + */ +export const getDocumentationLinkFromAutocomplete = ( + request: EditorRequest, + docLinkVersion: string +) => { + // get the url parts from the request url + const { urlPathTokens } = parseUrl(request.url); + // remove the last token, if it's empty + if (!urlPathTokens[urlPathTokens.length - 1]) { + urlPathTokens.pop(); + } + // add the end of url token + urlPathTokens.push(END_OF_URL_TOKEN); + const { endpoint } = populateContextForMethodAndUrl(request.method, urlPathTokens); + if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { + return endpoint.documentation + .replace('/master/', `/${docLinkVersion}/`) + .replace('/current/', `/${docLinkVersion}/`) + .replace('/{branch}/', `/${docLinkVersion}/`); + } + return null; +}; + +/* + * This function returns an array of completion items for the request method + */ +const autocompleteMethods = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH']; +export const getMethodCompletionItems = ( + model: monaco.editor.ITextModel, + position: monaco.Position +): monaco.languages.CompletionItem[] => { + // get the word before suggestions to replace when selecting a suggestion from the list + const wordUntilPosition = model.getWordUntilPosition(position); + return autocompleteMethods.map((method) => ({ + label: method, + insertText: method, + detail: i18nTexts.method, + // only used to configure the icon + kind: monaco.languages.CompletionItemKind.Constant, + range: { + // replace the whole word with the suggestion + startColumn: wordUntilPosition.startColumn, + startLineNumber: position.lineNumber, + endColumn: position.column, + endLineNumber: position.lineNumber, + }, + })); +}; + +/* + * This function initializes the autocomplete context for the provided method and url token path. + */ +const populateContextForMethodAndUrl = (method: string, urlTokenPath: string[]) => { + // get autocomplete components for the request method + const components = getTopLevelUrlCompleteComponents(method); + // this object will contain the information later, it needs to be initialized with some data + // similar to the old ace editor context + const context: AutoCompleteContext = { + method, + urlTokenPath, + }; + + // mutate the context object and put the autocomplete information there + populateContext(urlTokenPath, context, undefined, true, components); + + return context; +}; + +/* + * This function returns an array of completion items for the request method and the url path + */ +export const getUrlPathCompletionItems = ( + model: monaco.editor.ITextModel, + position: monaco.Position +): monaco.languages.CompletionItem[] => { + const { lineNumber, column } = position; + // get the content of the line up until the current position + const lineContent = model.getValueInRange({ + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: column, + }); + + // get the method and previous url parts for context + const { method, urlPathTokens } = parseLine(lineContent); + // remove the last token that is either empty if the url has like "_search/" as the last char + // or it's a word that need to be replaced with autocomplete suggestions like "_search/s" + urlPathTokens.pop(); + const { autoCompleteSet } = populateContextForMethodAndUrl(method, urlPathTokens); + + const wordUntilPosition = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + // replace the whole word with the suggestion + startColumn: lineContent.endsWith('.') + ? // if there is a dot at the end of the content, it's ignored in the wordUntilPosition + wordUntilPosition.startColumn - 1 + : wordUntilPosition.startColumn, + endLineNumber: position.lineNumber, + endColumn: position.column, + }; + if (autoCompleteSet && autoCompleteSet.length > 0) { + return ( + autoCompleteSet + // filter autocomplete items without a name + .filter(({ name }) => Boolean(name)) + // map autocomplete items to completion items + .map((item) => { + return { + label: item.name!, + insertText: item.name!, + detail: item.meta ?? i18nTexts.endpoint, + // the kind is only used to configure the icon + kind: monaco.languages.CompletionItemKind.Constant, + range, + }; + }) + ); + } + return []; +}; + +/* + * This function returns an array of completion items for the url params + */ +export const getUrlParamsCompletionItems = ( + model: monaco.editor.ITextModel, + position: monaco.Position +): monaco.languages.CompletionItem[] => { + const { lineNumber, column } = position; + // get the content of the line up until the current position + const lineContent = model.getValueInRange({ + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: column, + }); + + // get the method and previous url parts for context + const { method, urlPathTokens, urlParamsTokens } = parseLine(lineContent); + urlPathTokens.push(END_OF_URL_TOKEN); + const context = populateContextForMethodAndUrl(method, urlPathTokens); + + const urlParamsComponents = context.endpoint?.paramsAutocomplete.getTopLevelComponents(method); + + const currentUrlParamToken = urlParamsTokens.pop(); + // check if we are at the param name or the param value + const urlParamTokenPath = []; + // if there are 2 tokens in the current url param, then we have the name and the value of the param + if (currentUrlParamToken && currentUrlParamToken.length > 1) { + urlParamTokenPath.push(currentUrlParamToken![0]); + } + + populateContext(urlParamTokenPath, context, undefined, true, urlParamsComponents); + + if (context.autoCompleteSet && context.autoCompleteSet.length > 0) { + const wordUntilPosition = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + // replace the whole word with the suggestion + startColumn: wordUntilPosition.startColumn, + endLineNumber: position.lineNumber, + endColumn: position.column, + }; + return ( + context.autoCompleteSet + // filter autocomplete items without a name + .filter(({ name }) => Boolean(name)) + // map autocomplete items to completion items + .map((item) => { + return { + label: item.name!, + insertText: item.name!, + detail: item.meta ?? i18nTexts.param, + // the kind is only used to configure the icon + kind: monaco.languages.CompletionItemKind.Constant, + range, + }; + }) + ); + } + return []; +}; + +/* + * This function returns an array of completion items for the request body params + */ +export const getBodyCompletionItems = ( + model: monaco.editor.ITextModel, + position: monaco.Position, + requestStartLineNumber: number +): monaco.languages.CompletionItem[] => { + const { lineNumber, column } = position; + + // get the content on the method+url line + const lineContent = model.getLineContent(requestStartLineNumber); + // get the method and previous url parts for context + const { method, urlPathTokens } = parseLine(lineContent); + urlPathTokens.push(END_OF_URL_TOKEN); + const context = populateContextForMethodAndUrl(method, urlPathTokens); + + // get the content of the request body up until this position + const bodyRange: monaco.IRange = { + startLineNumber: requestStartLineNumber + 1, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: column, + }; + const bodyContent = model.getValueInRange(bodyRange); + + const bodyTokens = parseBody(bodyContent); + // needed for scope linking + global term resolving + context.endpointComponentResolver = getEndpointBodyCompleteComponents; + context.globalComponentResolver = getGlobalAutocompleteComponents; + let components: unknown; + if (context.endpoint) { + components = context.endpoint.bodyAutocompleteRootComponents; + } else { + components = getUnmatchedEndpointComponents(); + } + populateContext(bodyTokens, context, undefined, true, components); + + if (context.autoCompleteSet && context.autoCompleteSet.length > 0) { + const wordUntilPosition = model.getWordUntilPosition(position); + // if there is " after the cursor, replace it + let endColumn = position.column; + const charAfterPosition = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: position.lineNumber, + endColumn: position.column + 1, + }); + if (charAfterPosition === '"') { + endColumn = endColumn + 1; + } + const range = { + startLineNumber: position.lineNumber, + // replace the whole word with the suggestion + startColumn: wordUntilPosition.startColumn, + endLineNumber: position.lineNumber, + endColumn, + }; + return ( + context.autoCompleteSet + // filter autocomplete items without a name + .filter(({ name }) => Boolean(name)) + // map autocomplete items to completion items + .map((item) => { + const suggestion = { + // convert name to a string + label: item.name + '', + insertText: getInsertText(item, bodyContent), + detail: i18nTexts.api, + // the kind is only used to configure the icon + kind: monaco.languages.CompletionItemKind.Constant, + range, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }; + return suggestion; + }) + ); + } + return []; +}; + +const getInsertText = ( + { name, insertValue, template, value }: ResultTerm, + bodyContent: string +): string => { + let insertText = bodyContent.endsWith('"') ? '' : '"'; + if (insertValue && insertValue !== '{' && insertValue !== '[') { + insertText += `${insertValue}"`; + } else { + insertText += `${name}"`; + } + // check if there is template to add + if (template !== undefined) { + let templateLines; + const { __raw, value: templateValue } = template; + if (__raw && templateValue) { + templateLines = templateValue.split(newLineRegex); + } else { + templateLines = JSON.stringify(template, null, 2).split(newLineRegex); + } + // TODO add correct indentation + insertText += ': ' + templateLines.join('\n'); + } else if (value === '{') { + insertText += '{}'; + } else if (value === '[') { + insertText += '[]'; + } + return insertText; +}; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts new file mode 100644 index 00000000000000..c0616fc4dc0a17 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +/* + * CSS class name used for the styling of highlighted requests + */ +export const SELECTED_REQUESTS_CLASSNAME = 'console__monaco_editor__selectedRequests'; + +export const whitespacesRegex = /\s+/; +export const newLineRegex = /\n/; +export const slashRegex = /\//; +export const ampersandRegex = /&/; +export const equalsSignRegex = /=/; +export const questionMarkRegex = /\?/; +export const variableTemplateRegex = /\${(\w+)}/g; +export const numberStartRegex = /[-\d]/; +export const digitRegex = /[\d]/; +export const END_OF_URL_TOKEN = '__url_path_end__'; + +/* + * i18n for autocomplete labels + */ +export const i18nTexts = { + method: i18n.translate('console.autocompleteSuggestions.methodLabel', { + defaultMessage: 'method', + }), + endpoint: i18n.translate('console.autocompleteSuggestions.endpointLabel', { + defaultMessage: 'endpoint', + }), + param: i18n.translate('console.autocompleteSuggestions.paramLabel', { + defaultMessage: 'param', + }), + api: i18n.translate('console.autocompleteSuggestions.apiLabel', { + defaultMessage: 'API', + }), +}; + +export enum AutocompleteType { + PATH = 'path', + URL_PARAMS = 'url_params', + METHOD = 'method', + BODY = 'body', +} diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts new file mode 100644 index 00000000000000..a0de7b461e99a3 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { AutocompleteType, SELECTED_REQUESTS_CLASSNAME } from './constants'; +export { + getRequestStartLineNumber, + getRequestEndLineNumber, + stringifyRequest, + replaceRequestVariables, + getCurlRequest, + trackSentRequests, + getAutoIndentedRequests, +} from './requests_utils'; +export { + getDocumentationLinkFromAutocomplete, + getMethodCompletionItems, + getUrlPathCompletionItems, + getUrlParamsCompletionItems, + getBodyCompletionItems, +} from './autocomplete_utils'; +export { getLineTokens, containsUrlParams } from './tokens_utils'; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts similarity index 78% rename from src/plugins/console/public/application/containers/editor/monaco/utils.test.ts rename to src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts index 96bbea0cc54492..f07a6db3a68819 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts @@ -9,29 +9,13 @@ import { getAutoIndentedRequests, getCurlRequest, - getDocumentationLink, - removeTrailingWhitespaces, replaceRequestVariables, stringifyRequest, trackSentRequests, -} from './utils'; -import { MetricsTracker } from '../../../../types'; -import { AutoCompleteContext } from '../../../../lib/autocomplete/types'; +} from './requests_utils'; +import { MetricsTracker } from '../../../../../types'; -/* - * Mock the function "populateContext" that accesses the autocomplete definitions - */ -const mockPopulateContext = jest.fn(); - -jest.mock('../../../../lib/autocomplete/engine', () => { - return { - populateContext: (...args: any) => { - mockPopulateContext(args); - }, - }; -}); - -describe('monaco editor utils', () => { +describe('requests_utils', () => { const dataObjects = [ { query: { @@ -42,24 +26,6 @@ describe('monaco editor utils', () => { test: 'test', }, ]; - describe('removeTrailingWhitespaces', () => { - it(`works with an empty string`, () => { - const url = ''; - const result = removeTrailingWhitespaces(url); - expect(result).toBe(url); - }); - it(`doesn't change the string if no trailing whitespaces`, () => { - const url = '_search'; - const result = removeTrailingWhitespaces(url); - expect(result).toBe(url); - }); - it(`removes any text after the first whitespace`, () => { - const url = '_search some_text'; - const result = removeTrailingWhitespaces(url); - expect(result).toBe('_search'); - }); - }); - describe('stringifyRequest', () => { const request = { startOffset: 0, @@ -196,51 +162,6 @@ describe('monaco editor utils', () => { }); }); - describe('getDocumentationLink', () => { - const mockRequest = { method: 'GET', url: '_search', data: [] }; - const version = '8.13'; - const expectedLink = 'http://elastic.co/8.13/_search'; - - it('correctly replaces {branch} with the version', () => { - const endpoint = { - documentation: 'http://elastic.co/{branch}/_search', - } as AutoCompleteContext['endpoint']; - // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object - mockPopulateContext.mockImplementation((...args) => { - const context = args[0][1]; - context.endpoint = endpoint; - }); - const link = getDocumentationLink(mockRequest, version); - expect(link).toBe(expectedLink); - }); - - it('correctly replaces /master/ with the version', () => { - const endpoint = { - documentation: 'http://elastic.co/master/_search', - } as AutoCompleteContext['endpoint']; - // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object - mockPopulateContext.mockImplementation((...args) => { - const context = args[0][1]; - context.endpoint = endpoint; - }); - const link = getDocumentationLink(mockRequest, version); - expect(link).toBe(expectedLink); - }); - - it('correctly replaces /current/ with the version', () => { - const endpoint = { - documentation: 'http://elastic.co/current/_search', - } as AutoCompleteContext['endpoint']; - // mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object - mockPopulateContext.mockImplementation((...args) => { - const context = args[0][1]; - context.endpoint = endpoint; - }); - const link = getDocumentationLink(mockRequest, version); - expect(link).toBe(expectedLink); - }); - }); - describe('getAutoIndentedRequests', () => { const sampleEditorTextLines = [ ' ', // line 1 diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts new file mode 100644 index 00000000000000..bf9c6074bb20a7 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { monaco, ParsedRequest } from '@kbn/monaco'; +import { constructUrl } from '../../../../../lib/es'; +import { MetricsTracker } from '../../../../../types'; +import type { DevToolsVariable } from '../../../../components'; +import type { EditorRequest } from '../types'; +import { variableTemplateRegex } from './constants'; +import { removeTrailingWhitespaces } from './tokens_utils'; +import { AdjustedParsedRequest } from '../types'; + +/* + * This function stringifies and normalizes the parsed request: + * - the method is converted to upper case + * - any trailing comments are removed from the url + * - the request body is stringified from an object using JSON.stringify + */ +export const stringifyRequest = (parsedRequest: ParsedRequest): EditorRequest => { + const url = removeTrailingWhitespaces(parsedRequest.url); + const method = parsedRequest.method.toUpperCase(); + const data = parsedRequest.data?.map((parsedData) => JSON.stringify(parsedData, null, 2)); + return { url, method, data: data ?? [] }; +}; + +/* + * This function replaces any variables with its values stored in localStorage. + * For example 'GET ${exampleVariable1} -> 'GET _search'. + */ +export const replaceRequestVariables = ( + { method, url, data }: EditorRequest, + variables: DevToolsVariable[] +): EditorRequest => { + return { + method, + url: replaceVariables(url, variables), + data: data.map((dataObject) => replaceVariables(dataObject, variables)), + }; +}; + +/* + * This function converts a request into a corresponding CURL command. + */ +export const getCurlRequest = ( + { method, url, data }: EditorRequest, + elasticsearchBaseUrl: string +): string => { + const curlUrl = constructUrl(elasticsearchBaseUrl, url); + let curlRequest = `curl -X${method} "${curlUrl}" -H "kbn-xsrf: reporting"`; + if (data.length > 0) { + curlRequest += ` -H "Content-Type: application/json" -d'\n`; + curlRequest += data.join('\n'); + curlRequest += "'"; + } + return curlRequest; +}; + +/* + * This function uses the telemetry to track requests sent via Console. + */ +export const trackSentRequests = ( + requests: EditorRequest[], + trackUiMetric: MetricsTracker +): void => { + requests.map(({ method, url }) => { + const eventName = `${method}_${url}`; + trackUiMetric.count(eventName); + }); +}; + +/* + * This function converts the start offset value of the parsed request to a line number in the model + */ +export const getRequestStartLineNumber = ( + parsedRequest: ParsedRequest, + model: monaco.editor.ITextModel +): number => { + return model.getPositionAt(parsedRequest.startOffset).lineNumber; +}; + +/* + * This function converts the end offset value of the parsed request to a line number in the model. + * If there is no end offset (the parser was not able to parse this request completely), + * then the last non-empty line is returned or the line before the next request. + */ +export const getRequestEndLineNumber = ( + parsedRequest: ParsedRequest, + model: monaco.editor.ITextModel, + index: number, + parsedRequests: ParsedRequest[] +): number => { + let endLineNumber: number; + if (parsedRequest.endOffset) { + // if the parser set an end offset for this request, then find the line number for it + endLineNumber = model.getPositionAt(parsedRequest.endOffset).lineNumber; + } else { + // if no end offset, try to find the line before the next request starts + const nextRequest = parsedRequests.at(index + 1); + if (nextRequest) { + const nextRequestStartLine = model.getPositionAt(nextRequest.startOffset).lineNumber; + endLineNumber = nextRequestStartLine - 1; + } else { + // if there is no next request, take the last line of the model + endLineNumber = model.getLineCount(); + } + } + // if the end line is empty, go up to find the first non-empty line + let lineContent = model.getLineContent(endLineNumber).trim(); + while (!lineContent) { + endLineNumber = endLineNumber - 1; + lineContent = model.getLineContent(endLineNumber).trim(); + } + return endLineNumber; +}; + +/* + * Internal helpers + */ +const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { + if (variableTemplateRegex.test(text)) { + text = text.replaceAll(variableTemplateRegex, (match, key) => { + const variable = variables.find(({ name }) => name === key); + + return variable?.value ?? match; + }); + } + return text; +}; + +const containsComments = (text: string) => { + return text.indexOf('//') >= 0 || text.indexOf('/*') >= 0; +}; + +/** + * This function takes a string containing unformatted Console requests and + * returns a text in which the requests are auto-indented. + * @param requests The list of {@link AdjustedParsedRequest} that are in the selected text in the editor. + * @param selectedText The selected text in the editor. + * @param allText The whole text input in the editor. + */ +export const getAutoIndentedRequests = ( + requests: AdjustedParsedRequest[], + selectedText: string, + allText: string +): string => { + const selectedTextLines = selectedText.split(`\n`); + const allTextLines = allText.split(`\n`); + const formattedTextLines: string[] = []; + + let currentLineIndex = 0; + let currentRequestIndex = 0; + + while (currentLineIndex < selectedTextLines.length) { + const request = requests[currentRequestIndex]; + // Check if the current line is the start of the next request + if ( + request && + selectedTextLines[currentLineIndex] === allTextLines[request.startLineNumber - 1] + ) { + // Start of a request + const requestLines = allTextLines.slice(request.startLineNumber - 1, request.endLineNumber); + + if (requestLines.some((line) => containsComments(line))) { + // If request has comments, add it as it is - without formatting + // TODO: Format requests with comments + formattedTextLines.push(...requestLines); + } else { + // If no comments, add stringified parsed request + const stringifiedRequest = stringifyRequest(request); + const firstLine = stringifiedRequest.method + ' ' + stringifiedRequest.url; + formattedTextLines.push(firstLine); + + if (stringifiedRequest.data && stringifiedRequest.data.length > 0) { + formattedTextLines.push(...stringifiedRequest.data); + } + } + + currentLineIndex = currentLineIndex + requestLines.length; + currentRequestIndex++; + } else { + // Current line is a comment or whitespaces + // Trim white spaces and add it to the formatted text + formattedTextLines.push(selectedTextLines[currentLineIndex].trim()); + currentLineIndex++; + } + } + + return formattedTextLines.join('\n'); +}; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.test.ts new file mode 100644 index 00000000000000..56d9dea22b743e --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseBody, removeTrailingWhitespaces } from './tokens_utils'; + +describe('tokens_utils', () => { + describe('removeTrailingWhitespaces', () => { + it(`works with an empty string`, () => { + const url = ''; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it(`doesn't change the string if no trailing whitespaces`, () => { + const url = '_search'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it(`removes any text after the first whitespace`, () => { + const url = '_search some_text'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('_search'); + }); + }); + + describe('parseBody', () => { + const testCases: Array<{ value: string; tokens: string[] }> = [ + { + // add opening curly brackets to tokens + value: '{', + tokens: ['{'], + }, + { + // allow whitespaces + value: ' {', + tokens: ['{'], + }, + { + // allow line comments + value: '//comment\n{', + tokens: ['{'], + }, + { + // inside the object line comment are ignored + value: '{//comment', + tokens: ['{'], + }, + { + value: '{//comment\n"test":', + tokens: ['{', 'test'], + }, + { + // do not add property name if no colon (:) + value: '{"test"', + tokens: ['{'], + }, + { + // add property names to tokens (double quotes are removed) + value: '{"test":', + tokens: ['{', 'test'], + }, + { + // add nested object to tokens + value: '{"test":{', + tokens: ['{', 'test', '{'], + }, + { + // empty object + value: '{}', + tokens: [], + }, + { + // empty object with inline comment + value: '{//comment\n}', + tokens: [], + }, + { + value: '{"test":[', + tokens: ['{', 'test', '['], + }, + { + value: '{"test":123,', + tokens: ['{'], + }, + { + value: '{"test":{},', + tokens: ['{'], + }, + { + value: '{"test":[],', + tokens: ['{'], + }, + { + value: '{"property1":["nested1", []],"', + tokens: ['{'], + }, + { + value: '{"property1":"value1","property2":', + tokens: ['{', 'property2'], + }, + { + value: '{"property1":[123,', + tokens: ['{', 'property1', '['], + }, + { + value: '{"property1":[123,"test"]', + tokens: ['{'], + }, + { + value: '{"property1":{"nested1":"value"},"', + tokens: ['{'], + }, + { + value: '{"property1":{"nested1":"value","nested2":{}},"', + tokens: ['{'], + }, + ]; + for (const testCase of testCases) { + const { value, tokens } = testCase; + it(`${value}`, () => { + const parsedTokens = parseBody(value); + expect(parsedTokens).toEqual(tokens); + }); + } + }); +}); diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.ts new file mode 100644 index 00000000000000..76e6e9672252fc --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/tokens_utils.ts @@ -0,0 +1,480 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { + ampersandRegex, + digitRegex, + equalsSignRegex, + newLineRegex, + numberStartRegex, + questionMarkRegex, + slashRegex, + whitespacesRegex, +} from './constants'; + +/* + * This function parses a line with the method and url. + * The url is parsed into path and params, each parsed into tokens. + * Returns method, urlPathTokens and urlParamsTokens which are arrays of strings. + */ +export const parseLine = (line: string): ParsedLineTokens => { + // try to parse into method and url (split on whitespace) + const parts = line.split(whitespacesRegex); + // 1st part is the method + const method = parts[0]; + // 2nd part is the url + const url = parts[1]; + // try to parse into url path and url params (split on question mark) + const { urlPathTokens, urlParamsTokens } = parseUrl(url); + return { method, urlPathTokens, urlParamsTokens }; +}; + +/* + * This function parses an url into path and params, each parsed into tokens. + * Returns urlPathTokens and urlParamsTokens which are arrays of strings. + */ +export const parseUrl = ( + url: string +): { + urlPathTokens: ParsedLineTokens['urlPathTokens']; + urlParamsTokens: ParsedLineTokens['urlParamsTokens']; +} => { + let urlPathTokens: ParsedLineTokens['urlPathTokens'] = []; + let urlParamsTokens: ParsedLineTokens['urlParamsTokens'] = []; + const urlParts = url.split(questionMarkRegex); + // 1st part is the url path + const urlPath = urlParts[0]; + // try to parse into url path tokens (split on slash) + if (urlPath) { + urlPathTokens = urlPath.split(slashRegex); + } + // 2nd part is the url params + const urlParams = urlParts[1]; + // try to parse into url param tokens + if (urlParams) { + urlParamsTokens = urlParams.split(ampersandRegex).map((urlParamsPart) => { + return urlParamsPart.split(equalsSignRegex); + }); + } + return { urlPathTokens, urlParamsTokens }; +}; + +/* + * This function parses the body of the request into tokens. + * For example '{ "test": [' -> ['{', 'test', '[']. This array is used for autocomplete. + * Returns array of strings representing body tokens for autocomplete. + */ +export const parseBody = (value: string): string[] => { + let currentToken = ''; + const tokens = []; + let index = 0; + let char = value.charAt(index); + const next = () => { + index++; + char = value.charAt(index); + }; + const peek = (offset: number): string => { + return value.charAt(index + offset); + }; + const skipWhitespace = () => { + while (whitespacesRegex.test(char)) { + next(); + } + }; + const skipUntilAfterNewLine = () => { + while (char && !isNewLine(char)) { + next(); + } + // skip the new line + if (isNewLine(char)) { + next(); + } + }; + const skipComments = () => { + // # comment + if (isHashChar(char)) { + // first skip # + next(); + skipUntilAfterNewLine(); + } else if ( + // // comment + isSlash(char) && + isSlash(peek(1)) + ) { + // first skip // + next(); + next(); + skipUntilAfterNewLine(); + } else if ( + // multi line comment starting with /* + isSlash(char) && + isStar(peek(1)) + ) { + next(); + next(); + // skip until closing */ is found + while (char && !(isStar(char) && isSlash(peek(1)))) { + next(); + } + if (isStar(char) && isSlash(peek(1))) { + next(); + next(); + } else { + throw new Error('Not able to parse multi-line comment'); + } + } + }; + const parseString = () => { + // first check if it's a triple quote + if (isTripleQuote(char, peek(1), peek(2))) { + // skip the opening triple quote + next(); + next(); + next(); + // skip to the next triple quote + while (char && !isTripleQuote(char, peek(1), peek(2))) { + next(); + } + if (isTripleQuote(char, peek(1), peek(2))) { + // skip the closing triple quote + next(); + next(); + next(); + } else { + throw new Error('Missing closing triple quote'); + } + } else if (isDoubleQuote(char)) { + // skip the opening double quote + next(); + while (char && !isDoubleQuote(char)) { + next(); + } + if (isDoubleQuote(char)) { + // skip the closing double quote + next(); + } else { + throw new Error('Missing closing double quote'); + } + } else { + throw new Error('Not able to parse as string'); + } + }; + const parseNumber = () => { + // check the first char + if (!isNumberStartChar(char)) { + throw new Error('Not able to parse as number'); + } + if (isMinusSign(char)) { + next(); + } + // check that there is at least 1 digit + if (!isDigit(char)) { + throw new Error('Not able to parse as number'); + } + // skip digits + while (isDigit(char)) { + next(); + } + // optionally there is a dot + if (isDot(char)) { + next(); + // needs at least 1 digit after the dot + if (!isDigit(char)) { + throw new Error('Missing digits after a dot'); + } + while (isDigit(char)) { + next(); + } + } + // optionally there is E notation + if (isENotation(char)) { + next(); + // needs at least 1 digit after e or E + if (!isDigit(char)) { + throw new Error('Missing digits after E notation'); + } + while (isDigit(char)) { + next(); + } + } + // number parsing is complete + }; + const parseKeyword = () => { + switch (char) { + case 'n': { + if (peek(1) === 'u' && peek(2) === 'l' && peek(3) === 'l') { + next(); + next(); + next(); + next(); + } else { + throw new Error('Not able to parse as null'); + } + break; + } + case 't': { + if (peek(1) === 'r' && peek(2) === 'u' && peek(3) === 'e') { + next(); + next(); + next(); + next(); + } else { + throw new Error('Not able to parse as true'); + } + break; + } + case 'f': { + if (peek(1) === 'a' && peek(2) === 'l' && peek(3) === 's' && peek(3) === 'e') { + next(); + next(); + next(); + next(); + next(); + } else { + throw new Error('Not able to parse as false'); + } + break; + } + default: { + throw new Error('Not able to parse as null, true or false'); + } + } + }; + const parsePropertyName = () => { + if (!isDoubleQuote(char)) { + throw new Error('Missing " at the start of string'); + } + next(); + let propertyName = ''; + while (char && !isDoubleQuote(char)) { + propertyName = propertyName + char; + next(); + } + if (!isDoubleQuote(char)) { + throw new Error('Missing " at the end of string'); + } + next(); + if (!propertyName) { + throw new Error('Empty string used as property name'); + } + return propertyName; + }; + + try { + while (char) { + // the value in currentToken determines the state of the parser + if (!currentToken) { + // the start of the object + skipWhitespace(); + skipComments(); + // look for opening curly bracket + if (char === '{') { + tokens.push(char); + currentToken = char; + next(); + } else { + throw new Error('Missing { at object start'); + } + } else if ( + // inside an object + currentToken === '{' + ) { + skipWhitespace(); + skipComments(); + // inspect the current char + if (isDoubleQuote(char)) { + // property name: parse the string and add to tokens + const propertyName = parsePropertyName(); + // allow whitespace + skipWhitespace(); + // expecting a colon, otherwise the parser fails + if (!isColon(char)) { + throw new Error('Not able to parse'); + } + // add the property name to the tokens + tokens.push(propertyName); + currentToken = propertyName; + next(); + } else if (char === '}') { + // empty object: remove the corresponding opening { from tokens + tokens.pop(); + currentToken = tokens[tokens.length - 1]; + next(); + + skipWhitespace(); + // check if the empty object was used as a property value + if (isPropertyName(currentToken)) { + // the empty object was the value for this property name, remove it from tokens + tokens.pop(); + currentToken = tokens[tokens.length - 1]; + } + } else if (isComma(char)) { + // ignore the comma + next(); + } else { + throw new Error('Not able to parse'); + } + } else if ( + // inside an array + currentToken === '[' + ) { + skipWhitespace(); + skipComments(); + + // inspect the current char + if (char === ']') { + // an empty array + tokens.pop(); + currentToken = tokens[tokens.length - 1]; + next(); + + skipWhitespace(); + // check if empty array was used as a property value + if (isPropertyName(currentToken)) { + // the empty array was the value for this property name, remove it from tokens + tokens.pop(); + currentToken = tokens[tokens.length - 1]; + } + } else if (isComma(char)) { + // ignore the comma + next(); + } else { + // parsing array items + + // object or array: add to tokens + if (char === '{' || char === '[') { + tokens.push(char); + currentToken = char; + next(); + } else { + // simple values + if (isDoubleQuote(char)) { + parseString(); + } else if (isNumberStartChar(char)) { + parseNumber(); + } else if (isKeywordChar(char)) { + parseKeyword(); + } else { + throw new Error('Not able to parse'); + } + } + } + } else if ( + // parsing property value after a property name was found + isPropertyName(currentToken) + ) { + skipWhitespace(); + skipComments(); + if (char === '{' || char === '[') { + // nested object or array + tokens.push(char); + currentToken = char; + next(); + } else { + // simple values + if (isDoubleQuote(char)) { + parseString(); + } else if (isNumberStartChar(char)) { + parseNumber(); + } else if (isKeywordChar(char)) { + parseKeyword(); + } else { + throw new Error('Not able to parse'); + } + // after parsing a simple value, this property name is parsed and can be removed from tokens + tokens.pop(); + currentToken = tokens[tokens.length - 1]; + } + } else { + throw new Error('Not able to parse'); + } + } + return tokens; + } catch (e) { + return tokens; + } +}; + +/* + * This functions removes any trailing inline comments, for example + * "_search // comment" -> "_search" + * Ideally the parser would do that, but currently they are included in url. + */ +export const removeTrailingWhitespaces = (url: string): string => { + return url.trim().split(whitespacesRegex)[0]; +}; + +/* + * This function splits a string on whitespaces and returns its parts as an array. + */ +export const getLineTokens = (lineContent: string): string[] => { + return lineContent.split(whitespacesRegex); +}; + +/* + * This function checks if the url contains url params. + */ +export const containsUrlParams = (lineContent: string): boolean => { + return questionMarkRegex.test(lineContent); +}; + +/* + * Internal helpers + */ +interface ParsedLineTokens { + method: string; + urlPathTokens: string[]; + urlParamsTokens: string[][]; +} + +const isNewLine = (char: string): boolean => { + return newLineRegex.test(char); +}; +const isDoubleQuote = (char: string): boolean => { + return char === '"'; +}; +const isColon = (char: string): boolean => { + return char === ':'; +}; +const isComma = (char: string): boolean => { + return char === ','; +}; +const isHashChar = (char: string): boolean => { + return char === '#'; +}; +const isSlash = (char: string): boolean => { + return char === '/'; +}; +const isStar = (char: string): boolean => { + return char === '*'; +}; +const isPropertyName = (token: string): boolean => { + // we only have {, [ or property name in tokens + return token !== '{' && token !== '['; +}; +const isTripleQuote = (char1: string, char2: string, char3: string): boolean => { + return isDoubleQuote(char1) && isDoubleQuote(char2) && isDoubleQuote(char3); +}; +const isNumberStartChar = (char: string): boolean => { + return numberStartRegex.test(char); +}; +const isMinusSign = (char: string): boolean => { + return char === '-'; +}; +const isDigit = (char: string): boolean => { + return digitRegex.test(char); +}; +const isDot = (char: string): boolean => { + return char === '.'; +}; +const isENotation = (char: string): boolean => { + return char === 'e' || char === 'E'; +}; +const isKeywordChar = (char: string): boolean => { + // null, true or false + return char === 'n' || char === 't' || char === 'f'; +}; diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts index 1996330bef66b6..0bdedac7364547 100644 --- a/src/plugins/console/public/application/hooks/index.ts +++ b/src/plugins/console/public/application/hooks/index.ts @@ -8,6 +8,6 @@ export { useSetInputEditor } from './use_set_input_editor'; export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; -export { useSendCurrentRequest } from './use_send_current_request'; +export { useSendCurrentRequest, sendRequest } from './use_send_current_request'; export { useSaveCurrentTextObject } from './use_save_current_text_object'; export { useDataInit } from './use_data_init'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts index 33bdbef87f2efd..beb40449657391 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts @@ -7,3 +7,4 @@ */ export { useSendCurrentRequest } from './use_send_current_request'; +export { sendRequest } from './send_request'; diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 04bfaa4a57b596..bdb2a16c879abe 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -423,12 +423,7 @@ export default function ({ }); } - function applyTerm(term: { - value?: string; - context?: AutoCompleteContext; - template?: { __raw?: boolean; value?: string; [key: string]: unknown }; - insertValue?: string; - }) { + function applyTerm(term: ResultTerm) { const context = term.context!; if (context?.endpoint && term.value) { diff --git a/src/plugins/console/public/lib/autocomplete/types.ts b/src/plugins/console/public/lib/autocomplete/types.ts index 919d273b7a0901..7d1fb383f52a57 100644 --- a/src/plugins/console/public/lib/autocomplete/types.ts +++ b/src/plugins/console/public/lib/autocomplete/types.ts @@ -15,6 +15,7 @@ export interface ResultTerm { name?: string; value?: string; score?: number; + template?: { __raw?: boolean; value?: string; [key: string]: unknown }; } export interface DataAutoCompleteRulesOneOf { diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/esql.query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/esql.query.json index 452ab7c7b7eb9d..f109f849be68c9 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/esql.query.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/esql.query.json @@ -17,7 +17,7 @@ "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/{branch}/esql-rest.html", "availability": { "stack": true, - "serverless": false + "serverless": true } } } diff --git a/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.test.tsx b/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.test.tsx index 44d1847af1dce2..25a86366144bcc 100644 --- a/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.test.tsx +++ b/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.test.tsx @@ -19,19 +19,17 @@ import { CustomizePanelEditor } from './customize_panel_editor'; describe('customize panel editor', () => { let api: CustomizePanelActionApi; - let setTitle: (title: string | undefined) => void; + let setTitle: (title?: string) => void; let setViewMode: (viewMode: ViewMode) => void; - let setDescription: (description: string | undefined) => void; + let setDescription: (description?: string) => void; beforeEach(() => { const titleSubject = new BehaviorSubject(undefined); - setTitle = jest.fn().mockImplementation((title) => titleSubject.next(title)); + setTitle = jest.fn((title) => titleSubject.next(title)); const descriptionSubject = new BehaviorSubject(undefined); - setDescription = jest - .fn() - .mockImplementation((description) => descriptionSubject.next(description)); + setDescription = jest.fn((description) => descriptionSubject.next(description)); const viewMode = new BehaviorSubject('edit'); - setViewMode = jest.fn().mockImplementation((nextViewMode) => viewMode.next(nextViewMode)); + setViewMode = jest.fn((nextViewMode) => viewMode.next(nextViewMode)); api = { viewMode, @@ -75,27 +73,44 @@ describe('customize panel editor', () => { ); }); - it('Sets panel title on apply', () => { + it('should set panel title on apply', () => { renderPanelEditor(); userEvent.type(screen.getByTestId('customEmbeddablePanelTitleInput'), 'New title'); userEvent.click(screen.getByTestId('saveCustomizePanelButton')); expect(setTitle).toBeCalledWith('New title'); }); + it('should use default title when title is undefined', () => { + api.defaultPanelTitle = new BehaviorSubject('Default title'); + setTitle(undefined); + renderPanelEditor(); + const titleInput = screen.getByTestId('customEmbeddablePanelTitleInput'); + expect(titleInput).toHaveValue('Default title'); + }); + + it('should use title even when empty string', () => { + api.defaultPanelTitle = new BehaviorSubject('Default title'); + setTitle(''); + renderPanelEditor(); + const titleInput = screen.getByTestId('customEmbeddablePanelTitleInput'); + expect(titleInput).toHaveValue(''); + }); + it('Resets panel title to default when reset button is pressed', () => { api.defaultPanelTitle = new BehaviorSubject('Default title'); + setTitle('Initial title'); renderPanelEditor(); userEvent.type(screen.getByTestId('customEmbeddablePanelTitleInput'), 'New title'); userEvent.click(screen.getByTestId('resetCustomEmbeddablePanelTitleButton')); expect(screen.getByTestId('customEmbeddablePanelTitleInput')).toHaveValue('Default title'); }); - it('Reset panel title to undefined on apply', () => { - setTitle('very cool title'); + it('should hide title reset when no default exists', () => { + api.defaultPanelTitle = new BehaviorSubject(undefined); + setTitle('Initial title'); renderPanelEditor(); - userEvent.click(screen.getByTestId('resetCustomEmbeddablePanelTitleButton')); - userEvent.click(screen.getByTestId('saveCustomizePanelButton')); - expect(setTitle).toBeCalledWith(undefined); + userEvent.type(screen.getByTestId('customEmbeddablePanelTitleInput'), 'New title'); + expect(screen.queryByTestId('resetCustomEmbeddablePanelTitleButton')).not.toBeInTheDocument(); }); test('title input receives focus when `focusOnTitle` is `true`', async () => { @@ -128,7 +143,7 @@ describe('customize panel editor', () => { ); }); - it('Sets panel description on apply', () => { + it('should set panel description on apply', () => { renderPanelEditor(); userEvent.type( screen.getByTestId('customEmbeddablePanelDescriptionInput'), @@ -138,22 +153,47 @@ describe('customize panel editor', () => { expect(setDescription).toBeCalledWith('New description'); }); - it('Resets panel desription to default when reset button is pressed', () => { + it('should use default description when description is undefined', () => { + api.defaultPanelDescription = new BehaviorSubject('Default description'); + setDescription(undefined); + renderPanelEditor(); + const descriptionInput = screen.getByTestId('customEmbeddablePanelDescriptionInput'); + expect(descriptionInput).toHaveValue('Default description'); + }); + + it('should use description even when empty string', () => { api.defaultPanelDescription = new BehaviorSubject('Default description'); + setDescription(''); renderPanelEditor(); - userEvent.type(screen.getByTestId('customEmbeddablePanelDescriptionInput'), 'New desription'); + const descriptionInput = screen.getByTestId('customEmbeddablePanelDescriptionInput'); + expect(descriptionInput).toHaveValue(''); + }); + + it('Resets panel description to default when reset button is pressed', () => { + api.defaultPanelDescription = new BehaviorSubject('Default description'); + setDescription('Initial description'); + renderPanelEditor(); + userEvent.type( + screen.getByTestId('customEmbeddablePanelDescriptionInput'), + 'New description' + ); userEvent.click(screen.getByTestId('resetCustomEmbeddablePanelDescriptionButton')); expect(screen.getByTestId('customEmbeddablePanelDescriptionInput')).toHaveValue( 'Default description' ); }); - it('Reset panel description to undefined on apply', () => { - setDescription('very cool description'); + it('should hide description reset when no default exists', () => { + api.defaultPanelDescription = new BehaviorSubject(undefined); + setDescription('Initial description'); renderPanelEditor(); - userEvent.click(screen.getByTestId('resetCustomEmbeddablePanelDescriptionButton')); - userEvent.click(screen.getByTestId('saveCustomizePanelButton')); - expect(setDescription).toBeCalledWith(undefined); + userEvent.type( + screen.getByTestId('customEmbeddablePanelDescriptionInput'), + 'New description' + ); + expect( + screen.queryByTestId('resetCustomEmbeddablePanelDescriptionButton') + ).not.toBeInTheDocument(); }); }); diff --git a/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.tsx b/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.tsx index d74b55a69dae4c..ba6ae9280a07ac 100644 --- a/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.tsx +++ b/src/plugins/presentation_panel/public/panel_actions/customize_panel_action/customize_panel_editor.tsx @@ -33,6 +33,7 @@ import { apiPublishesTimeRange, apiPublishesUnifiedSearch, getInheritedViewMode, + getPanelDescription, getPanelTitle, PublishesUnifiedSearch, } from '@kbn/presentation-publishing'; @@ -62,10 +63,8 @@ export const CustomizePanelEditor = ({ */ const editMode = getInheritedViewMode(api) === 'edit'; const [hideTitle, setHideTitle] = useState(api.hidePanelTitle?.value); - const [panelDescription, setPanelDescription] = useState( - api.panelDescription?.value ?? api.defaultPanelDescription?.value - ); const [panelTitle, setPanelTitle] = useState(getPanelTitle(api)); + const [panelDescription, setPanelDescription] = useState(getPanelDescription(api)); const [timeRange, setTimeRange] = useState( api.timeRange$?.value ?? api.parentApi?.timeRange$?.value ); @@ -121,7 +120,6 @@ export const CustomizePanelEditor = ({ } labelAppend={ - setPanelTitle(api.defaultPanelTitle?.value)} - disabled={hideTitle || !editMode || api?.defaultPanelTitle?.value === panelTitle} - aria-label={i18n.translate( - 'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel', - { - defaultMessage: 'Reset title', - } - )} - > - - + api?.defaultPanelTitle?.value && ( + setPanelTitle(api.defaultPanelTitle?.value)} + disabled={hideTitle || panelTitle === api?.defaultPanelTitle?.value} + aria-label={i18n.translate( + 'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel', + { + defaultMessage: 'Reset title to default', + } + )} + > + + + ) } > setPanelTitle(e.target.value)} aria-label={i18n.translate( @@ -185,23 +185,25 @@ export const CustomizePanelEditor = ({ /> } labelAppend={ - setPanelDescription(api.defaultPanelDescription?.value)} - disabled={!editMode || api.defaultPanelDescription?.value === panelDescription} - aria-label={i18n.translate( - 'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel', - { - defaultMessage: 'Reset description', - } - )} - > - - + api.defaultPanelDescription?.value && ( + setPanelDescription(api.defaultPanelDescription?.value)} + disabled={api.defaultPanelDescription?.value === panelDescription} + aria-label={i18n.translate( + 'presentationPanel.action.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel', + { + defaultMessage: 'Reset description to default', + } + )} + > + + + ) } > { const panelTitle = getPanelTitle(embeddable) || i18n.translate('presentationPanel.action.inspectPanel.untitledEmbeddableFilename', { - defaultMessage: 'untitled', + defaultMessage: '[No Title]', }); const session = inspector.open(adapters, { title: panelTitle, diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index cb03cc72047cfa..c32c8d6d403310 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -79,7 +79,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` labelType="label" > > diff --git a/test/functional/apps/dashboard/group6/embeddable_library.ts b/test/functional/apps/dashboard/group6/embeddable_library.ts index f2bc3147095a2c..aa0a7341e17c00 100644 --- a/test/functional/apps/dashboard/group6/embeddable_library.ts +++ b/test/functional/apps/dashboard/group6/embeddable_library.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.closeAddPanel(); const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); - await panelActions.legacyUnlinkFromLibary(originalPanel); + await panelActions.legacyUnlinkFromLibrary(originalPanel); await testSubjects.existOrFail('unlinkPanelSuccess'); const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 665c5f824390c4..66ce19c3d6d4fd 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -589,7 +589,7 @@ export class DashboardPageObject extends FtrService { public async getPanelTitles() { this.log.debug('in getPanelTitles'); const titleObjects = await this.find.allByCssSelector( - '[data-test-subj=embeddablePanelTitleInner] .embPanel__titleText' + '[data-test-subj="embeddablePanelTitleInner"] .embPanel__titleText' ); return await Promise.all(titleObjects.map(async (title) => await title.getVisibleText())); } diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 26b2927fbb1d70..7c4e4cc6354d84 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -14,6 +14,7 @@ interface SaveModalArgs { dashboardId?: string; saveAsNew?: boolean; redirectToOrigin?: boolean; + description?: string; } type DashboardPickerOption = @@ -65,13 +66,27 @@ export class TimeToVisualizePageObject extends FtrService { public async setSaveModalValues( vizName: string, - { saveAsNew, redirectToOrigin, addToDashboard, dashboardId, saveToLibrary }: SaveModalArgs = {} + { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + saveToLibrary, + description, + }: SaveModalArgs = {} ) { await this.testSubjects.setValue('savedObjectTitle', vizName, { typeCharByChar: true, clearWithKeyboard: true, }); + if (description !== undefined) { + await this.testSubjects.setValue('savedObjectDescription', description, { + typeCharByChar: true, + clearWithKeyboard: true, + }); + } + const hasSaveAsNew = await this.testSubjects.exists('saveAsNewCheckbox'); if (hasSaveAsNew && saveAsNew !== undefined) { const state = saveAsNew ? 'check' : 'uncheck'; diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 521d2afe578c4f..d12e7f6b4a3fd6 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -447,7 +447,7 @@ export class VisualizePageObject extends FtrService { await this.testSubjects.setValue('savedObjectTitle', vizName); if (description) { - await this.testSubjects.setValue('viewDescription', description); + await this.testSubjects.setValue('savedObjectDescription', description); } const saveAsNewCheckboxExists = await this.testSubjects.exists('saveAsNewCheckbox'); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 49a6f0ee1cebac..3e63d526760efd 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -91,7 +91,16 @@ export class DashboardPanelActionsService extends FtrService { await this.clickContextMenuMoreItem(); } - private async navigateToEditorFromFlyout() { + async clickContextMenuItem(itemSelector: string, parent?: WebElementWrapper) { + await this.openContextMenu(parent); + const exists = await this.testSubjects.exists(itemSelector); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.click(itemSelector); + } + + async navigateToEditorFromFlyout() { await this.testSubjects.clickWhenNotDisabledWithoutRetry(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); await this.header.waitUntilLoadingHasFinished(); await this.testSubjects.click(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ); @@ -113,7 +122,8 @@ export class DashboardPanelActionsService extends FtrService { await this.common.waitForTopNavToBeVisible(); } - /** The dashboard/canvas panels can be either edited on their editor or inline. + /** + * The dashboard/canvas panels can be either edited on their editor or inline. * The inline editing panels allow the navigation to the editor after the flyout opens */ async clickEdit() { @@ -135,7 +145,8 @@ export class DashboardPanelActionsService extends FtrService { await this.common.waitForTopNavToBeVisible(); } - /** The dashboard/canvas panels can be either edited on their editor or inline. + /** + * The dashboard/canvas panels can be either edited on their editor or inline. * The inline editing panels allow the navigation to the editor after the flyout opens */ async editPanelByTitle(title?: string) { @@ -253,35 +264,20 @@ export class DashboardPanelActionsService extends FtrService { } async openInspector(parent?: WebElementWrapper) { - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); + await this.clickContextMenuItem(OPEN_INSPECTOR_TEST_SUBJ, parent); } - async legacyUnlinkFromLibary(parent?: WebElementWrapper) { + async legacyUnlinkFromLibrary(parent?: WebElementWrapper) { this.log.debug('legacyUnlinkFromLibrary'); - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.click(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ); + await this.clickContextMenuItem(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, parent); await this.testSubjects.waitForDeleted( 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION' ); } - async unlinkFromLibary(parent?: WebElementWrapper) { + async unlinkFromLibrary(parent?: WebElementWrapper) { this.log.debug('unlinkFromLibrary'); - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(UNLINK_FROM_LIBRARY_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.click(UNLINK_FROM_LIBRARY_TEST_SUBJ); + await this.clickContextMenuItem(UNLINK_FROM_LIBRARY_TEST_SUBJ, parent); await this.testSubjects.waitForDeleted( 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION' ); @@ -289,12 +285,7 @@ export class DashboardPanelActionsService extends FtrService { async legacySaveToLibrary(newTitle: string, parent?: WebElementWrapper) { this.log.debug('legacySaveToLibrary'); - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.click(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ); + await this.clickContextMenuItem(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, parent); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); @@ -308,12 +299,7 @@ export class DashboardPanelActionsService extends FtrService { async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { this.log.debug('saveToLibrary'); - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await this.clickContextMenuItem(SAVE_TO_LIBRARY_TEST_SUBJ, parent); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); diff --git a/test/functional/services/dashboard/panel_settings.ts b/test/functional/services/dashboard/panel_settings.ts index 7927d513e6e713..7bb428047698e0 100644 --- a/test/functional/services/dashboard/panel_settings.ts +++ b/test/functional/services/dashboard/panel_settings.ts @@ -124,6 +124,11 @@ export function DashboardCustomizePanelProvider({ getService, getPageObject }: F await testSubjects.click('customEmbeddablePanelHideTitleSwitch'); } + public async getCustomPanelTitle() { + log.debug('getCustomPanelTitle'); + return (await testSubjects.find('customEmbeddablePanelTitleInput')).getAttribute('value'); + } + public async setCustomPanelTitle(customTitle: string) { log.debug('setCustomPanelTitle'); await testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle, { @@ -136,6 +141,13 @@ export function DashboardCustomizePanelProvider({ getService, getPageObject }: F await testSubjects.click('resetCustomEmbeddablePanelTitleButton'); } + public async getCustomPanelDescription() { + log.debug('getCustomPanelDescription'); + return (await testSubjects.find('customEmbeddablePanelDescriptionInput')).getAttribute( + 'value' + ); + } + public async setCustomPanelDescription(customDescription: string) { log.debug('setCustomPanelDescription'); await testSubjects.setValue('customEmbeddablePanelDescriptionInput', customDescription, { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts index f872405a31ef54..1455c220e852ec 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/mappings.ts @@ -26,6 +26,9 @@ export function getDefaultProperties(field: Field): Properties { if (field.copy_to) { properties.copy_to = field.copy_to; } + if (field.store !== undefined) { + properties.store = field.store; + } return properties; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index 0ea833172e0288..7ea6fa0b692e8e 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -346,6 +346,26 @@ describe('EPM template', () => { expect(mappings).toEqual(keywordWithIndexFalseMapping); }); + it('tests processing text field with store true', () => { + const textWithStoreTrueYml = ` +- name: someTextId + type: text + store: true +`; + const textWithStoreTrueMapping = { + properties: { + someTextId: { + type: 'text', + store: true, + }, + }, + }; + const fields: Field[] = safeLoad(textWithStoreTrueYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(textWithStoreTrueMapping); + }); + it('tests processing text field with multi fields', () => { const textWithMultiFieldsLiteralYml = ` - name: textWithMultiFields @@ -1320,6 +1340,38 @@ describe('EPM template', () => { expect(mappings).toEqual(runtimeFieldMapping); }); + it('tests processing store true in a dynamic template', () => { + const textWithRuntimeFieldsLiteralYml = ` +- name: messages.* + type: text + store: true +`; + const runtimeFieldMapping = { + properties: { + messages: { + type: 'object', + dynamic: true, + }, + }, + dynamic_templates: [ + { + 'messages.*': { + match_mapping_type: 'string', + path_match: 'messages.*', + mapping: { + type: 'text', + store: true, + }, + }, + }, + ], + }; + const fields: Field[] = safeLoad(textWithRuntimeFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(runtimeFieldMapping); + }); + it('tests processing dimension fields on a dynamic template object', () => { const textWithRuntimeFieldsLiteralYml = ` - name: labels.* diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index b8ca555c95a9b7..b0f8338c89775b 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -41,6 +41,7 @@ export interface Field { default_field?: boolean; runtime?: boolean | string; subobjects?: boolean; + store?: boolean; // Fields specific of the aggregate_metric_double type metrics?: string[]; diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.test.ts index c6b1ffb8c10d77..6e955fa3928aad 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.test.ts @@ -51,7 +51,7 @@ describe('headless webgl arm mac workaround', () => { // please double-check that the --use-angle flag is still needed for arm macs // instead of --use-angle you may need --enable-gpu expect(getChromiumPackage().binaryChecksum).toBe( - '9ff994371f828a9e7ac8c69f95fd1d38b1115c438f4f94a4d75a41843ec53673' + 'e5d4ccdbf3b66532c7c0973be2d5bbd3189079646ced55fe4db61d8e7e7bfc7e' ); // just putting this here so that someone updating the chromium version will see this comment }); }); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts index 1ec89f1abe797d..21dde6af6b7de6 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts @@ -44,10 +44,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'x64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: 'd9b07faf9607cabf019282e30b81324542b259b9e1e1e85b9ac550fb680cf84d', - binaryChecksum: 'bf8ee3dcf9d4124c9dcaf3986a4ff633b2c7e12d57e06813aa7441b22935765d', + archiveChecksum: 'f69bb2f5f402aa2bdd28ccab3a9b36857d1591a1f443fa5b8865df644805cb96', + binaryChecksum: 'c6289a1a1d45021b2259ea0c9ae65ea37199452b5457831680a5e192a3f19add', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1250580, + revision: 1274550, location: 'common', archivePath: 'Mac', isPreInstalled: false, @@ -56,10 +56,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'arm64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: '8278ea0dba09f3cb8e74ed87ef0d10930c5bbc66f46711dfe82fa807a2053611', - binaryChecksum: '9ff994371f828a9e7ac8c69f95fd1d38b1115c438f4f94a4d75a41843ec53673', + archiveChecksum: '55d8cfe56d4461645a663de263ae670f78cc69b69aee16a62c47f361fa9a62f2', + binaryChecksum: 'e5d4ccdbf3b66532c7c0973be2d5bbd3189079646ced55fe4db61d8e7e7bfc7e', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1250595, // 1250580 is not available for Mac_Arm + revision: 1274557, // 1274542 is not available for Mac_Arm location: 'common', archivePath: 'Mac_Arm', isPreInstalled: false, @@ -67,22 +67,22 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'x64', - archiveFilename: 'chromium-81bc525-locales-linux_x64.zip', - archiveChecksum: 'b5d6bfca425e166d8dc15c556a935d79f1f23a4c7d52cecf16a8245c215336ce', - binaryChecksum: 'ec1c63d3509513b1324c58725c08668e3ff445f2e459ba934c1232016e27701e', + archiveFilename: 'chromium-46cf136-locales-linux_x64.zip', + archiveChecksum: '8ea5f2d72352230f7927725a6ffd6d5fb462a0c8ad76e320003748b62df6d221', + binaryChecksum: 'e7f8109ef7535dab500166b335524dfa4e92324fa31a2a61f510a5fa5afc2eee', binaryRelativePath: 'headless_shell-linux_x64/headless_shell', - revision: 1250580, + revision: 1274542, location: 'custom', isPreInstalled: true, }, { platform: 'linux', architecture: 'arm64', - archiveFilename: 'chromium-81bc525-locales-linux_arm64.zip', - archiveChecksum: 'b2ec85aa31956d272a7ab221edeea6ca41f8719ebf22f1d1853b8c4827babeaa', - binaryChecksum: 'f43490761fa34d511208abf684c0c9ee48fedbd2d0e311404779f9dc4e33549c', + archiveFilename: 'chromium-46cf136-locales-linux_arm64.zip', + archiveChecksum: '7d01fe8a78a019cf714c913d37353f85d54c4c7d4fde91d6c96605d259a66414', + binaryChecksum: 'd1a8b8708dc5ced8a9c526a0d823a4074325573f9c06fabe14e18fd3a36691c9', binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', - revision: 1250580, + revision: 1274542, location: 'custom', isPreInstalled: true, }, @@ -90,10 +90,10 @@ export class ChromiumArchivePaths { platform: 'win32', architecture: 'x64', archiveFilename: 'chrome-win.zip', - archiveChecksum: '6d1838bd84bf182e75fc31f387971b9c7ca3204ae67d4724fe34fe6a953c1662', - binaryChecksum: 'f785da29c45a5301dde6a68cb80f97fcc7233e8810db7550cfa053bc5968a61c', + archiveChecksum: 'd5e9691fb9d378ae13c8956be0d9eb45674d033e8b38125ace2f2fdd458e5ca0', + binaryChecksum: '4d8f95e4f218fc3010ab2f0d8f8674f16d554622218e73f9a7c8a22dbba2e078', binaryRelativePath: path.join('chrome-win', 'chrome.exe'), - revision: 1250580, + revision: 1274557, location: 'common', archivePath: 'Win', isPreInstalled: true, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx index b06206ff5937f0..6e3c8241136ea5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx @@ -39,9 +39,9 @@ export const AnalyzerPreviewContainer: React.FC = () => { }); // open timeline to the analyzer tab because the expandable flyout left panel Visualize => Analyzer tab is not ready - const goToAnalyzerTab = useCallback(() => { + const goToAnalyzerTab = useCallback(async () => { // open timeline - investigateInTimelineAlertClick(); + await investigateInTimelineAlertClick(); // open analyzer tab startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx index 7d4309dd6b1adc..54990dd6b67f8a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx @@ -44,9 +44,9 @@ export const SessionPreviewContainer: FC = () => { ecsRowData: dataAsNestedObject, }); - const goToSessionViewTab = useCallback(() => { + const goToSessionViewTab = useCallback(async () => { // open timeline - investigateInTimelineAlertClick(); + await investigateInTimelineAlertClick(); // open session view tab startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW }); diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_search_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_search_by_value.ts index b597eadc8a93ae..803844999a60d0 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_search_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_search_by_value.ts @@ -94,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { panels[0] ) ).to.be(true); - await dashboardPanelActions.legacyUnlinkFromLibary(panels[0]); + await dashboardPanelActions.legacyUnlinkFromLibrary(panels[0]); await testSubjects.existOrFail('unlinkPanelSuccess'); panels = await testSubjects.findAll('embeddablePanel'); expect(panels.length).to.be(1); diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index e03b5e616629a4..1eb629fdf0d35f 100644 --- a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardCustomizePanel = getService('dashboardCustomizePanel'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects([ 'common', 'dashboard', @@ -23,11 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'lens', ]); - const DASHBOARD_NAME = 'Panel Title Test'; - const CUSTOM_TITLE = 'Test Custom Title'; const EMPTY_TITLE = '[No Title]'; - const LIBRARY_TITLE_FOR_CUSTOM_TESTS = 'Library Title for Custom Title Tests'; - const LIBRARY_TITLE_FOR_EMPTY_TESTS = 'Library Title for Empty Title Tests'; describe('panel titles', () => { before(async () => { @@ -39,13 +36,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.navigateToApp(); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.saveDashboard(DASHBOARD_NAME); + await PageObjects.dashboard.saveDashboard('Panel Title Test'); + await PageObjects.lens.createAndAddLensFromDashboard({}); + }); + + beforeEach(async () => { + // close any open flyouts to prevent dirty state between tests + if (await testSubjects.exists('euiFlyoutCloseButton')) { + await testSubjects.click('euiFlyoutCloseButton'); + } }); describe('by value', () => { it('new panel by value has empty title', async () => { - await PageObjects.lens.createAndAddLensFromDashboard({}); - const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; + const [newPanelTitle] = await PageObjects.dashboard.getPanelTitles(); expect(newPanelTitle).to.equal(EMPTY_TITLE); }); @@ -58,78 +62,115 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('custom title causes unsaved changes and saving clears it', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE); + await dashboardCustomizePanel.setCustomPanelTitle('Custom title'); await dashboardCustomizePanel.clickSaveButton(); - const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(panelTitle).to.equal(CUSTOM_TITLE); + const [panelTitle] = await PageObjects.dashboard.getPanelTitles(); + expect(panelTitle).to.equal('Custom title'); await PageObjects.dashboard.clearUnsavedChanges(); }); - it('resetting title on a by value panel sets it to the empty string', async () => { - const BY_VALUE_TITLE = 'Reset Title - By Value'; + it('reset title should be hidden on a by value panel', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.setCustomPanelTitle(BY_VALUE_TITLE); + await dashboardCustomizePanel.setCustomPanelTitle('Some title'); await dashboardCustomizePanel.clickSaveButton(); + await dashboardPanelActions.customizePanel(); + expect(await testSubjects.exists('resetCustomEmbeddablePanelTitleButton')).to.be(false); + }); + it('reset description should be hidden on a by value panel', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.resetCustomPanelTitle(); + await dashboardCustomizePanel.setCustomPanelDescription('Some description'); await dashboardCustomizePanel.clickSaveButton(); - const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(panelTitle).to.equal(EMPTY_TITLE); - await PageObjects.dashboard.clearUnsavedChanges(); + await dashboardPanelActions.customizePanel(); + expect(await testSubjects.exists('resetCustomEmbeddablePanelDescriptionButton')).to.be( + false + ); }); }); - describe('by reference', () => { + describe('nick by reference', () => { + const VIS_LIBRARY_DESCRIPTION = 'Vis library description'; + + let count = 0; + const getVisTitle = (increment = false) => + `Vis Library Title - ${increment ? ++count : count}`; + it('linking a by value panel with a custom title to the library will overwrite the custom title with the library title', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE); + await dashboardCustomizePanel.setCustomPanelTitle('Custom title'); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacySaveToLibrary(LIBRARY_TITLE_FOR_CUSTOM_TESTS); - await retry.try(async () => { + await dashboardPanelActions.legacySaveToLibrary(getVisTitle(true)); + await retry.tryForTime(500, async () => { // need to surround in 'retry' due to delays in HTML updates causing the title read to be behind - const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_CUSTOM_TESTS); + const [newPanelTitle] = await PageObjects.dashboard.getPanelTitles(); + expect(newPanelTitle).to.equal(getVisTitle()); }); }); it('resetting title on a by reference panel sets it to the library title', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.setCustomPanelTitle('This should go away'); + await dashboardCustomizePanel.setCustomPanelTitle('Custom Title'); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.resetCustomPanelTitle(); await dashboardCustomizePanel.clickSaveButton(); - const resetPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(resetPanelTitle).to.equal(LIBRARY_TITLE_FOR_CUSTOM_TESTS); + await dashboardPanelActions.customizePanel(); + expect(await dashboardCustomizePanel.getCustomPanelTitle()).to.equal(getVisTitle()); + }); + + it('resetting description on a by reference panel sets it to the library title', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.navigateToEditorFromFlyout(); + // legacySaveToLibrary UI cannot set description + await PageObjects.lens.save( + getVisTitle(true), + false, + undefined, + undefined, + undefined, + undefined, + VIS_LIBRARY_DESCRIPTION + ); + + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.setCustomPanelDescription('Custom description'); + await dashboardCustomizePanel.clickSaveButton(); + + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.resetCustomPanelDescription(); + await dashboardCustomizePanel.clickSaveButton(); + + await dashboardPanelActions.customizePanel(); + expect(await dashboardCustomizePanel.getCustomPanelDescription()).to.equal( + VIS_LIBRARY_DESCRIPTION + ); }); it('unlinking a by reference panel with a custom title will keep the current title', async () => { await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE); + await dashboardCustomizePanel.setCustomPanelTitle('Custom title'); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacyUnlinkFromLibary(); - const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(newPanelTitle).to.equal(CUSTOM_TITLE); + await dashboardPanelActions.legacyUnlinkFromLibrary(); + const [newPanelTitle] = await PageObjects.dashboard.getPanelTitles(); + expect(newPanelTitle).to.equal('Custom title'); }); it("linking a by value panel with a blank title to the library will set the panel's title to the library title", async () => { await dashboardPanelActions.customizePanel(); await dashboardCustomizePanel.setCustomPanelTitle(''); await dashboardCustomizePanel.clickSaveButton(); - await dashboardPanelActions.legacySaveToLibrary(LIBRARY_TITLE_FOR_EMPTY_TESTS); - await retry.try(async () => { + await dashboardPanelActions.legacySaveToLibrary(getVisTitle(true)); + await retry.tryForTime(500, async () => { // need to surround in 'retry' due to delays in HTML updates causing the title read to be behind - const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_EMPTY_TESTS); + const [newPanelTitle] = await PageObjects.dashboard.getPanelTitles(); + expect(newPanelTitle).to.equal(getVisTitle()); }); }); it('unlinking a by reference panel without a custom title will keep the library title', async () => { - await dashboardPanelActions.legacyUnlinkFromLibary(); - const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; - expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_EMPTY_TESTS); + await dashboardPanelActions.legacyUnlinkFromLibrary(); + const [newPanelTitle] = await PageObjects.dashboard.getPanelTitles(); + expect(newPanelTitle).to.equal(getVisTitle()); }); }); }); diff --git a/x-pack/test/functional/apps/lens/group4/dashboard.ts b/x-pack/test/functional/apps/lens/group4/dashboard.ts index 773a9a04e646b0..33e3277855d2fb 100644 --- a/x-pack/test/functional/apps/lens/group4/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/dashboard.ts @@ -242,7 +242,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.closeAddPanel(); const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); - await panelActions.legacyUnlinkFromLibary(originalPanel); + await panelActions.legacyUnlinkFromLibrary(originalPanel); await testSubjects.existOrFail('unlinkPanelSuccess'); const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); diff --git a/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js index 45b17547221536..a14d80bd080ecd 100644 --- a/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js +++ b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }) { it('unlink map panel from embeddable library', async () => { const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); - await dashboardPanelActions.unlinkFromLibary(originalPanel); + await dashboardPanelActions.unlinkFromLibrary(originalPanel); await testSubjects.existOrFail('unlinkPanelSuccess'); const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 8b8f84adfa55e9..5e27c50848051e 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -746,7 +746,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont redirectToOrigin?: boolean, saveToLibrary?: boolean, addToDashboard?: 'new' | 'existing' | null, - dashboardId?: string + dashboardId?: string, + description?: string ) { await PageObjects.timeToVisualize.setSaveModalValues(title, { saveAsNew, @@ -754,6 +755,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont addToDashboard: addToDashboard ? addToDashboard : null, dashboardId, saveToLibrary, + description, }); await testSubjects.click('confirmSaveSavedObjectButton'); @@ -774,7 +776,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont redirectToOrigin?: boolean, saveToLibrary?: boolean, addToDashboard?: 'new' | 'existing' | null, - dashboardId?: string + dashboardId?: string, + description?: string ) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('lnsApp_saveButton'); @@ -785,7 +788,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont redirectToOrigin, saveToLibrary, addToDashboard, - dashboardId + dashboardId, + description ); }, diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts index 4087aeb465fa9f..39253534c54c7c 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Convert to by-value const byRefPanel = await testSubjects.find('embeddablePanelHeading-' + lensTitle); - await dashboardPanelActions.legacyUnlinkFromLibary(byRefPanel); + await dashboardPanelActions.legacyUnlinkFromLibrary(byRefPanel); await PageObjects.dashboard.waitForRenderComplete(); const byValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); diff --git a/yarn.lock b/yarn.lock index 2ebe8e341b81c3..efc5192e28f3da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7721,10 +7721,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@puppeteer/browsers@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.1.0.tgz#2683d3c908ecfc9af6b63111b5037679d3cebfd8" - integrity sha512-xloWvocjvryHdUjDam/ZuGMh7zn4Sn3ZAaV4Ah2e2EwEt90N3XphZlSsU3n0VDc1F7kggCjMuH0UuxfPQ5mD9w== +"@puppeteer/browsers@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.2.3.tgz#ad6b79129c50825e77ddaba082680f4dad0b674e" + integrity sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ== dependencies: debug "4.3.4" extract-zip "2.0.1" @@ -13581,13 +13581,14 @@ chromedriver@^123.0.3: proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" -chromium-bidi@0.5.10: - version "0.5.10" - resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.5.10.tgz#03ac8381e6a0d6be67886d27f77777ef3a978429" - integrity sha512-4hsPE1VaLLM/sgNK/SlLbI24Ra7ZOuWAjA3rhw1lVCZ8ZiUgccS6cL5L/iqo4hjRcl5vwgYJ8xTtbXdulA9b6Q== +chromium-bidi@0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.5.19.tgz#e4f4951b7d9b20d668d6b387839f7b7bf2d69ef4" + integrity sha512-UA6zL77b7RYCjJkZBsZ0wlvCTD+jTjllZ8f6wdO4buevXgTZYjV+XLB9CiEa2OuuTGGTLnI7eN9I60YxuALGQg== dependencies: mitt "3.0.1" urlpattern-polyfill "10.0.0" + zod "3.22.4" ci-info@^2.0.0: version "2.0.0" @@ -14368,13 +14369,6 @@ cronstrue@^1.51.0: resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.51.0.tgz#7a63153d61d940344049037628da38a60784c8e2" integrity sha512-fSRAz/MV0TRjeNZKAsovmH/MSsly7+8np4XsfsrjOOz7sjxLrE9SmedRYAs3nPAtLLC5UsMpvenjXYRz463bMA== -cross-fetch@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" - integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== - dependencies: - node-fetch "^2.6.12" - cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -15537,10 +15531,10 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" -devtools-protocol@0.0.1249869: - version "0.0.1249869" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz#000c3cf1afc189a18db98135a50d4a8f95a47d29" - integrity sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg== +devtools-protocol@0.0.1273771: + version "0.0.1273771" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1273771.tgz#46aeb5db41417e2c2ad3d8367c598c975290b1a5" + integrity sha512-QDbb27xcTVReQQW/GHJsdQqGKwYBE7re7gxehj467kKP2DKuYBUj6i2k5LRiAC66J1yZG/9gsxooz/s9pcm0Og== dezalgo@^1.0.0, dezalgo@^1.0.4: version "1.0.4" @@ -23308,7 +23302,7 @@ node-fetch-h2@^2.3.0: dependencies: http2-client "^1.2.5" -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: +node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -25528,26 +25522,26 @@ pupa@^3.1.0: dependencies: escape-goat "^4.0.0" -puppeteer-core@22.3.0: - version "22.3.0" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-22.3.0.tgz#89fa75bbbcfa2927e3045c69253cc676f7bda728" - integrity sha512-Ho5Vdpdro05ZyCx/l5Hkc5vHiibKTaY37fIAD9NF9Gi/vDxkVTeX40U/mFnEmeoxyuYALvWCJfi7JTT82R6Tuw== +puppeteer-core@22.8.1: + version "22.8.1" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-22.8.1.tgz#757ec8983ca38486dad8e5464e744f4b8aff5a13" + integrity sha512-m1F6ZSTw1xrJ6xD4B+HonkSNVQmMrRMaqca/ivRcZYJ6jqzOnfEh3QgO9HpNPj6heiAZ2+4IPAU3jdZaTIDnSA== dependencies: - "@puppeteer/browsers" "2.1.0" - chromium-bidi "0.5.10" - cross-fetch "4.0.0" + "@puppeteer/browsers" "2.2.3" + chromium-bidi "0.5.19" debug "4.3.4" - devtools-protocol "0.0.1249869" - ws "8.16.0" + devtools-protocol "0.0.1273771" + ws "8.17.0" -puppeteer@22.3.0: - version "22.3.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-22.3.0.tgz#4ea1f1c8c5a527b0e4ca21a242af7d3d9b89e294" - integrity sha512-GC+tyjzYKjaNjhlDAuqRgDM+IOsqOG75Da4L28G4eULNLLxKDt+79x2OOSQ47HheJBgGq7ATSExNE6gayxP6cg== +puppeteer@22.8.1: + version "22.8.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-22.8.1.tgz#d0b96cd722f62a157804dcc3b0d4909e3620bf1d" + integrity sha512-CFgPSKV+iydjO/8/hJVj251Hqp2PLcIa70j6H7sYqkwM8YJ+D3CA74Ufuj+yKtvDIntQPB/nLw4EHrHPcHOPjw== dependencies: - "@puppeteer/browsers" "2.1.0" + "@puppeteer/browsers" "2.2.3" cosmiconfig "9.0.0" - puppeteer-core "22.3.0" + devtools-protocol "0.0.1273771" + puppeteer-core "22.8.1" pure-rand@^6.0.0: version "6.0.2" @@ -31814,10 +31808,10 @@ write-file-atomic@^4.0.1, write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@8.16.0, ws@>=8.14.2, ws@^8.2.3, ws@^8.4.2, ws@^8.9.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== +ws@8.17.0, ws@>=8.14.2, ws@^8.2.3, ws@^8.4.2, ws@^8.9.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== ws@^7.3.1, ws@^7.4.2: version "7.5.9" @@ -32127,7 +32121,7 @@ zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz#3646e81cfc318dbad2a22519e5ce661615418673" integrity sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q== -zod@^3.22.3, zod@^3.22.4: +zod@3.22.4, zod@^3.22.3, zod@^3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==