From 64347285b270dec93dff634501519260795cc1b3 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 10 Jun 2024 15:55:08 +0200 Subject: [PATCH 01/14] fix: [Obs Synthetics > Overview][KEYBOARD]: Management table rows have a button that takes keyboard focus but does not change the UI (#184132) Closes: https://github.com/elastic/observability-dev/issues/3438 Closes: https://github.com/elastic/observability-dev/issues/3437 Closes: https://github.com/elastic/observability-dev/issues/3441 ## Description The Obs Synthetics > Overview page, Management table rows contain a button that takes keyboard focus but doesn't do anything, and should be updated to a non-interactive element. Screenshot attached below. ### Steps to recreate 1. Open the [Obs Synthetics](https://keepserverless-qa-oblt-b4ba07.kb.eu-west-1.aws.qa.elastic.cloud/app/synthetics) view 2. Click the `Create Monitor` button 3. Create a simple monitor. I added a simple journey for "https://example.com" 4. Click the `Management` tab 5. Press `Tab` to move through the table 6. Verify the Locations column labels will take focus on the colored circle but nothing happens on click or keypress ### What was done?: 1. `location_status_badges.tsx` component was updated. Removed empty `iconOnClick` prop, `aria-label` attribute was set ### Screen: https://github.com/elastic/kibana/assets/20072247/08098ed0-b35e-4056-b3e8-bd4debb1bf37 ### a11y tree ![image](https://github.com/elastic/kibana/assets/20072247/3bcd23a9-1deb-454e-a18f-90a8e9d928d0) --- .../common/components/location_status_badges.tsx | 14 ++++++-------- .../plugins/translations/translations/fr-FR.json | 1 - .../plugins/translations/translations/ja-JP.json | 1 - .../plugins/translations/translations/zh-CN.json | 1 - 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx index 8ba6fb3bc54ff..23009ef9067d1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/location_status_badges.tsx @@ -120,11 +120,13 @@ const MonitorDetailLinkForLocation = ({ } color="hollow" - iconOnClickAriaLabel={CLICK_LOCATION_LABEL} - iconOnClick={() => { - // Empty - }} href={monitorDetailLinkUrl ?? '/'} + aria-label={i18n.translate('xpack.synthetics.management.location.ariaLabel', { + defaultMessage: 'View details for {locationLabel} location', + values: { + locationLabel, + }, + })} > {locationLabel} @@ -144,7 +146,3 @@ const COLLAPSE_LOCATIONS_LABEL = i18n.translate( defaultMessage: 'Click to collapse locations', } ); - -const CLICK_LOCATION_LABEL = i18n.translate('xpack.synthetics.management.location.clickMessage', { - defaultMessage: 'Click to view details for this location.', -}); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index cda07fb993519..09336d56965fa 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -40534,7 +40534,6 @@ "xpack.synthetics.management.enableLabel": "Activer", "xpack.synthetics.management.enableMonitorLabel": "Activer le moniteur", "xpack.synthetics.management.enableStatusAlert": "Activer les alertes de statut", - "xpack.synthetics.management.location.clickMessage": "Cliquez pour afficher les détails de cet emplacement.", "xpack.synthetics.management.monitorDeleteFailureMessage": "Impossible de supprimer le moniteur. Réessayez plus tard.", "xpack.synthetics.management.monitorDeleteLoadingMessage": "Suppression du moniteur...", "xpack.synthetics.management.monitorDeleteSuccessMessage": "Moniteur supprimé.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1695925ab11e0..55589d731e969 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -40507,7 +40507,6 @@ "xpack.synthetics.management.enableLabel": "有効にする", "xpack.synthetics.management.enableMonitorLabel": "モニターを有効にする", "xpack.synthetics.management.enableStatusAlert": "ステータスアラートを有効にする", - "xpack.synthetics.management.location.clickMessage": "クリックすると、この場所の詳細が表示されます。", "xpack.synthetics.management.monitorDeleteFailureMessage": "モニターを削除できませんでした。しばらくたってから再試行してください。", "xpack.synthetics.management.monitorDeleteLoadingMessage": "モニターを削除しています...", "xpack.synthetics.management.monitorDeleteSuccessMessage": "モニターが正常に削除されました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3c84783042261..65cf2dad16d19 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -40553,7 +40553,6 @@ "xpack.synthetics.management.enableLabel": "启用", "xpack.synthetics.management.enableMonitorLabel": "启用监测", "xpack.synthetics.management.enableStatusAlert": "启用状态告警", - "xpack.synthetics.management.location.clickMessage": "单击可查看此位置的详情。", "xpack.synthetics.management.monitorDeleteFailureMessage": "无法删除监测。请稍后重试。", "xpack.synthetics.management.monitorDeleteLoadingMessage": "正在删除监测......", "xpack.synthetics.management.monitorDeleteSuccessMessage": "已成功删除监测。", From ad83c672f1708f3d5719cd0ddac82fdc8f29f491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:01:55 +0200 Subject: [PATCH 02/14] [Console Monaco migration] Trigger suggestions on deletion (#184673) ## Summary Closes https://github.com/elastic/kibana/issues/183422 This PR adds logic to manually trigger autocomplete suggestions when the user deletes some of the text in the editor. The suggestions are only triggered if the line content **before** the cursor position matches specific regular expressions. The use cases include: - when the string is empty -> suggest autocomplete for methods - when the string contains a method and a whitespace -> suggest autocomplete for a url path - when the string contains a method and some parts of the url and ends with `/`, `?`or `=`-> suggest autocomplete for a url path or url params - when the string contains only a double quote -> suggest autocomplete for body params - when the string contains a property and a colon -> suggest autocomplete for body param values ### Screen recording https://github.com/elastic/kibana/assets/6585477/503981b7-51d0-4f2c-aeeb-ddcf8c923aa9 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../monaco_editor_actions_provider.test.ts | 1 + .../monaco/monaco_editor_actions_provider.ts | 53 +++++++++++- .../monaco/utils/autocomplete_utils.test.ts | 73 +++++++++++++++- .../editor/monaco/utils/autocomplete_utils.ts | 86 ++++++++++++++----- .../editor/monaco/utils/constants.ts | 26 ++++++ .../containers/editor/monaco/utils/index.ts | 1 + 6 files changed, 212 insertions(+), 28 deletions(-) diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts index b294b1d8de865..b2bcbb70bcfab 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.test.ts @@ -65,6 +65,7 @@ describe('Editor actions provider', () => { onDidScrollChange: jest.fn(), onDidChangeCursorSelection: jest.fn(), onDidContentSizeChange: jest.fn(), + onKeyUp: jest.fn(), getSelection: jest.fn(), getPosition: jest.fn(), getTopForLineNumber: jest.fn(), 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 f0bc4b34d899b..a364e8db9ce6d 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 @@ -20,6 +20,7 @@ import { Actions } from '../../../stores/request'; import { AutocompleteType, containsUrlParams, + getAutoIndentedRequests, getBodyCompletionItems, getCurlRequest, getDocumentationLinkFromAutocomplete, @@ -31,9 +32,9 @@ import { getUrlPathCompletionItems, replaceRequestVariables, SELECTED_REQUESTS_CLASSNAME, + shouldTriggerSuggestions, stringifyRequest, trackSentRequests, - getAutoIndentedRequests, } from './utils'; import type { AdjustedParsedRequest } from './types'; @@ -41,6 +42,10 @@ import { StorageQuotaError } from '../../../components/storage_quota_error'; import { ContextValue } from '../../../contexts'; const AUTO_INDENTATION_ACTION_LABEL = 'Apply indentations'; +const TRIGGER_SUGGESTIONS_ACTION_LABEL = 'Trigger suggestions'; +const TRIGGER_SUGGESTIONS_HANDLER_ID = 'editor.action.triggerSuggest'; +const DEBOUNCE_HIGHLIGHT_WAIT_MS = 200; +const DEBOUNCE_AUTOCOMPLETE_WAIT_MS = 500; export class MonacoEditorActionsProvider { private parsedRequestsProvider: ConsoleParsedRequestsProvider; @@ -53,11 +58,26 @@ export class MonacoEditorActionsProvider { this.highlightedLines = this.editor.createDecorationsCollection(); this.editor.focus(); - const debouncedHighlightRequests = debounce(() => this.highlightRequests(), 200, { - leading: true, - }); + const debouncedHighlightRequests = debounce( + () => this.highlightRequests(), + DEBOUNCE_HIGHLIGHT_WAIT_MS, + { + leading: true, + } + ); debouncedHighlightRequests(); + const debouncedTriggerSuggestions = debounce( + () => { + this.triggerSuggestions(); + }, + DEBOUNCE_AUTOCOMPLETE_WAIT_MS, + { + leading: false, + trailing: true, + } + ); + // init all listeners editor.onDidChangeCursorPosition(async (event) => { await debouncedHighlightRequests(); @@ -71,6 +91,13 @@ export class MonacoEditorActionsProvider { editor.onDidContentSizeChange(async (event) => { await debouncedHighlightRequests(); }); + + editor.onKeyUp((event) => { + // trigger autocomplete on backspace + if (event.keyCode === monaco.KeyCode.Backspace) { + debouncedTriggerSuggestions(); + } + }); } private updateEditorActions(lineNumber?: number) { @@ -590,4 +617,22 @@ export class MonacoEditorActionsProvider { public getCurrentPosition(): monaco.IPosition { return this.editor.getPosition() ?? { lineNumber: 1, column: 1 }; } + + private triggerSuggestions() { + const model = this.editor.getModel(); + const position = this.editor.getPosition(); + if (!model || !position) { + return; + } + const lineContentBefore = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + // if the line is empty or it matches specified regex, trigger suggestions + if (!lineContentBefore.trim() || shouldTriggerSuggestions(lineContentBefore)) { + this.editor.trigger(TRIGGER_SUGGESTIONS_ACTION_LABEL, TRIGGER_SUGGESTIONS_HANDLER_ID, {}); + } + } } 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 index 9755bf4e8f442..c5a423e38902d 100644 --- 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 @@ -18,7 +18,10 @@ jest.mock('../../../../../lib/autocomplete/engine', () => { }; }); import { AutoCompleteContext } from '../../../../../lib/autocomplete/types'; -import { getDocumentationLinkFromAutocomplete } from './autocomplete_utils'; +import { + getDocumentationLinkFromAutocomplete, + shouldTriggerSuggestions, +} from './autocomplete_utils'; describe('autocomplete_utils', () => { describe('getDocumentationLinkFromAutocomplete', () => { @@ -65,4 +68,72 @@ describe('autocomplete_utils', () => { expect(link).toBe(expectedLink); }); }); + + describe('shouldTriggerSuggestions', () => { + it('triggers suggestions for the beginning of the url after a method', () => { + const actual = shouldTriggerSuggestions('GET '); + expect(actual).toBe(true); + }); + it('triggers suggestions for the url part', () => { + const actual = shouldTriggerSuggestions('GET _search/'); + expect(actual).toBe(true); + }); + it('triggers suggestions for the 2nd url part', () => { + const actual = shouldTriggerSuggestions('GET _search/test1/'); + expect(actual).toBe(true); + }); + it('triggers no suggestions for the url if not at the slash', () => { + const actual = shouldTriggerSuggestions('GET _search'); + expect(actual).toBe(false); + }); + it('triggers suggestions for the url params', () => { + const actual = shouldTriggerSuggestions('GET _search?'); + expect(actual).toBe(true); + }); + + it('triggers no suggestions for the url params when the param name is typed', () => { + const actual = shouldTriggerSuggestions('GET _search?test'); + expect(actual).toBe(false); + }); + it('triggers suggestions for the url param value', () => { + const actual = shouldTriggerSuggestions('GET _search?test='); + expect(actual).toBe(true); + }); + it('triggers suggestions for the url param value (index name with - and numbers)', () => { + const actual = shouldTriggerSuggestions('GET .test-index-01/_search?test='); + expect(actual).toBe(true); + }); + it('triggers no suggestions for the url param value when the value is typed', () => { + const actual = shouldTriggerSuggestions('GET _search?test=value'); + expect(actual).toBe(false); + }); + it('triggers suggestions for the 2nd url param', () => { + const actual = shouldTriggerSuggestions('GET _search?param1=value1¶m2='); + expect(actual).toBe(true); + }); + it('triggers suggestions for the property name', () => { + const actual = shouldTriggerSuggestions(' "'); + expect(actual).toBe(true); + }); + it('triggers no suggestions for the property name when the property name is typed', () => { + const actual = shouldTriggerSuggestions('"propertyName'); + expect(actual).toBe(false); + }); + it('triggers suggestions for the property value', () => { + const actual = shouldTriggerSuggestions(' "propertyName": '); + expect(actual).toBe(true); + }); + it('triggers suggestions for the property value with a double quote', () => { + const actual = shouldTriggerSuggestions(' "propertyName": "'); + expect(actual).toBe(true); + }); + it('triggers no suggestions for the property value when the value is typed (string)', () => { + const actual = shouldTriggerSuggestions(' "propertyName": "value'); + expect(actual).toBe(false); + }); + it('triggers no suggestions for the property value when the value is typed (number)', () => { + const actual = shouldTriggerSuggestions(' "propertyName": 5'); + expect(actual).toBe(false); + }); + }); }); 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 index 9f8a6e5efd99c..2eaf183058f75 100644 --- 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 @@ -22,7 +22,15 @@ import { 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'; +import { + END_OF_URL_TOKEN, + i18nTexts, + methodWhitespaceRegex, + methodWithUrlRegex, + newLineRegex, + propertyNameRegex, + propertyValueRegex, +} from './constants'; /* * This function initializes the autocomplete context for the request @@ -50,6 +58,11 @@ export const getDocumentationLinkFromAutocomplete = ( } return null; }; +/* + * Helper function that filters out suggestions without a name. + */ +const filterTermsWithoutName = (terms: ResultTerm[]): ResultTerm[] => + terms.filter((term) => term.name !== undefined); /* * This function returns an array of completion items for the request method @@ -132,9 +145,7 @@ export const getUrlPathCompletionItems = ( }; if (autoCompleteSet && autoCompleteSet.length > 0) { return ( - autoCompleteSet - // filter autocomplete items without a name - .filter(({ name }) => Boolean(name)) + filterTermsWithoutName(autoCompleteSet) // map autocomplete items to completion items .map((item) => { return { @@ -194,9 +205,7 @@ export const getUrlParamsCompletionItems = ( endColumn: position.column, }; return ( - context.autoCompleteSet - // filter autocomplete items without a name - .filter(({ name }) => Boolean(name)) + filterTermsWithoutName(context.autoCompleteSet) // map autocomplete items to completion items .map((item) => { return { @@ -238,9 +247,9 @@ export const getBodyCompletionItems = async ( endLineNumber: lineNumber, endColumn: column, }; - const bodyContent = model.getValueInRange(bodyRange); + const bodyContentBeforePosition = model.getValueInRange(bodyRange); - const bodyTokens = parseBody(bodyContent); + const bodyTokens = parseBody(bodyContentBeforePosition); // needed for scope linking + global term resolving context.endpointComponentResolver = getEndpointBodyCompleteComponents; context.globalComponentResolver = getGlobalAutocompleteComponents; @@ -256,12 +265,18 @@ export const getBodyCompletionItems = async ( if (!context) { return []; } + // loading async suggestions if (context.asyncResultsState?.isLoading && context.asyncResultsState) { const results = await context.asyncResultsState.results; - return getSuggestions(model, position, results, context, bodyContent); + return getSuggestions(model, position, results, context, bodyContentBeforePosition); } - - return getSuggestions(model, position, context.autoCompleteSet ?? [], context, bodyContent); + return getSuggestions( + model, + position, + context.autoCompleteSet ?? [], + context, + bodyContentBeforePosition + ); }; const getSuggestions = ( @@ -269,18 +284,23 @@ const getSuggestions = ( position: monaco.Position, autocompleteSet: ResultTerm[], context: AutoCompleteContext, - bodyContent: string + bodyContentBeforePosition: string ) => { const wordUntilPosition = model.getWordUntilPosition(position); - // if there is " after the cursor, replace it - let endColumn = position.column; - const charAfterPosition = model.getValueInRange({ + const lineContentAfterPosition = model.getValueInRange({ startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, - endColumn: position.column + 1, + endColumn: model.getLineMaxColumn(position.lineNumber), }); - if (charAfterPosition === '"') { + // if the rest of the line is empty or there is only " + // then template can be inserted, otherwise only name + context.addTemplate = isEmptyOrDoubleQuote(lineContentAfterPosition); + + // if there is " after the cursor, include it in the insert range + let endColumn = position.column; + + if (lineContentAfterPosition.startsWith('"')) { endColumn = endColumn + 1; } const range = { @@ -291,15 +311,13 @@ const getSuggestions = ( endColumn, }; return ( - autocompleteSet - // filter out items that don't have name - .filter(({ name }) => name !== undefined) + filterTermsWithoutName(autocompleteSet) // map autocomplete items to completion items .map((item) => { const suggestion = { // convert name to a string label: item.name + '', - insertText: getInsertText(item, bodyContent, context), + insertText: getInsertText(item, bodyContentBeforePosition, context), detail: i18nTexts.api, // the kind is only used to configure the icon kind: monaco.languages.CompletionItemKind.Constant, @@ -335,7 +353,7 @@ const getInsertText = ( if (conditionalTemplate) { template = conditionalTemplate; } - if (template !== undefined) { + if (template !== undefined && context.addTemplate) { let templateLines; const { __raw, value: templateValue } = template; if (__raw && templateValue) { @@ -388,3 +406,25 @@ const getConditionalTemplate = ( return matchedRule.__template; } }; + +/* + * This function checks the content of the line before the cursor and decides if the autocomplete + * suggestions should be triggered + */ +export const shouldTriggerSuggestions = (lineContent: string): boolean => { + return ( + methodWhitespaceRegex.test(lineContent) || + methodWithUrlRegex.test(lineContent) || + propertyNameRegex.test(lineContent) || + propertyValueRegex.test(lineContent) + ); +}; + +/* + * This function checks if the content of the line after the cursor is either empty + * or it only has a double quote. + */ +export const isEmptyOrDoubleQuote = (lineContent: string): boolean => { + lineContent = lineContent.trim(); + return !lineContent || lineContent === '"'; +}; 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 index c0616fc4dc0a1..c4ba20fef43f9 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/constants.ts @@ -22,8 +22,34 @@ export const questionMarkRegex = /\?/; export const variableTemplateRegex = /\${(\w+)}/g; export const numberStartRegex = /[-\d]/; export const digitRegex = /[\d]/; +export const lettersRegex = /[A-Za-z]/; export const END_OF_URL_TOKEN = '__url_path_end__'; +/* + * This regex matches a string that has a method with a whitespace after it, for example "GET ". + * In this case autocomplete suggestions should be triggered for an url. + */ +export const methodWhitespaceRegex = /^\s*(GET|POST|PUT|PATCH|DELETE)\s+$/i; +/* + * This regex matches a string that has + * a method and some parts of an url ending with a slash, a question mark or an equals sign, + * for example "GET _search/", "GET _search?", "GET _search?param=". + * In this case autocomplete suggestions should be triggered for an url part or param. + */ +export const methodWithUrlRegex = /^\s*(GET|POST|PUT|PATCH|DELETE)\s+[a-z0-9\/._\-?=&]*[?=\/]$/i; +/* + * This regex matches a string that has + * optional whitespace characters and a double quote, for example ` "`. + * In this case autocomplete suggestions should be triggered for a property name. + */ +export const propertyNameRegex = /^\s*"$/; +/* + * This regex matches a string that has + * a property name, a colon and an optional double quote, for example `"query" : "`. + * In this case autocomplete suggestions should be triggered for a property value. + */ +export const propertyValueRegex = /^\s*"[a-zA-Z0-9_]+"\s*:\s*"?$/; + /* * i18n for autocomplete labels */ 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 index a0de7b461e99a..069f99552222a 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts @@ -22,5 +22,6 @@ export { getUrlPathCompletionItems, getUrlParamsCompletionItems, getBodyCompletionItems, + shouldTriggerSuggestions, } from './autocomplete_utils'; export { getLineTokens, containsUrlParams } from './tokens_utils'; From 59912e8641effbf8b1eacfda8e83635696cfbfb7 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Mon, 10 Jun 2024 11:23:08 -0300 Subject: [PATCH 03/14] [Discover] One Discover context awareness (#183797) ## Summary This PR includes the initial implementation of the Discover contextual awareness framework and composable profiles: ![logs_table](https://github.com/elastic/kibana/assets/25592674/0815687a-c4d8-4a80-8f67-5e1de0c65adf) ### Context We currently support three levels of context in Discover: - Root context: - Based on the current solution type, navigational parameters, etc. - Resolved at application initialization and on parameter changes. - Runs synchronously or asynchronously. - Data source context: - Based on the current ES|QL query or data view. - Resolved on ES|QL query or data view change, before data fetching occurs. - Runs synchronously or asynchronously. - Document context: - Based on individual ES|QL records or ES documents. - Resolved individually for each ES|QL record or ES document after data fetching runs. - Runs synchronously only. ### Composable profiles To support application extensibility based on context, we've introduced the concept of "composable profiles". Composable profiles are implementations of a core `Profile` interface (or a subset of it) containing all of the available extension points Discover supports. A composable profile can be implemented at any context level through a "profile provider", responsible for defining the composable profile and its associated context resolution method. The context resolution method, named `resolve`, determines if its composable profile is a match for the current Discover context, and returns related metadata in a `context` object. ### Merged accessors Composable profiles operate similarly to middleware in that each of their extension point implementations are passed a `prev` argument, which can be called to access the results from profiles at previous context levels, and allows overwriting or composing a final result from the previous results. The method Discover calls to trigger the extension point merging process and obtain a final result from the combined profiles is referred to as a "merged accessor". The following diagram illustrates the extension point merging process: ![image](https://github.com/davismcphee/kibana/assets/25592674/59f7cd23-c1e0-4d8e-99ed-02460211ed96) ### Supporting services The contextual awareness framework is driven by two main supporting services called `ProfileService` and `ProfilesManager`. Each context level has a dedicated profile service, e.g. `RootProfileService`, which is responsible for accepting profile provider registrations and running through each provider in order during context resolution to identify a matching profile. A single `ProfilesManager` is instantiated on Discover load, or one per saved search panel in a dashboard. The profiles manager is responsible for the following: - Managing state associated with the current Discover context. - Coordinating profile services and exposing resolution methods for each context level. - Providing access to the combined set of resolved profiles. - Deduplicating profile resolution attempts with identical parameters. - Error handling and fallback behaviour on profile resolution failure. ### Bringing it all together The following diagram models the overall Discover contextual awareness framework and how each of the above concepts come together: ![image](https://github.com/elastic/kibana/assets/25592674/49193141-c50a-473f-9d38-eb09fbaaffbe) ### Followup work - We'll want to add developer documentation as a followup, which I've created an issue for here: #184698. The summary for this PR can be used as the basis for the documentation. - Since we currently have no profile or extension point implementations, this PR does not include any functional tests. We should create example implementations for functional testing and ensure they're only enabled when running the test suite or when developers want them enabled. I've created a followup issue for this work here: #184699. ### Testing notes Testing the framework is tricky since we have no actual profile or extension point implementations yet. However, I previously added example implementations that I was using for testing while building the framework. I've removed the example implementations so they don't get merged, but they can be temporarily restored for testing by reverting the commit where I removed them: `git revert 5752651f474d99dfbdecfe9d869377b9edaf7c62`. You'll also need to uncomment the following lines in `src/plugins/discover/public/plugin.tsx`: https://github.com/elastic/kibana/blob/ce85a6a35fa3623bfdfac7dae41df2d840394154/src/plugins/discover/public/plugin.tsx#L458-L463 To test the root profile resolution based on solution type, I'd recommend enabling the solution nav locally by adding the following to `kibana.dev.yml`: ```yml xpack.cloud_integrations.experiments.enabled: true xpack.cloud_integrations.experiments.flag_overrides: "solutionNavEnabled": true xpack.cloud.id: "ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=" xpack.cloud.base_url: "https://cloud.elastic.co" xpack.cloud.deployment_url: "/deployments/deploymentId" ``` In order to change the active solution type, modify the `mockSpaceState` in `src/plugins/navigation/public/plugin.tsx`: https://github.com/elastic/kibana/blob/79e51d64f83da6af56107a633a5a3b49947f1ebe/src/plugins/navigation/public/plugin.tsx#L159-L162 For test data, I'd recommend running the following commands to generate sample ECS compliant logs and metrics data: ``` node scripts/synthtrace.js --target "http://elastic:changeme@localhost:9200/" --kibana "http://elastic:changeme@localhost:5601/" --live --logLevel debug simple_logs.ts node scripts/synthtrace.js --target "http://elastic:changeme@localhost:9200/" --kibana "http://elastic:changeme@localhost:5601/" --live --logLevel debug simple_trace.ts ``` And lastly a couple of the ES|QL queries I used for testing: ``` // resolves to the example logs data source context from logs-synth-default // mixed dataset that falls back to vanilla Discover // helpful for testing document context in the doc viewer flyout from logs-synth-default,metrics-* ``` Resolves #181962. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/utils/build_data_record.test.ts | 12 + .../src/utils/build_data_record.ts | 10 +- .../discover/public/__mocks__/services.ts | 3 + .../layout/discover_documents.test.tsx | 23 ++ .../components/layout/discover_documents.tsx | 125 +++++---- .../main/data_fetching/fetch_all.ts | 13 +- .../data_fetching/fetch_documents.test.ts | 11 + .../main/data_fetching/fetch_documents.ts | 4 +- .../main/data_fetching/fetch_esql.test.ts | 66 +++++ .../main/data_fetching/fetch_esql.ts | 48 ++-- .../main/discover_main_route.test.tsx | 27 ++ .../application/main/discover_main_route.tsx | 6 +- .../discover_data_state_container.test.ts | 25 +- .../discover_data_state_container.ts | 8 +- src/plugins/discover/public/build_services.ts | 42 +-- .../discover_container/discover_container.tsx | 1 + .../discover_grid_flyout.test.tsx | 2 + .../context_awareness/__mocks__/index.ts | 100 +++++++ .../composable_profile.test.ts | 67 +++++ .../context_awareness/composable_profile.ts | 28 ++ .../public/context_awareness/hooks/index.ts | 10 + .../hooks/use_profile_accessor.test.ts | 78 ++++++ .../hooks/use_profile_accessor.ts | 25 ++ .../hooks/use_profiles.test.tsx | 91 ++++++ .../context_awareness/hooks/use_profiles.ts | 36 +++ .../hooks/use_root_profile.test.tsx | 45 +++ .../hooks/use_root_profile.ts | 33 +++ .../public/context_awareness/index.ts | 13 + .../context_awareness/profile_service.test.ts | 147 ++++++++++ .../context_awareness/profile_service.ts | 105 +++++++ .../profiles/data_source_profile.ts | 45 +++ .../profiles/document_profile.ts | 41 +++ .../profiles/example_profiles.tsx | 123 ++++++++ .../context_awareness/profiles/index.ts | 11 + .../profiles/root_profile.ts | 42 +++ .../profiles_manager.test.ts | 263 ++++++++++++++++++ .../context_awareness/profiles_manager.ts | 206 ++++++++++++++ .../public/context_awareness/types.ts | 13 + .../__mocks__/customization_context.ts | 10 +- .../public/customizations/defaults.ts | 1 + .../discover/public/customizations/types.ts | 4 + .../saved_search_embeddable.test.ts | 61 +++- .../embeddable/saved_search_embeddable.tsx | 38 ++- .../public/embeddable/saved_search_grid.tsx | 8 + .../view_saved_search_action.test.ts | 5 + .../public/hooks/show_confirm_panel.tsx | 71 ----- src/plugins/discover/public/plugin.tsx | 83 ++++-- .../components/log_ai_assistant/index.tsx | 6 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 51 files changed, 2008 insertions(+), 239 deletions(-) create mode 100644 src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts create mode 100644 src/plugins/discover/public/context_awareness/__mocks__/index.ts create mode 100644 src/plugins/discover/public/context_awareness/composable_profile.test.ts create mode 100644 src/plugins/discover/public/context_awareness/composable_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/index.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_profiles.ts create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx create mode 100644 src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_service.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_service.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/document_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx create mode 100644 src/plugins/discover/public/context_awareness/profiles/index.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles/root_profile.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles_manager.test.ts create mode 100644 src/plugins/discover/public/context_awareness/profiles_manager.ts create mode 100644 src/plugins/discover/public/context_awareness/types.ts delete mode 100644 src/plugins/discover/public/hooks/show_confirm_panel.tsx diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts index ad486aba543a1..e6046d4f5977f 100644 --- a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts +++ b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts @@ -30,5 +30,17 @@ describe('Data table record utils', () => { expect(doc).toHaveProperty('isAnchor'); }); }); + + test('should support processing each record', () => { + const result = buildDataTableRecordList(esHitsMock, dataViewMock, { + processRecord: (record) => ({ ...record, id: 'custom-id' }), + }); + result.forEach((doc) => { + expect(doc).toHaveProperty('id', 'custom-id'); + expect(doc).toHaveProperty('raw'); + expect(doc).toHaveProperty('flattened'); + expect(doc).toHaveProperty('isAnchor'); + }); + }); }); }); diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.ts b/packages/kbn-discover-utils/src/utils/build_data_record.ts index 43adf7b9c8b66..9769201e94aa4 100644 --- a/packages/kbn-discover-utils/src/utils/build_data_record.ts +++ b/packages/kbn-discover-utils/src/utils/build_data_record.ts @@ -35,9 +35,13 @@ export function buildDataTableRecord( * @param docs Array of documents returned from Elasticsearch * @param dataView this current data view */ -export function buildDataTableRecordList( +export function buildDataTableRecordList( docs: EsHitRecord[], - dataView?: DataView + dataView?: DataView, + { processRecord }: { processRecord?: (record: DataTableRecord) => T } = {} ): DataTableRecord[] { - return docs.map((doc) => buildDataTableRecord(doc, dataView)); + return docs.map((doc) => { + const record = buildDataTableRecord(doc, dataView); + return processRecord ? processRecord(record) : record; + }); } diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index e4e2b71de8e74..f75755319a112 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -41,6 +41,7 @@ import { SearchSourceDependencies } from '@kbn/data-plugin/common'; import { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { urlTrackerMock } from './url_tracker.mock'; import { createElement } from 'react'; +import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; export function createDiscoverServicesMock(): DiscoverServices { const dataPlugin = dataPluginMock.createStartContract(); @@ -137,6 +138,7 @@ export function createDiscoverServicesMock(): DiscoverServices { ...uiSettingsMock, }; + const { profilesManagerMock } = createContextAwarenessMocks(); const theme = themeServiceMock.createSetupContract({ darkMode: false }); corePluginMock.theme = theme; @@ -236,6 +238,7 @@ export function createDiscoverServicesMock(): DiscoverServices { contextLocator: { getRedirectUrl: jest.fn(() => '') }, singleDocLocator: { getRedirectUrl: jest.fn(() => '') }, urlTracker: urlTrackerMock, + profilesManager: profilesManagerMock, setHeaderActionMenu: jest.fn(), } as unknown as DiscoverServices; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index ae4b05f495cfa..85c2dd581eecb 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -31,6 +31,7 @@ const customisationService = createCustomizationService(); async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { const services = discoverServiceMock; + services.data.query.timefilter.timefilter.getTime = () => { return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; }; @@ -69,6 +70,10 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { } describe('Discover documents layout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('render loading when loading and no documents', async () => { const component = await mountComponent(FetchStatus.LOADING, []); expect(component.find('.dscDocuments__loading').exists()).toBeTruthy(); @@ -131,4 +136,22 @@ describe('Discover documents layout', () => { expect(discoverGridComponent.prop('externalCustomRenderers')).toBeDefined(); expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toBeDefined(); }); + + describe('context awareness', () => { + it('should pass cell renderers from profile', async () => { + customisationService.set({ + id: 'data_table', + logsEnabled: true, + }); + await discoverServiceMock.profilesManager.resolveRootProfile({ solutionNavId: 'test' }); + const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock); + const discoverGridComponent = component.find(DiscoverGrid); + expect(discoverGridComponent.exists()).toBeTruthy(); + expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([ + 'content', + 'resource', + 'rootProfile', + ]); + }); + }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index ad46b7f3db658..caba229e9137a 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -68,6 +68,7 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column'; import { useContextualGridCustomisations } from '../../hooks/grid_customisations'; import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; +import { useProfileAccessor } from '../../../../context_awareness'; const containerStyles = css` position: relative; @@ -263,6 +264,12 @@ function DiscoverDocumentsComponent({ useContextualGridCustomisations() || {}; const additionalFieldGroups = useAdditionalFieldGroups(); + const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); + const cellRenderers = useMemo(() => { + const getCellRenderers = getCellRenderersAccessor(() => customCellRenderer ?? {}); + return getCellRenderers(); + }, [customCellRenderer, getCellRenderersAccessor]); + const documents = useObservable(stateContainer.dataState.data$.documents$); const callouts = useMemo( @@ -373,66 +380,64 @@ function DiscoverDocumentsComponent({ )} {!isLegacy && ( - <> -
- - - -
- +
+ + + +
)} diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts index 410d1d468275d..aed3e6f9a0222 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts @@ -64,7 +64,7 @@ export function fetchAll( savedSearch, abortController, } = fetchDeps; - const { data } = services; + const { data, expressions, profilesManager } = services; const searchSource = savedSearch.searchSource.createChild(); try { @@ -100,14 +100,15 @@ export function fetchAll( // Start fetching all required requests const response = isEsqlQuery - ? fetchEsql( + ? fetchEsql({ query, dataView, - data, - services.expressions, + abortSignal: abortController.signal, inspectorAdapters, - abortController.signal - ) + data, + expressions, + profilesManager, + }) : fetchDocuments(searchSource, fetchDeps); const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments'; const startTime = window.performance.now(); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts index 7abc2d2744a60..be1fddf64e87f 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts @@ -30,6 +30,10 @@ const getDeps = () => } as unknown as FetchDeps); describe('test fetchDocuments', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('resolves with returned documents', async () => { const hits = [ { _id: '1', foo: 'bar' }, @@ -38,10 +42,17 @@ describe('test fetchDocuments', () => { const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock)); savedSearchMock.searchSource.fetch$ = () => of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse>); + const resolveDocumentProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDocumentProfile' + ); expect(await fetchDocuments(savedSearchMock.searchSource, getDeps())).toEqual({ interceptedWarnings: [], records: documents, }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[0] }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[1] }); }); test('rejects on query failure', async () => { diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts index 414d4b3a36587..4ffdd211c0e5e 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts @@ -67,7 +67,9 @@ export const fetchDocuments = ( .pipe( filter((res) => !isRunningResponse(res)), map((res) => { - return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView); + return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, { + processRecord: (record) => services.profilesManager.resolveDocumentProfile({ record }), + }); }) ); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts new file mode 100644 index 0000000000000..6546ae8ffaf2d --- /dev/null +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { EsHitRecord } from '@kbn/discover-utils'; +import type { ExecutionContract } from '@kbn/expressions-plugin/common'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; +import { of } from 'rxjs'; +import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { fetchEsql } from './fetch_esql'; + +describe('fetchEsql', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('resolves with returned records', async () => { + const hits = [ + { _id: '1', foo: 'bar' }, + { _id: '2', foo: 'baz' }, + ] as unknown as EsHitRecord[]; + const records = hits.map((hit, i) => ({ + id: String(i), + raw: hit, + flattened: hit, + })); + const expressionsExecuteSpy = jest.spyOn(discoverServiceMock.expressions, 'execute'); + expressionsExecuteSpy.mockReturnValueOnce({ + cancel: jest.fn(), + getData: jest.fn(() => + of({ + result: { + columns: ['_id', 'foo'], + rows: hits, + }, + }) + ), + } as unknown as ExecutionContract); + const resolveDocumentProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDocumentProfile' + ); + expect( + await fetchEsql({ + query: { esql: 'from *' }, + dataView: dataViewWithTimefieldMock, + inspectorAdapters: { requests: new RequestAdapter() }, + data: discoverServiceMock.data, + expressions: discoverServiceMock.expressions, + profilesManager: discoverServiceMock.profilesManager, + }) + ).toEqual({ + records, + esqlQueryColumns: ['_id', 'foo'], + esqlHeaderWarning: undefined, + }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[0] }); + expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[1] }); + }); +}); diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts index 3aba795d26920..3f54984ae3d3f 100644 --- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts +++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts @@ -8,15 +8,16 @@ import { pluck } from 'rxjs'; import { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { Query, AggregateQuery, Filter } from '@kbn/es-query'; +import type { Query, AggregateQuery, Filter } from '@kbn/es-query'; import type { Adapters } from '@kbn/inspector-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils'; import type { RecordsFetchResponse } from '../../types'; +import type { ProfilesManager } from '../../../context_awareness'; interface EsqlErrorResponse { error: { @@ -25,16 +26,27 @@ interface EsqlErrorResponse { type: 'error'; } -export function fetchEsql( - query: Query | AggregateQuery, - dataView: DataView, - data: DataPublicPluginStart, - expressions: ExpressionsStart, - inspectorAdapters: Adapters, - abortSignal?: AbortSignal, - filters?: Filter[], - inputQuery?: Query -): Promise { +export function fetchEsql({ + query, + inputQuery, + filters, + dataView, + abortSignal, + inspectorAdapters, + data, + expressions, + profilesManager, +}: { + query: Query | AggregateQuery; + inputQuery?: Query; + filters?: Filter[]; + dataView: DataView; + abortSignal?: AbortSignal; + inspectorAdapters: Adapters; + data: DataPublicPluginStart; + expressions: ExpressionsStart; + profilesManager: ProfilesManager; +}): Promise { const timeRange = data.query.timefilter.timefilter.getTime(); return textBasedQueryStateToAstWithValidation({ filters, @@ -69,12 +81,14 @@ export function fetchEsql( const rows = table?.rows ?? []; esqlQueryColumns = table?.columns ?? undefined; esqlHeaderWarning = table.warning ?? undefined; - finalData = rows.map((row: Record, idx: number) => { - return { + finalData = rows.map((row, idx) => { + const record: DataTableRecord = { id: String(idx), - raw: row, + raw: row as EsHitRecord, flattened: row, - } as unknown as DataTableRecord; + }; + + return profilesManager.resolveDocumentProfile({ record }); }); } }); @@ -91,7 +105,7 @@ export function fetchEsql( }); } return { - records: [] as DataTableRecord[], + records: [], esqlQueryColumns: [], esqlHeaderWarning: undefined, }; diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx index 496bb91f92cf1..b49abb2fe6685 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx @@ -40,9 +40,22 @@ jest.mock('./discover_main_app', () => { }; }); +let mockRootProfileLoading = false; + +jest.mock('../../context_awareness', () => { + const originalModule = jest.requireActual('../../context_awareness'); + return { + ...originalModule, + useRootProfile: () => ({ + rootProfileLoading: mockRootProfileLoading, + }), + }; +}); + describe('DiscoverMainRoute', () => { beforeEach(() => { mockCustomizationService = createCustomizationService(); + mockRootProfileLoading = false; }); test('renders the main app when hasESData=true & hasUserDataView=true ', async () => { @@ -97,6 +110,20 @@ describe('DiscoverMainRoute', () => { }); }); + test('renders LoadingIndicator while root profile is loading', async () => { + mockRootProfileLoading = true; + const component = mountComponent(true, true); + await waitFor(() => { + component.update(); + expect(component.find(DiscoverMainApp).exists()).toBe(false); + }); + mockRootProfileLoading = false; + await waitFor(() => { + component.setProps({}).update(); + expect(component.find(DiscoverMainApp).exists()).toBe(true); + }); + }); + test('should pass hideNavMenuItems=true to DiscoverTopNavInline while loading', async () => { const component = mountComponent(true, true); expect(component.find(DiscoverTopNavInline).prop('hideNavMenuItems')).toBe(true); diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 560f4cb03535e..f37487b6b93b7 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -40,6 +40,7 @@ import { import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline'; import { DiscoverStateContainer, LoadParams } from './state_management/discover_state'; import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; +import { useRootProfile } from '../../context_awareness'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); @@ -338,11 +339,14 @@ export function DiscoverMainRoute({ stateContainer, ]); + const { solutionNavId } = customizationContext; + const { rootProfileLoading } = useRootProfile({ solutionNavId }); + if (error) { return ; } - if (!customizationService) { + if (!customizationService || rootProfileLoading) { return loadingIndicator; } diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts index 67586670c01c4..05668e0406f9c 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts @@ -26,6 +26,10 @@ jest.mock('@kbn/ebt-tools', () => ({ const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction; describe('test getDataStateContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('return is valid', async () => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); const dataState = stateContainer.dataState; @@ -35,6 +39,7 @@ describe('test getDataStateContainer', () => { expect(dataState.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING); expect(dataState.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING); }); + test('refetch$ triggers a search', async () => { const stateContainer = getDiscoverStateMock({ isTimeBased: true }); jest.spyOn(stateContainer.searchSessionManager, 'getNextSearchSessionId'); @@ -46,10 +51,15 @@ describe('test getDataStateContainer', () => { discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => { return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; }); - const dataState = stateContainer.dataState; + const dataState = stateContainer.dataState; const unsubscribe = dataState.subscribe(); + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(dataState.data$.totalHits$.value.result).toBe(undefined); expect(dataState.data$.documents$.value.result).toEqual(undefined); @@ -58,6 +68,12 @@ describe('test getDataStateContainer', () => { expect(dataState.data$.main$.value.fetchStatus).toBe('complete'); }); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledTimes(1); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: stateContainer.appState.get().dataSource, + dataView: stateContainer.savedSearchState.getState().searchSource.getField('index'), + query: stateContainer.appState.get().query, + }); expect(dataState.data$.totalHits$.value.result).toBe(0); expect(dataState.data$.documents$.value.result).toEqual([]); @@ -117,9 +133,13 @@ describe('test getDataStateContainer', () => { ).not.toHaveBeenCalled(); const dataState = stateContainer.dataState; - const unsubscribe = dataState.subscribe(); + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(dataState.data$.documents$.value.result).toEqual(initialRecords); let hasLoadingMoreStarted = false; @@ -131,6 +151,7 @@ describe('test getDataStateContainer', () => { } if (hasLoadingMoreStarted && value.fetchStatus === FetchStatus.COMPLETE) { + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); expect(value.result).toEqual([...initialRecords, ...moreRecords]); // it uses the same current search session id expect( diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts index 71ad2ed87e79b..aaa1f6c15c0f4 100644 --- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts +++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts @@ -152,7 +152,7 @@ export function getDataStateContainer({ getSavedSearch: () => SavedSearch; setDataView: (dataView: DataView) => void; }): DiscoverDataStateContainer { - const { data, uiSettings, toastNotifications } = services; + const { data, uiSettings, toastNotifications, profilesManager } = services; const { timefilter } = data.query.timefilter; const inspectorAdapters = { requests: new RequestAdapter() }; @@ -249,6 +249,12 @@ export function getDataStateContainer({ return; } + await profilesManager.resolveDataSourceProfile({ + dataSource: getAppState().dataSource, + dataView: getSavedSearch().searchSource.getField('index'), + query: getAppState().query, + }); + abortController = new AbortController(); const prevAutoRefreshDone = autoRefreshDone; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index e3524dcdf115c..519d6a36fb528 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -7,8 +7,7 @@ */ import { History } from 'history'; - -import { +import type { Capabilities, ChromeStart, CoreStart, @@ -24,28 +23,26 @@ import { AppMountParameters, ScopedHistory, } from '@kbn/core/public'; -import { +import type { FilterManager, TimefilterContract, DataViewsContract, DataPublicPluginStart, } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; -import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { UiCounterMetricType } from '@kbn/analytics'; +import type { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import type { UiCounterMetricType } from '@kbn/analytics'; import { Storage } from '@kbn/kibana-utils-plugin/public'; - -import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; -import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; -import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; - +import type { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { SpacesApi } from '@kbn/spaces-plugin/public'; -import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; @@ -59,10 +56,11 @@ import { memoize, noop } from 'lodash'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; -import { DiscoverStartPlugins } from './plugin'; -import { DiscoverContextAppLocator } from './application/context/services/locator'; -import { DiscoverSingleDocLocator } from './application/doc/locator'; -import { DiscoverAppLocator } from '../common'; +import type { DiscoverStartPlugins } from './plugin'; +import type { DiscoverContextAppLocator } from './application/context/services/locator'; +import type { DiscoverSingleDocLocator } from './application/doc/locator'; +import type { DiscoverAppLocator } from '../common'; +import type { ProfilesManager } from './context_awareness'; /** * Location state of internal Discover history instance @@ -129,6 +127,7 @@ export interface DiscoverServices { contentClient: ContentClient; noDataPage?: NoDataPagePluginStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; + profilesManager: ProfilesManager; } export const buildServices = memoize( @@ -142,6 +141,7 @@ export const buildServices = memoize( history, scopedHistory, urlTracker, + profilesManager, setHeaderActionMenu = noop, }: { core: CoreStart; @@ -153,6 +153,7 @@ export const buildServices = memoize( history: History; scopedHistory?: ScopedHistory; urlTracker: UrlTracker; + profilesManager: ProfilesManager; setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; }): DiscoverServices => { const { usageCollection } = plugins; @@ -212,6 +213,7 @@ export const buildServices = memoize( contentClient: plugins.contentManagement.client, noDataPage: plugins.noDataPage, observabilityAIAssistant: plugins.observabilityAIAssistant, + profilesManager, }; } ); diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx index 4f768554e1e54..c253760fff2ac 100644 --- a/src/plugins/discover/public/components/discover_container/discover_container.tsx +++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx @@ -44,6 +44,7 @@ const discoverContainerWrapperCss = css` `; const customizationContext: DiscoverCustomizationContext = { + solutionNavId: null, displayMode: 'embedded', inlineTopNav: { enabled: false, diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx index 3907bc4999232..cb02e3b736663 100644 --- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx @@ -25,6 +25,7 @@ import { ReactWrapper } from 'enzyme'; import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin'; import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__'; import { FlyoutCustomization, useDiscoverCustomization } from '../../customizations'; +import { discoverServiceMock } from '../../__mocks__/services'; const mockFlyoutCustomization: FlyoutCustomization = { id: 'flyout', @@ -76,6 +77,7 @@ describe('Discover flyout', function () { }) => { const onClose = jest.fn(); const services = { + ...discoverServiceMock, filterManager: createFilterManagerMock(), addBasePath: (path: string) => `/base${path}`, history: () => ({ location: {} }), diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.ts b/src/plugins/discover/public/context_awareness/__mocks__/index.ts new file mode 100644 index 0000000000000..0f8beed5d955f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.ts @@ -0,0 +1,100 @@ +/* + * 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 { getDataTableRecords } from '../../__fixtures__/real_hits'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { + DataSourceCategory, + DataSourceProfileProvider, + DataSourceProfileService, + DocumentProfileProvider, + DocumentProfileService, + DocumentType, + RootProfileProvider, + RootProfileService, + SolutionType, +} from '../profiles'; +import { ProfilesManager } from '../profiles_manager'; + +export const createContextAwarenessMocks = () => { + const rootProfileProviderMock: RootProfileProvider = { + profileId: 'root-profile', + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'root-profile', + })), + }, + resolve: jest.fn(() => ({ + isMatch: true, + context: { + solutionType: SolutionType.Observability, + }, + })), + }; + + const dataSourceProfileProviderMock: DataSourceProfileProvider = { + profileId: 'data-source-profile', + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'data-source-profile', + })), + }, + resolve: jest.fn(() => ({ + isMatch: true, + context: { + category: DataSourceCategory.Logs, + }, + })), + }; + + const documentProfileProviderMock: DocumentProfileProvider = { + profileId: 'document-profile', + profile: { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + rootProfile: () => 'document-profile', + })), + } as DocumentProfileProvider['profile'], + resolve: jest.fn(() => ({ + isMatch: true, + context: { + type: DocumentType.Log, + }, + })), + }; + + const records = getDataTableRecords(dataViewWithTimefieldMock); + const contextRecordMock = records[0]; + const contextRecordMock2 = records[1]; + + const rootProfileServiceMock = new RootProfileService(); + rootProfileServiceMock.registerProvider(rootProfileProviderMock); + + const dataSourceProfileServiceMock = new DataSourceProfileService(); + dataSourceProfileServiceMock.registerProvider(dataSourceProfileProviderMock); + + const documentProfileServiceMock = new DocumentProfileService(); + documentProfileServiceMock.registerProvider(documentProfileProviderMock); + + const profilesManagerMock = new ProfilesManager( + rootProfileServiceMock, + dataSourceProfileServiceMock, + documentProfileServiceMock + ); + + return { + rootProfileProviderMock, + dataSourceProfileProviderMock, + documentProfileProviderMock, + contextRecordMock, + contextRecordMock2, + profilesManagerMock, + }; +}; diff --git a/src/plugins/discover/public/context_awareness/composable_profile.test.ts b/src/plugins/discover/public/context_awareness/composable_profile.test.ts new file mode 100644 index 0000000000000..251da37fa0126 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/composable_profile.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { ComposableProfile, getMergedAccessor } from './composable_profile'; +import { Profile } from './types'; + +describe('getMergedAccessor', () => { + it('should return the base implementation if no profiles are provided', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const mergedAccessor = getMergedAccessor([], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).toHaveBeenCalled(); + expect(result).toEqual({ base: expect.any(Function) }); + }); + + it('should merge the accessors in the correct order', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const profile1: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile1: jest.fn(), + })), + }; + const profile2: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile2: jest.fn(), + })), + }; + const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).toHaveBeenCalled(); + expect(profile1.getCellRenderers).toHaveBeenCalled(); + expect(profile2.getCellRenderers).toHaveBeenCalled(); + expect(result).toEqual({ + base: expect.any(Function), + profile1: expect.any(Function), + profile2: expect.any(Function), + }); + expect(Object.keys(result)).toEqual(['base', 'profile1', 'profile2']); + }); + + it('should allow overwriting previous accessors', () => { + const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() })); + const profile1: ComposableProfile = { + getCellRenderers: jest.fn(() => () => ({ profile1: jest.fn() })), + }; + const profile2: ComposableProfile = { + getCellRenderers: jest.fn((prev) => () => ({ + ...prev(), + profile2: jest.fn(), + })), + }; + const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl); + const result = mergedAccessor(); + expect(baseImpl).not.toHaveBeenCalled(); + expect(profile1.getCellRenderers).toHaveBeenCalled(); + expect(profile2.getCellRenderers).toHaveBeenCalled(); + expect(result).toEqual({ profile1: expect.any(Function), profile2: expect.any(Function) }); + expect(Object.keys(result)).toEqual(['profile1', 'profile2']); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts new file mode 100644 index 0000000000000..c2211dee3f370 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/composable_profile.ts @@ -0,0 +1,28 @@ +/* + * 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 { Profile } from './types'; + +export type PartialProfile = Partial; + +export type ComposableAccessor = (getPrevious: T) => T; + +export type ComposableProfile = { + [TKey in keyof TProfile]?: ComposableAccessor; +}; + +export const getMergedAccessor = ( + profiles: ComposableProfile[], + key: TKey, + baseImpl: Profile[TKey] +) => { + return profiles.reduce((nextAccessor, profile) => { + const currentAccessor = profile[key]; + return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor; + }, baseImpl); +}; diff --git a/src/plugins/discover/public/context_awareness/hooks/index.ts b/src/plugins/discover/public/context_awareness/hooks/index.ts new file mode 100644 index 0000000000000..3235844de4fc5 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { useProfileAccessor } from './use_profile_accessor'; +export { useRootProfile } from './use_root_profile'; diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts new file mode 100644 index 0000000000000..7f3cd816ae9e8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { ComposableProfile, getMergedAccessor } from '../composable_profile'; +import { useProfileAccessor } from './use_profile_accessor'; +import { getDataTableRecords } from '../../__fixtures__/real_hits'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { useProfiles } from './use_profiles'; + +let mockProfiles: ComposableProfile[] = []; + +jest.mock('./use_profiles', () => ({ + useProfiles: jest.fn(() => mockProfiles), +})); + +jest.mock('../composable_profile', () => { + const originalModule = jest.requireActual('../composable_profile'); + return { + ...originalModule, + getMergedAccessor: jest.fn(originalModule.getMergedAccessor), + }; +}); + +const record = getDataTableRecords(dataViewWithTimefieldMock)[0]; + +describe('useProfileAccessor', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockProfiles = [ + { getCellRenderers: (prev) => () => ({ ...prev(), profile1: jest.fn() }) }, + { getCellRenderers: (prev) => () => ({ ...prev(), profile2: jest.fn() }) }, + ]; + }); + + it('should return a function that merges accessors', () => { + const { result } = renderHook(() => useProfileAccessor('getCellRenderers', { record })); + expect(useProfiles).toHaveBeenCalledTimes(1); + expect(useProfiles).toHaveBeenCalledWith({ record }); + const base = () => ({ base: jest.fn() }); + const accessor = result.current(base); + expect(getMergedAccessor).toHaveBeenCalledTimes(1); + expect(getMergedAccessor).toHaveBeenCalledWith(mockProfiles, 'getCellRenderers', base); + const renderers = accessor(); + expect(renderers).toEqual({ + base: expect.any(Function), + profile1: expect.any(Function), + profile2: expect.any(Function), + }); + expect(Object.keys(renderers)).toEqual(['base', 'profile1', 'profile2']); + }); + + it('should recalculate the accessor when the key changes', () => { + const { rerender, result } = renderHook(({ key }) => useProfileAccessor(key, { record }), { + initialProps: { key: 'getCellRenderers' as const }, + }); + const prevResult = result.current; + rerender({ key: 'getCellRenderers' }); + expect(result.current).toBe(prevResult); + rerender({ key: 'otherKey' as unknown as 'getCellRenderers' }); + expect(result.current).not.toBe(prevResult); + }); + + it('should recalculate the accessor when the profiles change', () => { + const { rerender, result } = renderHook(() => + useProfileAccessor('getCellRenderers', { record }) + ); + const prevResult = result.current; + mockProfiles = [{ getCellRenderers: (prev) => () => ({ ...prev(), profile3: jest.fn() }) }]; + rerender(); + expect(result.current).not.toBe(prevResult); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts new file mode 100644 index 0000000000000..58c5a316f86cf --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts @@ -0,0 +1,25 @@ +/* + * 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 { useCallback } from 'react'; +import { getMergedAccessor } from '../composable_profile'; +import type { GetProfilesOptions } from '../profiles_manager'; +import { useProfiles } from './use_profiles'; +import type { Profile } from '../types'; + +export const useProfileAccessor = ( + key: TKey, + options: GetProfilesOptions = {} +) => { + const profiles = useProfiles(options); + + return useCallback( + (baseImpl: Profile[TKey]) => getMergedAccessor(profiles, key, baseImpl), + [key, profiles] + ); +}; diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx new file mode 100644 index 0000000000000..f8613e4fea380 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { GetProfilesOptions } from '../profiles_manager'; +import { createContextAwarenessMocks } from '../__mocks__'; +import { useProfiles } from './use_profiles'; + +const { + rootProfileProviderMock, + dataSourceProfileProviderMock, + documentProfileProviderMock, + contextRecordMock, + contextRecordMock2, + profilesManagerMock, +} = createContextAwarenessMocks(); + +profilesManagerMock.resolveRootProfile({}); +profilesManagerMock.resolveDataSourceProfile({}); + +const record = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock }); +const record2 = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock2 }); + +discoverServiceMock.profilesManager = profilesManagerMock; + +const getProfilesSpy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles'); +const getProfiles$Spy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles$'); + +const render = () => { + return renderHook((props) => useProfiles(props), { + initialProps: { record } as GetProfilesOptions, + wrapper: ({ children }) => ( + {children} + ), + }); +}; + +describe('useProfiles', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return profiles', () => { + const { result } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toEqual([ + rootProfileProviderMock.profile, + dataSourceProfileProviderMock.profile, + documentProfileProviderMock.profile, + ]); + }); + + it('should return the same array reference if profiles do not change', () => { + const { result, rerender } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + const prevResult = result.current; + rerender({ record }); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toBe(prevResult); + rerender({ record: record2 }); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(getProfiles$Spy).toHaveBeenCalledTimes(2); + expect(result.current).toBe(prevResult); + }); + + it('should return a different array reference if profiles change', () => { + const { result, rerender } = render(); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + const prevResult = result.current; + rerender({ record }); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(getProfiles$Spy).toHaveBeenCalledTimes(1); + expect(result.current).toBe(prevResult); + rerender({ record: undefined }); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(getProfiles$Spy).toHaveBeenCalledTimes(2); + expect(result.current).not.toBe(prevResult); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts new file mode 100644 index 0000000000000..9bd86e4386150 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.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 { useEffect, useMemo, useState } from 'react'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; +import type { GetProfilesOptions } from '../profiles_manager'; + +export const useProfiles = ({ record }: GetProfilesOptions = {}) => { + const { profilesManager } = useDiscoverServices(); + const [profiles, setProfiles] = useState(() => profilesManager.getProfiles({ record })); + const profiles$ = useMemo( + () => profilesManager.getProfiles$({ record }), + [profilesManager, record] + ); + + useEffect(() => { + const subscription = profiles$.subscribe((newProfiles) => { + setProfiles((currentProfiles) => { + return currentProfiles.every((profile, i) => profile === newProfiles[i]) + ? currentProfiles + : newProfiles; + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [profiles$]); + + return profiles; +}; diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx new file mode 100644 index 0000000000000..a41ec7c23cf88 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { discoverServiceMock } from '../../__mocks__/services'; +import { useRootProfile } from './use_root_profile'; + +const render = () => { + return renderHook((props) => useRootProfile(props), { + initialProps: { solutionNavId: 'solutionNavId' }, + wrapper: ({ children }) => ( + {children} + ), + }); +}; + +describe('useRootProfile', () => { + it('should return rootProfileLoading as true', () => { + const { result } = render(); + expect(result.current.rootProfileLoading).toBe(true); + }); + + it('should return rootProfileLoading as false', async () => { + const { result, waitForNextUpdate } = render(); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + }); + + it('should return rootProfileLoading as true when solutionNavId changes', async () => { + const { result, rerender, waitForNextUpdate } = render(); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + rerender({ solutionNavId: 'newSolutionNavId' }); + expect(result.current.rootProfileLoading).toBe(true); + await waitForNextUpdate(); + expect(result.current.rootProfileLoading).toBe(false); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts new file mode 100644 index 0000000000000..ff2d7edbcefb8 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts @@ -0,0 +1,33 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; + +export const useRootProfile = ({ solutionNavId }: { solutionNavId: string | null }) => { + const { profilesManager } = useDiscoverServices(); + const [rootProfileLoading, setRootProfileLoading] = useState(true); + + useEffect(() => { + let aborted = false; + + setRootProfileLoading(true); + + profilesManager.resolveRootProfile({ solutionNavId }).then(() => { + if (!aborted) { + setRootProfileLoading(false); + } + }); + + return () => { + aborted = true; + }; + }, [profilesManager, solutionNavId]); + + return { rootProfileLoading }; +}; diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts new file mode 100644 index 0000000000000..6106d9d154e49 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/index.ts @@ -0,0 +1,13 @@ +/* + * 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 * from './types'; +export * from './profiles'; +export { getMergedAccessor } from './composable_profile'; +export { ProfilesManager } from './profiles_manager'; +export { useProfileAccessor, useRootProfile } from './hooks'; diff --git a/src/plugins/discover/public/context_awareness/profile_service.test.ts b/src/plugins/discover/public/context_awareness/profile_service.test.ts new file mode 100644 index 0000000000000..e306ee149f52f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_service.test.ts @@ -0,0 +1,147 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import { AsyncProfileService, ContextWithProfileId, ProfileService } from './profile_service'; +import { Profile } from './types'; + +interface TestParams { + myParam: string; +} + +interface TestContext { + myContext: string; +} + +const defaultContext: ContextWithProfileId = { + profileId: 'test-profile', + myContext: 'test', +}; + +class TestProfileService extends ProfileService { + constructor() { + super(defaultContext); + } +} + +type TestProfileProvider = Parameters[0]; + +class TestAsyncProfileService extends AsyncProfileService { + constructor() { + super(defaultContext); + } +} + +type TestAsyncProfileProvider = Parameters[0]; + +const provider: TestProfileProvider = { + profileId: 'test-profile-1', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(() => ({ isMatch: false })), +}; + +const provider2: TestProfileProvider = { + profileId: 'test-profile-2', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +const provider3: TestProfileProvider = { + profileId: 'test-profile-3', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +const asyncProvider2: TestAsyncProfileProvider = { + profileId: 'test-profile-2', + profile: { getCellRenderers: jest.fn() }, + resolve: jest.fn(async ({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })), +}; + +describe('ProfileService', () => { + let service: TestProfileService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new TestProfileService(); + }); + + it('should expose defaultContext', () => { + expect(service.defaultContext).toBe(defaultContext); + }); + + it('should allow registering providers and getting profiles', () => { + service.registerProvider(provider); + service.registerProvider(provider2); + expect(service.getProfile({ profileId: 'test-profile-1', myContext: 'test' })).toBe( + provider.profile + ); + expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toBe( + provider2.profile + ); + }); + + it('should return empty profile if no provider is found', () => { + service.registerProvider(provider); + expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toEqual({}); + }); + + it('should resolve to first matching context', () => { + service.registerProvider(provider); + service.registerProvider(provider2); + service.registerProvider(provider3); + expect(service.resolve({ myParam: 'test' })).toEqual({ + profileId: 'test-profile-2', + myContext: 'test', + }); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider2.resolve).toHaveBeenCalledTimes(1); + expect(provider2.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider3.resolve).not.toHaveBeenCalled(); + }); + + it('should resolve to default context if no matching context is found', () => { + service.registerProvider(provider); + expect(service.resolve({ myParam: 'test' })).toEqual(defaultContext); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + }); +}); + +describe('AsyncProfileService', () => { + let service: TestAsyncProfileService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new TestAsyncProfileService(); + }); + + it('should resolve to first matching context', async () => { + service.registerProvider(provider); + service.registerProvider(asyncProvider2); + service.registerProvider(provider3); + await expect(service.resolve({ myParam: 'test' })).resolves.toEqual({ + profileId: 'test-profile-2', + myContext: 'test', + }); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(asyncProvider2.resolve).toHaveBeenCalledTimes(1); + expect(asyncProvider2.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + expect(provider3.resolve).not.toHaveBeenCalled(); + }); + + it('should resolve to default context if no matching context is found', async () => { + service.registerProvider(provider); + await expect(service.resolve({ myParam: 'test' })).resolves.toEqual(defaultContext); + expect(provider.resolve).toHaveBeenCalledTimes(1); + expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' }); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts new file mode 100644 index 0000000000000..2b43595761d19 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_service.ts @@ -0,0 +1,105 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +import type { ComposableProfile, PartialProfile } from './composable_profile'; +import type { Profile } from './types'; + +export type ResolveProfileResult = + | { isMatch: true; context: TContext } + | { isMatch: false }; + +export type ProfileProviderMode = 'sync' | 'async'; + +export interface ProfileProvider< + TProfile extends PartialProfile, + TParams, + TContext, + TMode extends ProfileProviderMode +> { + profileId: string; + profile: ComposableProfile; + resolve: ( + params: TParams + ) => TMode extends 'sync' + ? ResolveProfileResult + : ResolveProfileResult | Promise>; +} + +export type ContextWithProfileId = TContext & { profileId: string }; + +const EMPTY_PROFILE = {}; + +abstract class BaseProfileService< + TProfile extends PartialProfile, + TParams, + TContext, + TMode extends ProfileProviderMode +> { + protected readonly providers: Array> = []; + + protected constructor(public readonly defaultContext: ContextWithProfileId) {} + + public registerProvider(provider: ProfileProvider) { + this.providers.push(provider); + } + + public getProfile(context: ContextWithProfileId): ComposableProfile { + const provider = this.providers.find((current) => current.profileId === context.profileId); + return provider?.profile ?? EMPTY_PROFILE; + } + + public abstract resolve( + params: TParams + ): TMode extends 'sync' + ? ContextWithProfileId + : Promise>; +} + +export class ProfileService< + TProfile extends PartialProfile, + TParams, + TContext +> extends BaseProfileService { + public resolve(params: TParams) { + for (const provider of this.providers) { + const result = provider.resolve(params); + + if (result.isMatch) { + return { + ...result.context, + profileId: provider.profileId, + }; + } + } + + return this.defaultContext; + } +} + +export class AsyncProfileService< + TProfile extends PartialProfile, + TParams, + TContext +> extends BaseProfileService { + public async resolve(params: TParams) { + for (const provider of this.providers) { + const result = await provider.resolve(params); + + if (result.isMatch) { + return { + ...result.context, + profileId: provider.profileId, + }; + } + } + + return this.defaultContext; + } +} diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts new file mode 100644 index 0000000000000..f616fef913259 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts @@ -0,0 +1,45 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import type { AggregateQuery, Query } from '@kbn/es-query'; +import type { DiscoverDataSource } from '../../../common/data_sources'; +import { AsyncProfileService } from '../profile_service'; +import { Profile } from '../types'; + +export enum DataSourceCategory { + Logs = 'logs', + Default = 'default', +} + +export interface DataSourceProfileProviderParams { + dataSource?: DiscoverDataSource; + dataView?: DataView; + query?: Query | AggregateQuery; +} + +export interface DataSourceContext { + category: DataSourceCategory; +} + +export type DataSourceProfile = Profile; + +export class DataSourceProfileService extends AsyncProfileService< + DataSourceProfile, + DataSourceProfileProviderParams, + DataSourceContext +> { + constructor() { + super({ + profileId: 'default-data-source-profile', + category: DataSourceCategory.Default, + }); + } +} + +export type DataSourceProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts new file mode 100644 index 0000000000000..70b134da452e4 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts @@ -0,0 +1,41 @@ +/* + * 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 { DataTableRecord } from '@kbn/discover-utils'; +import type { Profile } from '../types'; +import { ProfileService } from '../profile_service'; + +export enum DocumentType { + Log = 'log', + Default = 'default', +} + +export interface DocumentProfileProviderParams { + record: DataTableRecord; +} + +export interface DocumentContext { + type: DocumentType; +} + +export type DocumentProfile = Omit; + +export class DocumentProfileService extends ProfileService< + DocumentProfile, + DocumentProfileProviderParams, + DocumentContext +> { + constructor() { + super({ + profileId: 'default-document-profile', + type: DocumentType.Default, + }); + } +} + +export type DocumentProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx new file mode 100644 index 0000000000000..3835337b25304 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx @@ -0,0 +1,123 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { + DataTableRecord, + getMessageFieldWithFallbacks, + LogDocumentOverview, +} from '@kbn/discover-utils'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { DataSourceType, isDataSourceType } from '../../../common/data_sources'; +import { DataSourceCategory, DataSourceProfileProvider } from './data_source_profile'; +import { DocumentProfileProvider, DocumentType } from './document_profile'; +import { RootProfileProvider, SolutionType } from './root_profile'; + +export const o11yRootProfileProvider: RootProfileProvider = { + profileId: 'o11y-root-profile', + profile: {}, + resolve: (params) => { + if (params.solutionNavId === 'oblt') { + return { + isMatch: true, + context: { + solutionType: SolutionType.Observability, + }, + }; + } + + return { isMatch: false }; + }, +}; + +export const logsDataSourceProfileProvider: DataSourceProfileProvider = { + profileId: 'logs-data-source-profile', + profile: { + getCellRenderers: (prev) => () => ({ + ...prev(), + '@timestamp': (props) => { + const timestamp = getFieldValue(props.row, '@timestamp'); + return ( + + {timestamp} + + ); + }, + 'log.level': (props) => { + const level = getFieldValue(props.row, 'log.level'); + if (!level) { + return (None); + } + const levelMap: Record = { + info: 'primary', + debug: 'default', + error: 'danger', + }; + return ( + + {capitalize(level)} + + ); + }, + message: (props) => { + const { value } = getMessageFieldWithFallbacks( + props.row.flattened as unknown as LogDocumentOverview + ); + return value || (None); + }, + }), + }, + resolve: (params) => { + let indices: string[] = []; + + if (isDataSourceType(params.dataSource, DataSourceType.Esql)) { + if (!isOfAggregateQueryType(params.query)) { + return { isMatch: false }; + } + + indices = getIndexPatternFromESQLQuery(params.query.esql).split(','); + } else if (isDataSourceType(params.dataSource, DataSourceType.DataView) && params.dataView) { + indices = params.dataView.getIndexPattern().split(','); + } + + if (indices.every((index) => index.startsWith('logs-'))) { + return { + isMatch: true, + context: { category: DataSourceCategory.Logs }, + }; + } + + return { isMatch: false }; + }, +}; + +export const logDocumentProfileProvider: DocumentProfileProvider = { + profileId: 'log-document-profile', + profile: {}, + resolve: (params) => { + if (getFieldValue(params.record, 'data_stream.type') === 'logs') { + return { + isMatch: true, + context: { + type: DocumentType.Log, + }, + }; + } + + return { isMatch: false }; + }, +}; + +const getFieldValue = (record: DataTableRecord, field: string) => { + const value = record.flattened[field]; + return Array.isArray(value) ? value[0] : value; +}; diff --git a/src/plugins/discover/public/context_awareness/profiles/index.ts b/src/plugins/discover/public/context_awareness/profiles/index.ts new file mode 100644 index 0000000000000..f661276b4a04c --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './root_profile'; +export * from './data_source_profile'; +export * from './document_profile'; diff --git a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts new file mode 100644 index 0000000000000..42497fe680c5c --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts @@ -0,0 +1,42 @@ +/* + * 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 { Profile } from '../types'; +import { AsyncProfileService } from '../profile_service'; + +export enum SolutionType { + Observability = 'oblt', + Security = 'security', + Search = 'search', + Default = 'default', +} + +export interface RootProfileProviderParams { + solutionNavId?: string | null; +} + +export interface RootContext { + solutionType: SolutionType; +} + +export type RootProfile = Profile; + +export class RootProfileService extends AsyncProfileService< + RootProfile, + RootProfileProviderParams, + RootContext +> { + constructor() { + super({ + profileId: 'default-root-profile', + solutionType: SolutionType.Default, + }); + } +} + +export type RootProfileProvider = Parameters[0]; diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts new file mode 100644 index 0000000000000..153ef979aabba --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts @@ -0,0 +1,263 @@ +/* + * 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 { firstValueFrom, Subject } from 'rxjs'; +import { createEsqlDataSource } from '../../common/data_sources'; +import { addLog } from '../utils/add_log'; +import { createContextAwarenessMocks } from './__mocks__'; + +jest.mock('../utils/add_log'); + +let mocks = createContextAwarenessMocks(); + +describe('ProfilesManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + mocks = createContextAwarenessMocks(); + }); + + it('should return default profiles', () => { + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should resolve root profile', async () => { + await mocks.profilesManagerMock.resolveRootProfile({}); + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]); + }); + + it('should resolve data source profile', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + const profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]); + }); + + it('should resolve document profile', async () => { + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles = mocks.profilesManagerMock.getProfiles({ record }); + expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); + }); + + it('should resolve multiple profiles', async () => { + await mocks.profilesManagerMock.resolveRootProfile({}); + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles = mocks.profilesManagerMock.getProfiles({ record }); + expect(profiles).toEqual([ + mocks.rootProfileProviderMock.profile, + mocks.dataSourceProfileProviderMock.profile, + mocks.documentProfileProviderMock.profile, + ]); + }); + + it('should expose profiles as an observable', async () => { + const getProfilesSpy = jest.spyOn(mocks.profilesManagerMock, 'getProfiles'); + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + const profiles$ = mocks.profilesManagerMock.getProfiles$({ record }); + const next = jest.fn(); + profiles$.subscribe(next); + expect(getProfilesSpy).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith([{}, {}, mocks.documentProfileProviderMock.profile]); + await mocks.profilesManagerMock.resolveRootProfile({}); + expect(getProfilesSpy).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledWith([ + mocks.rootProfileProviderMock.profile, + {}, + mocks.documentProfileProviderMock.profile, + ]); + await mocks.profilesManagerMock.resolveDataSourceProfile({}); + expect(getProfilesSpy).toHaveBeenCalledTimes(3); + expect(next).toHaveBeenCalledWith([ + mocks.rootProfileProviderMock.profile, + mocks.dataSourceProfileProviderMock.profile, + mocks.documentProfileProviderMock.profile, + ]); + }); + + it("should not resolve root profile again if params haven't changed", async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + }); + + it('should resolve root profile again if params have changed', async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' }); + expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(2); + }); + + it('should not resolve data source profile again if params have not changed', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + }); + + it('should resolve data source profile again if params have changed', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(2); + }); + + it('should log an error and fall back to the default profile if root profile resolution fails', async () => { + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' }); + let profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]); + const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve'); + resolveSpy.mockRejectedValue(new Error('Failed to resolve')); + await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] root context resolution failed with params: {\n "solutionNavId": "newSolutionNavId"\n}', + new Error('Failed to resolve') + ); + profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should log an error and fall back to the default profile if data source profile resolution fails', async () => { + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + let profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]); + const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve'); + resolveSpy.mockRejectedValue(new Error('Failed to resolve')); + await mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] data source context resolution failed with params: {\n "esqlQuery": "from logs-*"\n}', + new Error('Failed to resolve') + ); + profiles = mocks.profilesManagerMock.getProfiles(); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should log an error and fall back to the default profile if document profile resolution fails', () => { + const record = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock, + }); + let profiles = mocks.profilesManagerMock.getProfiles({ record }); + expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]); + const resolveSpy = jest.spyOn(mocks.documentProfileProviderMock, 'resolve'); + resolveSpy.mockImplementation(() => { + throw new Error('Failed to resolve'); + }); + const record2 = mocks.profilesManagerMock.resolveDocumentProfile({ + record: mocks.contextRecordMock2, + }); + profiles = mocks.profilesManagerMock.getProfiles({ record: record2 }); + expect(addLog).toHaveBeenCalledWith( + '[ProfilesManager] document context resolution failed with params: {\n "recordId": "logstash-2014.09.09::388::"\n}', + new Error('Failed to resolve') + ); + expect(profiles).toEqual([{}, {}, {}]); + }); + + it('should cancel existing root profile resolution when another is triggered', async () => { + const context = await mocks.rootProfileProviderMock.resolve({ solutionNavId: 'solutionNavId' }); + const newContext = await mocks.rootProfileProviderMock.resolve({ + solutionNavId: 'newSolutionNavId', + }); + const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve'); + resolveSpy.mockClear(); + const resolvedDeferredResult$ = new Subject(); + const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context); + resolveSpy.mockResolvedValueOnce(deferredResult); + const promise1 = mocks.profilesManagerMock.resolveRootProfile({ + solutionNavId: 'solutionNavId', + }); + expect(resolveSpy).toHaveReturnedTimes(1); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + const resolvedDeferredResult2$ = new Subject(); + const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext); + resolveSpy.mockResolvedValueOnce(deferredResult2); + const promise2 = mocks.profilesManagerMock.resolveRootProfile({ + solutionNavId: 'newSolutionNavId', + }); + expect(resolveSpy).toHaveReturnedTimes(2); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult2); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult$.next(undefined); + await promise1; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult2$.next(undefined); + await promise2; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([ + mocks.rootProfileProviderMock.profile, + {}, + {}, + ]); + }); + + it('should cancel existing data source profile resolution when another is triggered', async () => { + const context = await mocks.dataSourceProfileProviderMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + const newContext = await mocks.dataSourceProfileProviderMock.resolve({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve'); + resolveSpy.mockClear(); + const resolvedDeferredResult$ = new Subject(); + const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context); + resolveSpy.mockResolvedValueOnce(deferredResult); + const promise1 = mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from *' }, + }); + expect(resolveSpy).toHaveReturnedTimes(1); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + const resolvedDeferredResult2$ = new Subject(); + const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext); + resolveSpy.mockResolvedValueOnce(deferredResult2); + const promise2 = mocks.profilesManagerMock.resolveDataSourceProfile({ + dataSource: createEsqlDataSource(), + query: { esql: 'from logs-*' }, + }); + expect(resolveSpy).toHaveReturnedTimes(2); + expect(resolveSpy).toHaveLastReturnedWith(deferredResult2); + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult$.next(undefined); + await promise1; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]); + resolvedDeferredResult2$.next(undefined); + await promise2; + expect(mocks.profilesManagerMock.getProfiles()).toEqual([ + {}, + mocks.dataSourceProfileProviderMock.profile, + {}, + ]); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts new file mode 100644 index 0000000000000..316419d2a7d3f --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts @@ -0,0 +1,206 @@ +/* + * 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 { DataTableRecord } from '@kbn/discover-utils'; +import { isOfAggregateQueryType } from '@kbn/es-query'; +import { isEqual } from 'lodash'; +import { BehaviorSubject, combineLatest, map } from 'rxjs'; +import { DataSourceType, isDataSourceType } from '../../common/data_sources'; +import { addLog } from '../utils/add_log'; +import type { + RootProfileService, + DataSourceProfileService, + DocumentProfileService, + RootProfileProviderParams, + DataSourceProfileProviderParams, + DocumentProfileProviderParams, + RootContext, + DataSourceContext, + DocumentContext, +} from './profiles'; +import type { ContextWithProfileId } from './profile_service'; + +interface SerializedRootProfileParams { + solutionNavId: RootProfileProviderParams['solutionNavId']; +} + +interface SerializedDataSourceProfileParams { + dataViewId: string | undefined; + esqlQuery: string | undefined; +} + +interface DataTableRecordWithContext extends DataTableRecord { + context: ContextWithProfileId; +} + +export interface GetProfilesOptions { + record?: DataTableRecord; +} + +export class ProfilesManager { + private readonly rootContext$: BehaviorSubject>; + private readonly dataSourceContext$: BehaviorSubject>; + + private prevRootProfileParams?: SerializedRootProfileParams; + private prevDataSourceProfileParams?: SerializedDataSourceProfileParams; + private rootProfileAbortController?: AbortController; + private dataSourceProfileAbortController?: AbortController; + + constructor( + private readonly rootProfileService: RootProfileService, + private readonly dataSourceProfileService: DataSourceProfileService, + private readonly documentProfileService: DocumentProfileService + ) { + this.rootContext$ = new BehaviorSubject(rootProfileService.defaultContext); + this.dataSourceContext$ = new BehaviorSubject(dataSourceProfileService.defaultContext); + } + + public async resolveRootProfile(params: RootProfileProviderParams) { + const serializedParams = serializeRootProfileParams(params); + + if (isEqual(this.prevRootProfileParams, serializedParams)) { + return; + } + + const abortController = new AbortController(); + this.rootProfileAbortController?.abort(); + this.rootProfileAbortController = abortController; + + let context = this.rootProfileService.defaultContext; + + try { + context = await this.rootProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.Root, serializedParams, e); + } + + if (abortController.signal.aborted) { + return; + } + + this.rootContext$.next(context); + this.prevRootProfileParams = serializedParams; + } + + public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) { + const serializedParams = serializeDataSourceProfileParams(params); + + if (isEqual(this.prevDataSourceProfileParams, serializedParams)) { + return; + } + + const abortController = new AbortController(); + this.dataSourceProfileAbortController?.abort(); + this.dataSourceProfileAbortController = abortController; + + let context = this.dataSourceProfileService.defaultContext; + + try { + context = await this.dataSourceProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.DataSource, serializedParams, e); + } + + if (abortController.signal.aborted) { + return; + } + + this.dataSourceContext$.next(context); + this.prevDataSourceProfileParams = serializedParams; + } + + public resolveDocumentProfile(params: DocumentProfileProviderParams) { + let context: ContextWithProfileId | undefined; + + return new Proxy(params.record, { + has: (target, prop) => prop === 'context' || Reflect.has(target, prop), + get: (target, prop, receiver) => { + if (prop !== 'context') { + return Reflect.get(target, prop, receiver); + } + + if (!context) { + try { + context = this.documentProfileService.resolve(params); + } catch (e) { + logResolutionError(ContextType.Document, { recordId: params.record.id }, e); + context = this.documentProfileService.defaultContext; + } + } + + return context; + }, + }); + } + + public getProfiles({ record }: GetProfilesOptions = {}) { + return [ + this.rootProfileService.getProfile(this.rootContext$.getValue()), + this.dataSourceProfileService.getProfile(this.dataSourceContext$.getValue()), + this.documentProfileService.getProfile( + recordHasContext(record) ? record.context : this.documentProfileService.defaultContext + ), + ]; + } + + public getProfiles$(options: GetProfilesOptions = {}) { + return combineLatest([this.rootContext$, this.dataSourceContext$]).pipe( + map(() => this.getProfiles(options)) + ); + } +} + +const serializeRootProfileParams = ( + params: RootProfileProviderParams +): SerializedRootProfileParams => { + return { + solutionNavId: params.solutionNavId, + }; +}; + +const serializeDataSourceProfileParams = ( + params: DataSourceProfileProviderParams +): SerializedDataSourceProfileParams => { + return { + dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView) + ? params.dataSource.dataViewId + : undefined, + esqlQuery: + isDataSourceType(params.dataSource, DataSourceType.Esql) && + isOfAggregateQueryType(params.query) + ? params.query.esql + : undefined, + }; +}; + +const recordHasContext = ( + record: DataTableRecord | undefined +): record is DataTableRecordWithContext => { + return Boolean(record && 'context' in record); +}; + +enum ContextType { + Root = 'root', + DataSource = 'data source', + Document = 'document', +} + +const logResolutionError = ( + profileType: ContextType, + params: TParams, + error: TError +) => { + addLog( + `[ProfilesManager] ${profileType} context resolution failed with params: ${JSON.stringify( + params, + null, + 2 + )}`, + error + ); +}; diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts new file mode 100644 index 0000000000000..b612b2ce29907 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { CustomCellRenderer } from '@kbn/unified-data-table'; + +export interface Profile { + getCellRenderers: () => CustomCellRenderer; +} diff --git a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts index 6ede54673cda9..1fabe661dd20e 100644 --- a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts +++ b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts @@ -6,12 +6,6 @@ * Side Public License, v 1. */ -import type { DiscoverCustomizationContext } from '../types'; +import { defaultCustomizationContext } from '../defaults'; -export const mockCustomizationContext: DiscoverCustomizationContext = { - displayMode: 'standalone', - inlineTopNav: { - enabled: false, - showLogsExplorerTabs: false, - }, -}; +export const mockCustomizationContext = defaultCustomizationContext; diff --git a/src/plugins/discover/public/customizations/defaults.ts b/src/plugins/discover/public/customizations/defaults.ts index a9dc60ac356ff..034e7be2b5dc6 100644 --- a/src/plugins/discover/public/customizations/defaults.ts +++ b/src/plugins/discover/public/customizations/defaults.ts @@ -9,6 +9,7 @@ import { DiscoverCustomizationContext } from './types'; export const defaultCustomizationContext: DiscoverCustomizationContext = { + solutionNavId: null, displayMode: 'standalone', inlineTopNav: { enabled: false, diff --git a/src/plugins/discover/public/customizations/types.ts b/src/plugins/discover/public/customizations/types.ts index 079cde37da716..21419da709946 100644 --- a/src/plugins/discover/public/customizations/types.ts +++ b/src/plugins/discover/public/customizations/types.ts @@ -21,6 +21,10 @@ export type CustomizationCallback = ( export type DiscoverDisplayMode = 'embedded' | 'standalone'; export interface DiscoverCustomizationContext { + /** + * The current solution nav ID + */ + solutionNavId: string | null; /* * Display mode in which discover is running */ diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 35e336df52325..53ce8c798f251 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -9,6 +9,7 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { createDataViewDataSource } from '../../common/data_sources'; import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -17,7 +18,7 @@ import { ReactWrapper } from 'enzyme'; import { ReactElement } from 'react'; import { render } from 'react-dom'; import { act } from 'react-dom/test-utils'; -import { Observable, throwError } from 'rxjs'; +import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { SearchInput } from '..'; import { VIEW_MODE } from '../../common/constants'; import { DiscoverServices } from '../build_services'; @@ -26,6 +27,7 @@ import { discoverServiceMock } from '../__mocks__/services'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; +import { DiscoverGrid } from '../components/discover_grid'; jest.mock('./get_discover_locator_params', () => { const actual = jest.requireActual('./get_discover_locator_params'); @@ -140,6 +142,7 @@ describe('saved search embeddable', () => { }; beforeEach(() => { + jest.clearAllMocks(); mountpoint = document.createElement('div'); showFieldStatisticsMockValue = false; @@ -152,6 +155,10 @@ describe('saved search embeddable', () => { if (key === SHOW_FIELD_STATISTICS) return showFieldStatisticsMockValue; } ); + + jest + .spyOn(servicesMock.core.chrome, 'getActiveSolutionNavId$') + .mockReturnValue(new BehaviorSubject('test')); }); afterEach(() => { @@ -475,4 +482,56 @@ describe('saved search embeddable', () => { expect(editUrl).toBe('/base/mock-url'); }); }); + + describe('context awareness', () => { + it('should resolve root profile on init', async () => { + const resolveRootProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveRootProfile' + ); + const { embeddable } = createEmbeddable(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + await waitOneTick(); + expect(resolveRootProfileSpy).toHaveBeenCalledWith({ solutionNavId: 'test' }); + resolveRootProfileSpy.mockReset(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + embeddable.reload(); + await waitOneTick(); + expect(resolveRootProfileSpy).not.toHaveBeenCalled(); + }); + + it('should resolve data source profile when fetching', async () => { + const resolveDataSourceProfileSpy = jest.spyOn( + discoverServiceMock.profilesManager, + 'resolveDataSourceProfile' + ); + const { embeddable } = createEmbeddable(); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); + await waitOneTick(); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), + dataView: dataViewMock, + query: embeddable.getInput().query, + }); + resolveDataSourceProfileSpy.mockReset(); + expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); + embeddable.reload(); + expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({ + dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }), + dataView: dataViewMock, + query: embeddable.getInput().query, + }); + }); + + it('should pass cell renderers from profile', async () => { + const { embeddable } = createEmbeddable(); + await waitOneTick(); + embeddable.render(mountpoint); + const discoverGridComponent = discoverComponent.find(DiscoverGrid); + expect(discoverGridComponent.exists()).toBeTruthy(); + expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([ + 'rootProfile', + ]); + }); + }); }); diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 3a6f9f9c9c8ac..861a0d50eeba6 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { lastValueFrom, Subscription } from 'rxjs'; +import { firstValueFrom, lastValueFrom, Subscription } from 'rxjs'; import { onlyDisabledFiltersChanged, Filter, @@ -71,6 +71,7 @@ import { fetchEsql } from '../application/main/data_fetching/fetch_esql'; import { getValidViewMode } from '../application/main/utils/get_valid_view_mode'; import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../constants'; import { getDiscoverLocatorParams } from './get_discover_locator_params'; +import { createDataViewDataSource, createEsqlDataSource } from '../../common/data_sources'; export interface SearchEmbeddableConfig { editable: boolean; @@ -163,6 +164,12 @@ export class SavedSearchEmbeddable await this.initializeOutput(); + const solutionNavId = await firstValueFrom( + this.services.core.chrome.getActiveSolutionNavId$() + ); + + await this.services.profilesManager.resolveRootProfile({ solutionNavId }); + // deferred loading of this embeddable is complete this.setInitializationFinished(); @@ -305,18 +312,29 @@ export class SavedSearchEmbeddable const isEsqlMode = this.isEsqlMode(savedSearch); try { + await this.services.profilesManager.resolveDataSourceProfile({ + dataSource: isOfAggregateQueryType(query) + ? createEsqlDataSource() + : dataView.id + ? createDataViewDataSource({ dataViewId: dataView.id }) + : undefined, + dataView, + query, + }); + // Request ES|QL data if (isEsqlMode && query) { - const result = await fetchEsql( - savedSearch.searchSource.getField('query')!, + const result = await fetchEsql({ + query: savedSearch.searchSource.getField('query')!, + inputQuery: this.input.query, + filters: this.input.filters, dataView, - this.services.data, - this.services.expressions, - this.services.inspector, - this.abortController.signal, - this.input.filters, - this.input.query - ); + abortSignal: this.abortController.signal, + inspectorAdapters: this.services.inspector, + data: this.services.data, + expressions: this.services.expressions, + profilesManager: this.services.profilesManager, + }); this.updateOutput({ ...this.getOutput(), diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 8bf43fa5b3e3b..39a6dc1307c04 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -21,6 +21,7 @@ import './saved_search_grid.scss'; import { DiscoverGridFlyout } from '../components/discover_grid_flyout'; import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; +import { useProfileAccessor } from '../context_awareness'; export interface DiscoverGridEmbeddableProps extends Omit { @@ -88,6 +89,12 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { [props.totalHitCount] ); + const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); + const cellRenderers = useMemo(() => { + const getCellRenderers = getCellRenderersAccessor(() => ({})); + return getCellRenderers(); + }, [getCellRenderersAccessor]); + return ( { it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => { const action = new ViewSavedSearchAction(applicationMock, services.locator); diff --git a/src/plugins/discover/public/hooks/show_confirm_panel.tsx b/src/plugins/discover/public/hooks/show_confirm_panel.tsx deleted file mode 100644 index 79d2524c93161..0000000000000 --- a/src/plugins/discover/public/hooks/show_confirm_panel.tsx +++ /dev/null @@ -1,71 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { EuiConfirmModal } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { StartRenderServices } from '../plugin'; - -let isOpenConfirmPanel = false; - -export const showConfirmPanel = ({ - onConfirm, - onCancel, - startServices, -}: { - onConfirm: () => void; - onCancel: () => void; - startServices: StartRenderServices; -}) => { - if (isOpenConfirmPanel) { - return; - } - - isOpenConfirmPanel = true; - const container = document.createElement('div'); - const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpenConfirmPanel = false; - }; - - document.body.appendChild(container); - const element = ( - - { - onClose(); - onCancel(); - }} - onConfirm={() => { - onClose(); - onConfirm(); - }} - cancelButtonText={i18n.translate('discover.confirmDataViewSave.cancel', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('discover.confirmDataViewSave.saveAndContinue', { - defaultMessage: 'Save and continue', - })} - defaultFocusedButton="confirm" - > -

- {i18n.translate('discover.confirmDataViewSave.message', { - defaultMessage: 'The action you chose requires a saved data view.', - })} -

-
-
- ); - ReactDOM.render(element, container); -}; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 2eb34b20345e4..7228070fe2d2c 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -82,6 +82,12 @@ import { import { getESQLSearchProvider } from './global_search/search_provider'; import { HistoryService } from './history_service'; import { ConfigSchema, ExperimentalFeatures } from '../common/config'; +import { + DataSourceProfileService, + DocumentProfileService, + ProfilesManager, + RootProfileService, +} from './context_awareness'; /** * @public @@ -209,8 +215,6 @@ export interface DiscoverStartPlugins { observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } -export type StartRenderServices = Pick; - /** * Contains Discover, one of the oldest parts of Kibana * Discover provides embeddables for Dashboards @@ -218,25 +222,28 @@ export type StartRenderServices = Pick { - constructor(private readonly initializerContext: PluginInitializerContext) { - this.experimentalFeatures = - initializerContext.config.get().experimental ?? this.experimentalFeatures; - } + private readonly rootProfileService = new RootProfileService(); + private readonly dataSourceProfileService = new DataSourceProfileService(); + private readonly documentProfileService = new DocumentProfileService(); + private readonly appStateUpdater = new BehaviorSubject(() => ({})); + private readonly historyService = new HistoryService(); + private readonly inlineTopNav: Map = + new Map([[null, defaultCustomizationContext.inlineTopNav]]); + private readonly experimentalFeatures: ExperimentalFeatures = { + ruleFormV2Enabled: false, + }; - private appStateUpdater = new BehaviorSubject(() => ({})); - private historyService = new HistoryService(); private scopedHistory?: ScopedHistory; private urlTracker?: UrlTracker; - private stopUrlTracking: (() => void) | undefined = undefined; + private stopUrlTracking?: () => void; private locator?: DiscoverAppLocator; private contextLocator?: DiscoverContextAppLocator; private singleDocLocator?: DiscoverSingleDocLocator; - private inlineTopNav: Map = new Map([ - [null, defaultCustomizationContext.inlineTopNav], - ]); - private experimentalFeatures: ExperimentalFeatures = { - ruleFormV2Enabled: false, - }; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.experimentalFeatures = + initializerContext.config.get().experimental ?? this.experimentalFeatures; + } setup( core: CoreSetup, @@ -331,6 +338,7 @@ export class DiscoverPlugin history: this.historyService.getHistory(), scopedHistory: this.scopedHistory, urlTracker: this.urlTracker!, + profilesManager: this.createProfilesManager(), setHeaderActionMenu: params.setHeaderActionMenu, }); @@ -344,10 +352,11 @@ export class DiscoverPlugin const customizationContext$: Observable = services.chrome .getActiveSolutionNavId$() .pipe( - map((navId) => ({ + map((solutionNavId) => ({ ...defaultCustomizationContext, + solutionNavId, inlineTopNav: - this.inlineTopNav.get(navId) ?? + this.inlineTopNav.get(solutionNavId) ?? this.inlineTopNav.get(null) ?? defaultCustomizationContext.inlineTopNav, })) @@ -412,10 +421,7 @@ export class DiscoverPlugin } start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart { - // we need to register the application service at setup, but to render it - // there are some start dependencies necessary, for this reason - // initializeServices are assigned at start and used - // when the application/embeddable is mounted + this.registerProfiles(); const viewSavedSearchAction = new ViewSavedSearchAction(core.application, this.locator!); @@ -423,7 +429,6 @@ export class DiscoverPlugin plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER); injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT)); - const getDiscoverServicesInternal = () => this.getDiscoverServices(core, plugins); const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL); if (plugins.share && this.locator && isEsqlEnabled) { @@ -435,6 +440,10 @@ export class DiscoverPlugin ); } + const getDiscoverServicesInternal = () => { + return this.getDiscoverServices(core, plugins, this.createEmptyProfilesManager()); + }; + return { locator: this.locator, DiscoverContainer: (props: DiscoverContainerProps) => ( @@ -449,7 +458,34 @@ export class DiscoverPlugin } } - private getDiscoverServices = (core: CoreStart, plugins: DiscoverStartPlugins) => { + private registerProfiles() { + // TODO: Conditionally register example profiles for functional testing in a follow up PR + // this.rootProfileService.registerProvider(o11yRootProfileProvider); + // this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider); + // this.documentProfileService.registerProvider(logDocumentProfileProvider); + } + + private createProfilesManager() { + return new ProfilesManager( + this.rootProfileService, + this.dataSourceProfileService, + this.documentProfileService + ); + } + + private createEmptyProfilesManager() { + return new ProfilesManager( + new RootProfileService(), + new DataSourceProfileService(), + new DocumentProfileService() + ); + } + + private getDiscoverServices = ( + core: CoreStart, + plugins: DiscoverStartPlugins, + profilesManager = this.createProfilesManager() + ) => { return buildServices({ core, plugins, @@ -459,6 +495,7 @@ export class DiscoverPlugin singleDocLocator: this.singleDocLocator!, history: this.historyService.getHistory(), urlTracker: this.urlTracker!, + profilesManager, }); }; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx index 465f1f3f66eff..484af6e4a0809 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx @@ -22,12 +22,12 @@ export function createLogAIAssistant({ export const createLogsAIAssistantRenderer = (LogAIAssistantRender: ReturnType) => ({ doc }: ObservabilityLogsAIAssistantFeatureRenderDeps) => { - const mappedDoc = useMemo( + const mappedDoc = useMemo( () => ({ fields: Object.entries(doc.flattened).map(([field, value]) => ({ field, - value, - })) as LogAIAssistantDocument['fields'], + value: Array.isArray(value) ? value : [value], + })), }), [doc] ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 09336d56965fa..f4067130e1346 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2322,10 +2322,6 @@ "discover.backToTopLinkText": "Revenir en haut de la page.", "discover.badge.readOnly.text": "Lecture seule", "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches", - "discover.confirmDataViewSave.cancel": "Annuler", - "discover.confirmDataViewSave.message": "L'action que vous avez choisie requiert une vue de données enregistrée.", - "discover.confirmDataViewSave.saveAndContinue": "Enregistrer et continuer", - "discover.confirmDataViewSave.title": "Enregistrer la vue de données", "discover.context.breadcrumb": "Documents relatifs", "discover.context.failedToLoadAnchorDocumentDescription": "Échec de chargement du document ancré", "discover.context.failedToLoadAnchorDocumentErrorDescription": "Le document ancré n’a pas pu être chargé.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 55589d731e969..f18f09518aa37 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2319,10 +2319,6 @@ "discover.backToTopLinkText": "最上部へ戻る。", "discover.badge.readOnly.text": "読み取り専用", "discover.badge.readOnly.tooltip": "検索を保存できません", - "discover.confirmDataViewSave.cancel": "キャンセル", - "discover.confirmDataViewSave.message": "選択したアクションでは、保存されたデータビューが必要です。", - "discover.confirmDataViewSave.saveAndContinue": "保存して続行", - "discover.confirmDataViewSave.title": "データビューを保存", "discover.context.breadcrumb": "周りのドキュメント", "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました", "discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 65cf2dad16d19..90699177a60b4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2323,10 +2323,6 @@ "discover.backToTopLinkText": "返回顶部。", "discover.badge.readOnly.text": "只读", "discover.badge.readOnly.tooltip": "无法保存搜索", - "discover.confirmDataViewSave.cancel": "取消", - "discover.confirmDataViewSave.message": "您选择的操作需要已保存的数据视图。", - "discover.confirmDataViewSave.saveAndContinue": "保存并继续", - "discover.confirmDataViewSave.title": "保存数据视图。", "discover.context.breadcrumb": "周围文档", "discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档", "discover.context.failedToLoadAnchorDocumentErrorDescription": "无法加载定位点文档。", From 5a9bfd0f1f0b8c40e54d37ca8453ef20f1ab696e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 10 Jun 2024 16:23:51 +0200 Subject: [PATCH 04/14] fix: [Obs Applications > Services][KEYBOARD]: "What are these metrics?" tooltip icon must receive keyboard focus (#183808) Closes: https://github.com/elastic/observability-dev/issues/3402 ## Description The Obs Applications > Services view has a "What are these metrics?" icon that exposes a tooltip when hovered, but that tooltip cannot take keyboard focus, making it unavailable to keyboard users. Screenshot attached below. ### Steps to recreate 1. Open the [Obs Services](https://keepserverless-qa-oblt-b4ba07.kb.eu-west-1.aws.qa.elastic.cloud/app/apm/services) view 2. Tab through the table, until focus is on an element after the "What are these metrics" question mark icon 6. Verify the icons in the table row(s) never receive focus and the tooltips never become visible ### What was changed?: 1. `EuiToolTip` -> `EuiIconTip` ### Screen: https://github.com/elastic/kibana/assets/20072247/3f9ea6f5-1e5e-4640-9ce8-6c390532cdd2 --- .../app/service_inventory/service_list/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/service_list/index.tsx index 49850707cb804..7a01e5d6bc166 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/service_list/index.tsx @@ -445,15 +445,15 @@ export function ServiceList({ )} - - - + /> From c12325078494e8f0f0dd11ca59e9d43ec6fe355e Mon Sep 17 00:00:00 2001 From: dkirchan <55240027+dkirchan@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:30:57 +0300 Subject: [PATCH 05/14] [Security] [Serverless] Fixed quality gates parallelism for Cypress (#184781) In this PR a chain of pipeline structures is introduced. For each different solution team, depending on the flag KIBANA_MKI_QUALITY_GATE, if it is '1' it uploads the respective team pipeline from the path `.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate` otherwise it uses the respective pipeline from the path `.buildkite/pipelines/security_solution_quality_gate/mki_periodic`. For the quality gate, the cypress tests will be using for now the level of parallelism equal to 1 as not many tests are yet enabled. For the periodic pipeline the original level of parallelism is respected. Co-authored-by: Gloria Hornero --- .../mki_periodic_defend_workflows.yml | 18 + .../mki_periodic_detection_engine.yml | 254 +++++++ .../mki_periodic_entity_analytics.yml | 52 ++ .../mki_periodic/mki_periodic_explore.yml | 19 + .../mki_periodic/mki_periodic_gen_ai.yml | 67 ++ .../mki_periodic_investigations.yml | 19 + .../mki_periodic_rule_management.yml | 329 +++++++++ .../mki_quality_gate_defend_workflows.yml | 18 + .../mki_quality_gate_detection_engine.yml | 254 +++++++ .../mki_quality_gate_entity_analytics.yml | 52 ++ .../mki_quality_gate_explore.yml | 19 + .../mki_quality_gate_gen_ai.yml | 67 ++ .../mki_quality_gate_investigations.yml | 19 + .../mki_quality_gate_rule_management.yml | 329 +++++++++ ...mki_security_solution_defend_workflows.yml | 24 +- ...mki_security_solution_detection_engine.yml | 496 +------------ ...mki_security_solution_entity_analytics.yml | 90 +-- .../mki_security_solution_explore.yml | 25 +- .../mki_security_solution_gen_ai.yml | 122 +--- .../mki_security_solution_investigations.yml | 25 +- .../mki_security_solution_rule_management.yml | 656 +----------------- 21 files changed, 1561 insertions(+), 1393 deletions(-) create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_detection_engine.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_entity_analytics.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_explore.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_gen_ai.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_investigations.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_rule_management.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_defend_workflows.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_detection_engine.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_entity_analytics.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_explore.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_gen_ai.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_investigations.yml create mode 100644 .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml new file mode 100644 index 0000000000000..a20d3c709223f --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml @@ -0,0 +1,18 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh cypress:dw:qa:serverless:run + label: "Serverless MKI QA Defend Workflows Cypress Tests on Serverless" + key: test_defend_workflows + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 300 + parallelism: 6 + retry: + automatic: + - exit_status: "*" + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_detection_engine.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_detection_engine.yml new file mode 100644 index 0000000000000..aee2f92b712be --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_detection_engine.yml @@ -0,0 +1,254 @@ +steps: + - group: "Serverless MKI QA Detection Engine - Cypress Tests" + key: cypress_test_detections_engine + steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine + label: "Serverless MKI QA Detection Engine - Security Solution Cypress Tests" + key: test_detection_engine + env: + BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 8 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine:exceptions + label: "Serverless MKI QA Detection Engine - Exceptions - Security Solution Cypress Tests" + key: test_detection_engine_exceptions + env: + BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 6 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA Detection Engine - API Integration" + key: api_test_detections_engine + steps: + - label: Running exception_lists_items:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_lists_items:qa:serverless + key: exception_lists_items:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running lists_items:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh lists_items:qa:serverless + key: lists_items:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running user_roles:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh user_roles:qa:serverless + key: user_roles:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running telemetry:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh telemetry:qa:serverless + key: telemetry:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_workflows:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_workflows:essentials:qa:serverless + key: exception_workflows:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_date_numeric_types:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_date_numeric_types:essentials:qa:serverless + key: exception_operators_date_numeric_types:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_keyword:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_keyword:essentials:qa:serverless + key: exception_operators_keyword:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_ips:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_ips:essentials:qa:serverless + key: exception_operators_ips:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_long:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_long:essentials:qa:serverless + key: exception_operators_long:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_text:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_text:essentials:qa:serverless + key: exception_operators_text:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running actions:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh actions:qa:serverless + key: actions:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running alerts:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:qa:serverless + key: alerts:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running alerts:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:essentials:qa:serverless + key: alerts:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_execution_logic:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_execution_logic:qa:serverless + key: rule_execution_logic:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_entity_analytics.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_entity_analytics.yml new file mode 100644 index 0000000000000..238da924ffd24 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_entity_analytics.yml @@ -0,0 +1,52 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:entity_analytics + label: 'Serverless MKI QA Entity Analytics - Security Solution Cypress Tests' + key: test_entity_analytics + env: + BK_TEST_SUITE_KEY: "serverless-cypress-entity-analytics" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 2 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - group: "Serverless MKI QA Entity Analytics - API Integration" + key: api_test_entity_analytics + steps: + - label: Running entity_analytics:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:qa:serverless + key: entity_analytics:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running entity_analytics:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless + key: entity_analytics:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_explore.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_explore.yml new file mode 100644 index 0000000000000..e35f6004ad3e5 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_explore.yml @@ -0,0 +1,19 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:explore + key: test_explore + label: 'Serverless MKI QA Explore - Security Solution Cypress Tests' + env: + BK_TEST_SUITE_KEY: "serverless-cypress-explore" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 4 + retry: + automatic: + - exit_status: '-1' + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_gen_ai.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_gen_ai.yml new file mode 100644 index 0000000000000..d6ce8b4a80eb2 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_gen_ai.yml @@ -0,0 +1,67 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:ai_assistant + label: "Serverless MKI QA AI Assistant - Security Solution Cypress Tests" + key: test_ai_assistant + env: + BK_TEST_SUITE_KEY: "serverless-cypress-gen-ai" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA AI Assistant - API Integration" + key: api_test_ai_assistant + steps: + - label: Running genai:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh genai:qa:serverless + key: genai:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running nlp_cleanup_task:complete:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:complete:qa:serverless + key: nlp_cleanup_task:complete:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running nlp_cleanup_task:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:essentials:qa:serverless + key: nlp_cleanup_task:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_investigations.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_investigations.yml new file mode 100644 index 0000000000000..caa788853c11e --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_investigations.yml @@ -0,0 +1,19 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:investigations + key: test_investigations + label: 'Serverless MKI QA Investigations - Security Solution Cypress Tests' + env: + BK_TEST_SUITE_KEY: "serverless-cypress-investigations" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 8 + retry: + automatic: + - exit_status: '-1' + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_rule_management.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_rule_management.yml new file mode 100644 index 0000000000000..428325ec0a1d0 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_rule_management.yml @@ -0,0 +1,329 @@ +steps: + - group: "Serverless MKI QA Rule Management - Cypress Test" + key: cypress_test_rule_management + steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management + label: "Serverless MKI QA Rule Management - Security Solution Cypress Tests" + key: test_rule_management + env: + BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 8 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules + label: "Serverless MKI QA Rule Management - Prebuilt Rules - Security Solution Cypress Tests" + key: test_rule_management_prebuilt_rules + env: + BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 4 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA Rule Management - API Integration" + key: api_test_rule_management + steps: + - label: Running rule_creation:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:qa:serverless + key: rule_creation:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_creation:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:essentials:qa:serverless + key: rule_creation:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_update:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:qa:serverless + key: rule_update:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_update:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:essentials:qa:serverless + key: rule_update:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_patch:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:qa:serverless + key: rule_patch:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_patch:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:essentials:qa:serverless + key: rule_patch:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_management:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_management:qa:serverless + key: prebuilt_rules_management:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless + key: prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_large_prebuilt_rules_package:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_large_prebuilt_rules_package:qa:serverless + key: prebuilt_rules_large_prebuilt_rules_package:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_update_prebuilt_rules_package:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_update_prebuilt_rules_package:qa:serverless + key: prebuilt_rules_update_prebuilt_rules_package:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_delete:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:qa:serverless + key: rule_delete:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_delete:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:essentials:qa:serverless + key: rule_delete:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_import_export:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:qa:serverless + key: rule_import_export:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_import_export:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:essentials:qa:serverless + key: rule_import_export:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_management:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_management:qa:serverless + key: rule_management:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_bulk_actions:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_bulk_actions:qa:serverless + key: rule_bulk_actions:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_read:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:qa:serverless + key: rule_read:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_read:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:essentials:qa:serverless + key: rule_read:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rules_management:essentials:qa:serverless + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rules_management:essentials:qa:serverless + key: rules_management:essentials:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_defend_workflows.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_defend_workflows.yml new file mode 100644 index 0000000000000..96761bb5e9d7f --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_defend_workflows.yml @@ -0,0 +1,18 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh cypress:dw:qa:serverless:run + label: 'Serverless MKI QA Defend Workflows Cypress Tests on Serverless' + key: test_defend_workflows + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: '*' + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_detection_engine.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_detection_engine.yml new file mode 100644 index 0000000000000..a44847c52b05e --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_detection_engine.yml @@ -0,0 +1,254 @@ +steps: + - group: "Serverless MKI QA Detection Engine - Cypress Tests" + key: cypress_test_detections_engine + steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine + label: "Serverless MKI QA Detection Engine - Security Solution Cypress Tests" + key: test_detection_engine + env: + BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine:exceptions + label: "Serverless MKI QA Detection Engine - Exceptions - Security Solution Cypress Tests" + key: test_detection_engine_exceptions + env: + BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA Detection Engine - API Integration" + key: api_test_detections_engine + steps: + - label: Running exception_lists_items:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_lists_items:qa:serverless:release + key: exception_lists_items:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running lists_items:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless:release + key: lists_items:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running user_roles:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh user_roles:qa:serverless:release + key: user_roles:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running telemetry:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh telemetry:qa:serverless:release + key: telemetry:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_workflows:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_workflows:essentials:qa:serverless:release + key: exception_workflows:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_date_numeric_types:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_date_numeric_types:essentials:qa:serverless:release + key: exception_operators_date_numeric_types:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_keyword:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_keyword:essentials:qa:serverless:release + key: exception_operators_keyword:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_ips:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_ips:essentials:qa:serverless:release + key: exception_operators_ips:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_long:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_long:essentials:qa:serverless:release + key: exception_operators_long:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running exception_operators_text:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_text:essentials:qa:serverless:release + key: exception_operators_text:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running actions:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh actions:qa:serverless:release + key: actions:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running alerts:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:qa:serverless:release + key: alerts:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running alerts:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:essentials:qa:serverless:release + key: alerts:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_execution_logic:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_execution_logic:qa:serverless:release + key: rule_execution_logic:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_entity_analytics.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_entity_analytics.yml new file mode 100644 index 0000000000000..a3552645ac531 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_entity_analytics.yml @@ -0,0 +1,52 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:entity_analytics + label: 'Serverless MKI QA Entity Analytics - Security Solution Cypress Tests' + key: test_entity_analytics + env: + BK_TEST_SUITE_KEY: "serverless-cypress-entity-analytics" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 + + - group: "Serverless MKI QA Entity Analytics - API Integration" + key: api_test_entity_analytics + steps: + - label: Running entity_analytics:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:qa:serverless:release + key: entity_analytics:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running entity_analytics:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless:release + key: entity_analytics:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_explore.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_explore.yml new file mode 100644 index 0000000000000..e51e06a8a0543 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_explore.yml @@ -0,0 +1,19 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:explore + key: test_explore + label: 'Serverless MKI QA Explore - Security Solution Cypress Tests' + env: + BK_TEST_SUITE_KEY: "serverless-cypress-explore" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_gen_ai.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_gen_ai.yml new file mode 100644 index 0000000000000..60677728a0481 --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_gen_ai.yml @@ -0,0 +1,67 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:ai_assistant + label: "Serverless MKI QA AI Assistant - Security Solution Cypress Tests" + key: test_ai_assistant + env: + BK_TEST_SUITE_KEY: "serverless-cypress-gen-ai" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA AI Assistant - API Integration" + key: api_test_ai_assistant + steps: + - label: Running genai:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh genai:qa:serverless:release + key: genai:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running nlp_cleanup_task:complete:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:complete:qa:serverless:release + key: nlp_cleanup_task:complete:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running nlp_cleanup_task:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:essentials:qa:serverless:release + key: nlp_cleanup_task:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_investigations.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_investigations.yml new file mode 100644 index 0000000000000..5e5707ad2ea8f --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_investigations.yml @@ -0,0 +1,19 @@ +steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:investigations + key: test_investigations + label: 'Serverless MKI QA Investigations - Security Solution Cypress Tests' + env: + BK_TEST_SUITE_KEY: "serverless-cypress-investigations" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: '-1' + limit: 1 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml new file mode 100644 index 0000000000000..ca13baa0bd2ad --- /dev/null +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml @@ -0,0 +1,329 @@ +steps: + - group: "Serverless MKI QA Rule Management - Cypress Test" + key: cypress_test_rule_management + steps: + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management + label: "Serverless MKI QA Rule Management - Security Solution Cypress Tests" + key: test_rule_management + env: + BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules + label: "Serverless MKI QA Rule Management - Prebuilt Rules - Security Solution Cypress Tests" + key: test_rule_management_prebuilt_rules + env: + BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. + timeout_in_minutes: 300 + parallelism: 1 + retry: + automatic: + - exit_status: "-1" + limit: 1 + + - group: "Serverless MKI QA Rule Management - API Integration" + key: api_test_rule_management + steps: + - label: Running rule_creation:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:qa:serverless:release + key: rule_creation:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_creation:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:essentials:qa:serverless:release + key: rule_creation:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_update:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:qa:serverless:release + key: rule_update:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_update:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:essentials:qa:serverless:release + key: rule_update:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_patch:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:qa:serverless:release + key: rule_patch:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_patch:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:essentials:qa:serverless:release + key: rule_patch:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_management:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_management:qa:serverless:release + key: prebuilt_rules_management:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release + key: prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release + key: prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release + key: prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_delete:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:qa:serverless:release + key: rule_delete:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_delete:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:essentials:qa:serverless:release + key: rule_delete:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_import_export:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:qa:serverless:release + key: rule_import_export:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_import_export:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:essentials:qa:serverless:release + key: rule_import_export:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_management:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_management:qa:serverless:release + key: rule_management:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_bulk_actions:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_bulk_actions:qa:serverless:release + key: rule_bulk_actions:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_read:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:qa:serverless:release + key: rule_read:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rule_read:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:essentials:qa:serverless:release + key: rule_read:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 + + - label: Running rules_management:essentials:qa:serverless:release + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rules_management:essentials:qa:serverless:release + key: rules_management:essentials:qa:serverless:release + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + machineType: n2-standard-4 + preemptible: true + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 2 diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_defend_workflows.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_defend_workflows.yml index ec829a5dd4e45..27fc5ef19a5c6 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_defend_workflows.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_defend_workflows.yml @@ -14,20 +14,10 @@ steps: - exit_status: "*" limit: 1 - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh cypress:dw:qa:serverless:run - label: 'Serverless MKI QA Defend Workflows Cypress Tests on Serverless' - key: test_defend_workflows - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - enableNestedVirtualization: true - localSsds: 1 - localSsdInterface: nvme - machineType: n2-standard-4 - timeout_in_minutes: 300 - parallelism: 6 - retry: - automatic: - - exit_status: '*' - limit: 1 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_defend_workflows.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Defend Workflows pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" + + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Defend Workflows Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_detection_engine.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_detection_engine.yml index 9e34569d6a808..6226b427526e7 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_detection_engine.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_detection_engine.yml @@ -14,494 +14,10 @@ steps: - exit_status: "*" limit: 1 - - group: "Serverless MKI QA Detection Engine - Cypress Tests" - key: cypress_test_detections_engine - steps: - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine - label: "Serverless MKI QA Detection Engine - Security Solution Cypress Tests" - key: test_detection_engine - env: - BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 8 - retry: - automatic: - - exit_status: "-1" - limit: 1 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_detection_engine.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Detection Engine pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:detection_engine:exceptions - label: "Serverless MKI QA Detection Engine - Exceptions - Security Solution Cypress Tests" - key: test_detection_engine_exceptions - env: - BK_TEST_SUITE_KEY: "serverless-cypress-detection-engine" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 6 - retry: - automatic: - - exit_status: "-1" - limit: 1 - - - group: "Serverless MKI QA Detection Engine - API Integration" - key: api_test_detections_engine - steps: - - label: Running exception_lists_items:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_lists_items:qa:serverless - key: exception_lists_items:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_lists_items:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_lists_items:qa:serverless:release - key: exception_lists_items:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running lists_items:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh lists_items:qa:serverless - key: lists_items:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running lists_items:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless:release - key: lists_items:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running user_roles:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh user_roles:qa:serverless - key: user_roles:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running user_roles:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh user_roles:qa:serverless:release - key: user_roles:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running telemetry:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh telemetry:qa:serverless - key: telemetry:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running telemetry:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh telemetry:qa:serverless:release - key: telemetry:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_workflows:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_workflows:essentials:qa:serverless - key: exception_workflows:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_workflows:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_workflows:essentials:qa:serverless:release - key: exception_workflows:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_date_numeric_types:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_date_numeric_types:essentials:qa:serverless - key: exception_operators_date_numeric_types:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_date_numeric_types:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_date_numeric_types:essentials:qa:serverless:release - key: exception_operators_date_numeric_types:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_keyword:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_keyword:essentials:qa:serverless - key: exception_operators_keyword:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_keyword:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_keyword:essentials:qa:serverless:release - key: exception_operators_keyword:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_ips:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_ips:essentials:qa:serverless - key: exception_operators_ips:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_ips:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_ips:essentials:qa:serverless:release - key: exception_operators_ips:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_long:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_long:essentials:qa:serverless - key: exception_operators_long:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_long:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_long:essentials:qa:serverless:release - key: exception_operators_long:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_text:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_text:essentials:qa:serverless - key: exception_operators_text:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running exception_operators_text:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh exception_operators_text:essentials:qa:serverless:release - key: exception_operators_text:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running actions:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh actions:qa:serverless - key: actions:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running actions:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh actions:qa:serverless:release - key: actions:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running alerts:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:qa:serverless - key: alerts:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running alerts:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:qa:serverless:release - key: alerts:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running alerts:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:essentials:qa:serverless - key: alerts:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running alerts:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh alerts:essentials:qa:serverless:release - key: alerts:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_execution_logic:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_execution_logic:qa:serverless - key: rule_execution_logic:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_execution_logic:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_execution_logic:qa:serverless:release - key: rule_execution_logic:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_detection_engine.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Detection Engine Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_entity_analytics.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_entity_analytics.yml index 1b36316ad2307..22654c358e6ee 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_entity_analytics.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_entity_analytics.yml @@ -14,88 +14,10 @@ steps: - exit_status: "*" limit: 1 - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:entity_analytics - label: 'Serverless MKI QA Entity Analytics - Security Solution Cypress Tests' - key: test_entity_analytics - env: - BK_TEST_SUITE_KEY: "serverless-cypress-entity-analytics" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 2 - retry: - automatic: - - exit_status: '-1' - limit: 1 - - - group: "Serverless MKI QA Entity Analytics - API Integration" - key: api_test_entity_analytics - steps: - - label: Running entity_analytics:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:qa:serverless - key: entity_analytics:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running entity_analytics:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:qa:serverless:release - key: entity_analytics:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running entity_analytics:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless - key: entity_analytics:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_entity_analytics.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Entity Analytics pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - - label: Running entity_analytics:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh entity_analytics:essentials:qa:serverless:release - key: entity_analytics:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_entity_analytics.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Entity Analytics Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_explore.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_explore.yml index 6d5e47e96054f..3840a6bd3413d 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_explore.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_explore.yml @@ -14,21 +14,10 @@ steps: - exit_status: "*" limit: 1 - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:explore - key: test_explore - label: 'Serverless MKI QA Explore - Security Solution Cypress Tests' - env: - BK_TEST_SUITE_KEY: "serverless-cypress-explore" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 4 - retry: - automatic: - - exit_status: '-1' - limit: 1 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_explore.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Explore pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" + + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_explore.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Explore Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_gen_ai.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_gen_ai.yml index f392c099560c6..218f2e5e1cb62 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_gen_ai.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_gen_ai.yml @@ -14,120 +14,10 @@ steps: - exit_status: "*" limit: 1 - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:ai_assistant - label: "Serverless MKI QA AI Assistant - Security Solution Cypress Tests" - key: test_ai_assistant - env: - BK_TEST_SUITE_KEY: "serverless-cypress-gen-ai" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 1 - retry: - automatic: - - exit_status: "-1" - limit: 1 - - - group: "Serverless MKI QA AI Assistant - API Integration" - key: api_test_ai_assistant - steps: - - label: Running genai:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh genai:qa:serverless - key: genai:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running genai:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh genai:qa:serverless:release - key: genai:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running nlp_cleanup_task:complete:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:complete:qa:serverless - key: nlp_cleanup_task:complete:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running nlp_cleanup_task:complete:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:complete:qa:serverless:release - key: nlp_cleanup_task:complete:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running nlp_cleanup_task:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:essentials:qa:serverless - key: nlp_cleanup_task:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_gen_ai.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Gen AI pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - - label: Running nlp_cleanup_task:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh nlp_cleanup_task:essentials:qa:serverless:release - key: nlp_cleanup_task:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_gen_ai.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Gen AI Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_investigations.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_investigations.yml index 504cb562b02b0..215bbbdfa8626 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_investigations.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_investigations.yml @@ -14,21 +14,10 @@ steps: - exit_status: "*" limit: 1 - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:investigations - key: test_investigations - label: 'Serverless MKI QA Investigations - Security Solution Cypress Tests' - env: - BK_TEST_SUITE_KEY: "serverless-cypress-investigations" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 8 - retry: - automatic: - - exit_status: '-1' - limit: 1 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_investigations.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Investigations pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" + + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_investigations.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Investigations Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_rule_management.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_rule_management.yml index 485a1391b57d9..0193f197da699 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_rule_management.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_security_solution_rule_management.yml @@ -14,654 +14,10 @@ steps: - exit_status: "*" limit: 1 - - group: "Serverless MKI QA Rule Management - Cypress Test" - key: cypress_test_rule_management - steps: - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management - label: "Serverless MKI QA Rule Management - Security Solution Cypress Tests" - key: test_rule_management - env: - BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 8 - retry: - automatic: - - exit_status: "-1" - limit: 1 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Release Rule Management pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules - label: "Serverless MKI QA Rule Management - Prebuilt Rules - Security Solution Cypress Tests" - key: test_rule_management_prebuilt_rules - env: - BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - # TODO : Revise the timeout when the pipeline will be officially integrated with the quality gate. - timeout_in_minutes: 300 - parallelism: 4 - retry: - automatic: - - exit_status: "-1" - limit: 1 - - - group: "Serverless MKI QA Rule Management - API Integration" - key: api_test_rule_management - steps: - - label: Running rule_creation:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:qa:serverless - key: rule_creation:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_creation:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:qa:serverless:release - key: rule_creation:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_creation:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:essentials:qa:serverless - key: rule_creation:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_creation:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_creation:essentials:qa:serverless:release - key: rule_creation:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_update:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:qa:serverless - key: rule_update:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_update:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:qa:serverless:release - key: rule_update:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_update:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:essentials:qa:serverless - key: rule_update:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_update:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_update:essentials:qa:serverless:release - key: rule_update:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_patch:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:qa:serverless - key: rule_patch:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_patch:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:qa:serverless:release - key: rule_patch:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_patch:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:essentials:qa:serverless - key: rule_patch:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_patch:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_patch:essentials:qa:serverless:release - key: rule_patch:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_management:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_management:qa:serverless - key: prebuilt_rules_management:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_management:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_management:qa:serverless:release - key: prebuilt_rules_management:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless - key: prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release - key: prebuilt_rules_bundled_prebuilt_rules_package:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_large_prebuilt_rules_package:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_large_prebuilt_rules_package:qa:serverless - key: prebuilt_rules_large_prebuilt_rules_package:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release - key: prebuilt_rules_large_prebuilt_rules_package:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_update_prebuilt_rules_package:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_update_prebuilt_rules_package:qa:serverless - key: prebuilt_rules_update_prebuilt_rules_package:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release - key: prebuilt_rules_update_prebuilt_rules_package:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_delete:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:qa:serverless - key: rule_delete:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_delete:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:qa:serverless:release - key: rule_delete:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_delete:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:essentials:qa:serverless - key: rule_delete:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_delete:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_delete:essentials:qa:serverless:release - key: rule_delete:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_import_export:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:qa:serverless - key: rule_import_export:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_import_export:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:qa:serverless:release - key: rule_import_export:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_import_export:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:essentials:qa:serverless - key: rule_import_export:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_import_export:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_import_export:essentials:qa:serverless:release - key: rule_import_export:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_management:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_management:qa:serverless - key: rule_management:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_management:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_management:qa:serverless:release - key: rule_management:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_bulk_actions:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_bulk_actions:qa:serverless - key: rule_bulk_actions:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_bulk_actions:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_bulk_actions:qa:serverless:release - key: rule_bulk_actions:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_read:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:qa:serverless - key: rule_read:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_read:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:qa:serverless:release - key: rule_read:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_read:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:essentials:qa:serverless - key: rule_read:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rule_read:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rule_read:essentials:qa:serverless:release - key: rule_read:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rules_management:essentials:qa:serverless - if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rules_management:essentials:qa:serverless - key: rules_management:essentials:qa:serverless - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 - - - label: Running rules_management:essentials:qa:serverless:release - if: "build.env('KIBANA_MKI_QUALITY_GATE') == '1'" - command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh rules_management:essentials:qa:serverless:release - key: rules_management:essentials:qa:serverless:release - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - machineType: n2-standard-4 - preemptible: true - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: "1" - limit: 2 + - command: "cat .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_rule_management.yml | buildkite-agent pipeline upload" + label: 'Upload Serverless Periodic Rule Management Pipeline' + if: "build.env('KIBANA_MKI_QUALITY_GATE') != '1'" From b8213523dfe6ecdcd4dfe10fe43386e1ce5deb44 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Mon, 10 Jun 2024 10:46:30 -0400 Subject: [PATCH 06/14] chore(slo): Friendlier security exception (#185019) --- .../slo/server/services/create_slo.ts | 5 +++++ .../slo/server/services/update_slo.ts | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts index cb5b59bb3cf1e..4d2229ef7ba64 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts @@ -20,6 +20,7 @@ import { import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; import { Duration, DurationUnit, SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; +import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; @@ -98,6 +99,10 @@ export class CreateSLO { } }); + if (err.meta?.body?.error?.type === 'security_exception') { + throw new SecurityException(err.meta.body.error.reason); + } + throw err; } diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts index cefdc703bb434..c990024a6d03b 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts @@ -20,6 +20,7 @@ import { import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; import { SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; +import { SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; @@ -93,6 +94,10 @@ export class UpdateSLO { } }); + if (err.meta?.body?.error?.type === 'security_exception') { + throw new SecurityException(err.meta.body.error.reason); + } + throw err; } @@ -154,6 +159,10 @@ export class UpdateSLO { } }); + if (err.meta?.body?.error?.type === 'security_exception') { + throw new SecurityException(err.meta.body.error.reason); + } + throw err; } From d1e372e1cda0c9c2d489fcb4bdc74bd58248ed9a Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 10 Jun 2024 17:38:54 +0200 Subject: [PATCH 07/14] fix: [Obs Applications > Services][KEYBOARD]: Environment badges must show tooltips on keyboard focus as well as mouse hover (#183809) Closes: https://github.com/elastic/observability-dev/issues/3403 ## Description The Obs Applications > Services table rows have an `Environment` badge that exposes a tooltip when hovered, but that tooltip cannot take keyboard focus, making it unavailable to keyboard users. Screenshot attached below. ### Steps to recreate 1. Open the [Obs Services](https://keepserverless-qa-oblt-b4ba07.kb.eu-west-1.aws.qa.elastic.cloud/app/apm/services) view 2. Tab through the table, until focus is on an element after the first `Environment` badge 6. Verify the icons in the table row(s) never receive focus and the tooltips never become visible ### What was changed?: 1. Added tabIndex attribute to handle keyboard navigation correctly ### Screen: https://github.com/elastic/kibana/assets/20072247/9a7976d7-63bd-4aa6-a467-72830d16ef30 --- .../apm/public/components/shared/item_badge/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/item_badge/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/item_badge/index.tsx index ab0b9154bd8cb..f0b88da1ae4d1 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/item_badge/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/item_badge/index.tsx @@ -41,7 +41,7 @@ export function ItemsBadge({ ))} > - {multipleItemsMessage} + {multipleItemsMessage} ); } From ec15bb283496f18c1d880dd0d9579dc4f364553f Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 10 Jun 2024 17:39:51 +0200 Subject: [PATCH 08/14] [Synthetics] Implement clone monitor action (#184393) ## Summary Added a clone monitor action which will load the create new monitor form with pre-filed values. I often have to do this, so i think it's worth doing it !! image image --- .../hooks/use_clone_monitor.ts | 18 +++++++++++ .../monitor_add_page.test.tsx | 4 +-- .../monitor_add_edit/monitor_add_page.tsx | 17 +++++++++-- .../management/monitor_list_table/columns.tsx | 30 ++++++++++++++++--- .../management/monitor_list_table/labels.tsx | 4 +++ .../overview/actions_popover.test.tsx | 29 +++++++++++++++--- .../overview/overview/actions_popover.tsx | 23 ++++++++++++++ .../url_params/get_supported_url_params.ts | 2 ++ 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts new file mode 100644 index 0000000000000..ff871ae5fad98 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts @@ -0,0 +1,18 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { useGetUrlParams } from '../../../hooks'; +import { getDecryptedMonitorAPI } from '../../../state/monitor_management/api'; + +export const useCloneMonitor = () => { + const { cloneId } = useGetUrlParams(); + return useFetcher(() => { + if (!cloneId) return Promise.resolve(undefined); + return getDecryptedMonitorAPI({ id: cloneId }); + }, [cloneId]); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.test.tsx index 5317a36da9fc6..e00a471936095 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.test.tsx @@ -11,7 +11,7 @@ import { MonitorAddPage } from './monitor_add_page'; describe('MonitorAddPage', () => { it('renders correctly', async () => { - const { getByText } = render(, { + const { findByText } = render(, { state: { serviceLocations: { locations: [ @@ -31,7 +31,7 @@ describe('MonitorAddPage', () => { }); // page is loaded - expect(getByText('Add a script')).toBeInTheDocument(); + expect(await findByText('Add a script')).toBeInTheDocument(); }); it('renders when loading', async () => { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx index f1b4b2648e4d7..cb7fbb18db06a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_page.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; +import { useCloneMonitor } from './hooks/use_clone_monitor'; import { useCanUsePublicLocations } from '../../../../hooks/use_capabilities'; import { CanUsePublicLocationsCallout } from './steps/can_use_public_locations_callout'; import { DisabledCallout } from '../monitors_page/management/disabled_callout'; @@ -33,6 +34,8 @@ export const MonitorAddPage = () => { const canUsePublicLocations = useCanUsePublicLocations(); + const { data: cloneMonitor, loading: cloneMonitorLoading } = useCloneMonitor(); + const dispatch = useDispatch(); useEffect(() => { dispatch(getServiceLocations()); @@ -43,12 +46,22 @@ export const MonitorAddPage = () => { return ; } - if (!locationsLoaded) { + if (!locationsLoaded || cloneMonitorLoading) { return ; } return ( - + diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index e7600777ca9ba..16dbd8ec1d349 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -49,6 +49,7 @@ export function useMonitorListColumns({ setMonitorPendingDeletion: (config: EncryptedSyntheticsSavedMonitor) => void; }): Array> { const history = useHistory(); + const { http } = useKibana().services; const canEditSynthetics = useCanEditSynthetics(); const { isServiceAllowed } = useEnablement(); @@ -194,10 +195,31 @@ export function useMonitorListColumns({ !isActionLoading(fields) && isPublicLocationsAllowed(fields) && isServiceAllowed, - onClick: (fields) => { - history.push({ - pathname: `/edit-monitor/${fields[ConfigKey.CONFIG_ID]}`, - }); + href: (fields) => { + return http?.basePath.prepend(`edit-monitor/${fields[ConfigKey.CONFIG_ID]}`)!; + }, + }, + { + 'data-test-subj': 'syntheticsMonitorCopyAction', + isPrimary: true, + name: (fields) => ( + + {labels.CLONE_LABEL} + + ), + description: labels.CLONE_LABEL, + icon: 'copy' as const, + type: 'icon' as const, + enabled: (fields) => + canEditSynthetics && + !isActionLoading(fields) && + isPublicLocationsAllowed(fields) && + isServiceAllowed, + href: (fields) => { + return http?.basePath.prepend(`add-monitor?cloneId=${fields[ConfigKey.CONFIG_ID]}`)!; }, }, { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx index 75660bcf91071..03bc7fa059860 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx @@ -45,6 +45,10 @@ export const EDIT_LABEL = i18n.translate('xpack.synthetics.management.editLabel' defaultMessage: 'Edit', }); +export const CLONE_LABEL = i18n.translate('xpack.synthetics.management.cloneLabel', { + defaultMessage: 'Clone', +}); + export const ENABLE_STATUS_ALERT = i18n.translate('xpack.synthetics.management.enableStatusAlert', { defaultMessage: 'Enable status alerts', }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.test.tsx index 6ff6118ad29db..f3c4571db100f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.test.tsx @@ -109,7 +109,7 @@ describe('ActionsPopover', () => { jest .spyOn(editMonitorLocatorModule, 'useEditMonitorLocator') .mockReturnValue('/a/test/edit/url'); - const { getByRole } = render( + const { getByTestId } = render( { /> ); - expect(getByRole('link')?.getAttribute('href')).toBe('/a/test/edit/url'); + expect(getByTestId('editMonitorLink')?.getAttribute('href')).toBe('/a/test/edit/url'); + }); + + it('contains link to clone monitor', async () => { + jest + .spyOn(editMonitorLocatorModule, 'useEditMonitorLocator') + .mockReturnValue('/a/test/edit/url'); + const { getByTestId } = render( + + ); + + expect(getByTestId('cloneMonitorLink')?.getAttribute('href')).toBe( + 'synthetics/add-monitor?cloneId=1lkjelre' + ); }); it('contains link to detail page', async () => { jest .spyOn(monitorDetailLocatorModule, 'useMonitorDetailLocator') .mockReturnValue('/a/test/detail/url'); - const { getByRole } = render( + const { getByTestId } = render( { locationId={testMonitor.location.id} /> ); - expect(getByRole('link')?.getAttribute('href')).toBe('/a/test/detail/url'); + expect(getByTestId('actionsPopoverGoToMonitor')?.getAttribute('href')).toBe( + '/a/test/detail/url' + ); }); it('sets the enabled state', async () => { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx index dc65e36ed18d7..c2dba4060a7eb 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx @@ -19,6 +19,7 @@ import { import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { TEST_SCHEDULED_LABEL } from '../../../monitor_add_edit/form/run_test_btn'; import { useCanUsePublicLocById } from '../../hooks/use_can_use_public_loc_id'; import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config'; @@ -106,6 +107,8 @@ export function ActionsPopover({ const dispatch = useDispatch(); const locationName = useLocationName(monitor); + const { http } = useKibana().services; + const detailUrl = useMonitorDetailLocator({ configId: monitor.configId, locationId: locationId ?? monitor.location.id, @@ -174,6 +177,7 @@ export function ActionsPopover({ name: actionsMenuGoToMonitorName, icon: 'sortRight', href: detailUrl, + 'data-test-subj': 'actionsPopoverGoToMonitor', }, quickInspectPopoverItem, { @@ -203,6 +207,18 @@ export function ActionsPopover({ icon: 'pencil', disabled: !canEditSynthetics || !isServiceAllowed, href: editUrl, + 'data-test-subj': 'editMonitorLink', + }, + { + name: ( + + {actionsMenuCloneMonitorName} + + ), + icon: 'copy', + disabled: !canEditSynthetics || !isServiceAllowed, + href: http?.basePath.prepend(`synthetics/add-monitor?cloneId=${monitor.configId}`), + 'data-test-subj': 'cloneMonitorLink', }, { name: ( @@ -333,6 +349,13 @@ const actionsMenuEditMonitorName = i18n.translate( } ); +const actionsMenuCloneMonitorName = i18n.translate( + 'xpack.synthetics.overview.actions.cloneMonitor.name', + { + defaultMessage: 'Clone monitor', + } +); + const loadingLabel = (isEnabled: boolean) => isEnabled ? i18n.translate('xpack.synthetics.overview.actions.disablingLabel', { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts index af5879b85b1c1..ce2eb6f30829f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts @@ -37,6 +37,7 @@ export interface SyntheticsUrlParams { groupBy?: MonitorOverviewState['groupBy']['field']; groupOrderBy?: MonitorOverviewState['groupBy']['order']; packagePolicyId?: string; + cloneId?: string; } const { ABSOLUTE_DATE_RANGE_START, ABSOLUTE_DATE_RANGE_END, SEARCH, FILTERS, STATUS_FILTER } = @@ -127,6 +128,7 @@ export const getSupportedUrlParams = (params: { projects: parseFilters(projects), schedules: parseFilters(schedules), locationId: locationId || undefined, + cloneId: filteredParams.cloneId, }; }; From e1c86f11f6fbf6dbee005a6c9f48e193249b2f2e Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 10 Jun 2024 17:45:56 +0200 Subject: [PATCH 09/14] Creates ES|QL where filters for ordinal charts (#184420) ## Summary Closes https://github.com/elastic/kibana/issues/183425 ![meow](https://github.com/elastic/kibana/assets/17003240/88baec65-5c35-41f8-b01e-bbce88536c7d) This PR enables the creation of where clause filters by clicking a chart in Discover and wherever the ES|QL editor exists. This means that this is not available in dashboards. This is possible only for ordinal charts. For date fields is quite difficult to know the interval so I don't allow it for now. We already support brushing so time filtering is already available. ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lukas Olson --- .../public/components/heatmap_component.tsx | 5 +- .../components/partition_vis_component.tsx | 4 +- .../public/components/xy_chart.tsx | 2 +- .../create_filters_from_value_click.test.ts | 210 ++++++++++++------ .../create_filters_from_value_click.ts | 39 +++- .../data/public/actions/value_click_action.ts | 33 ++- src/plugins/data/tsconfig.json | 1 + src/plugins/text_based_languages/kibana.jsonc | 3 +- .../text_based_languages/public/plugin.ts | 22 +- .../public/triggers/index.ts | 10 + .../update_esql_query_actions.test.ts | 37 +++ .../triggers/update_esql_query_actions.ts | 49 ++++ .../triggers/update_esql_query_helpers.ts | 34 +++ .../triggers/update_esql_query_trigger.ts | 22 ++ .../text_based_languages/tsconfig.json | 5 +- 15 files changed, 390 insertions(+), 86 deletions(-) create mode 100644 src/plugins/text_based_languages/public/triggers/index.ts create mode 100644 src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts create mode 100644 src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts create mode 100644 src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts create mode 100644 src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index d36f8f54b9365..c8ab912acc7b2 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -7,6 +7,7 @@ */ import React, { memo, FC, useMemo, useState, useCallback, useRef } from 'react'; +import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common'; import { Chart, ElementClickListener, @@ -253,7 +254,9 @@ export const HeatmapComponent: FC = memo( datatables: [formattedTable.table], }); - const hasTooltipActions = interactive; + const isEsqlMode = table?.meta?.type === ESQL_TABLE_TYPE; + + const hasTooltipActions = interactive && !isEsqlMode; const onElementClick = useCallback( (e: HeatmapElementEvent[]) => { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index 855d32ccc312a..de0def30577a4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -22,6 +22,7 @@ import { Tooltip, TooltipValue, } from '@elastic/charts'; +import { ESQL_TABLE_TYPE } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; import { useEuiTheme } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; @@ -409,8 +410,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { ? getColumnByAccessor(splitRow[0], visData.columns) : undefined; + const isEsqlMode = originalVisData?.meta?.type === ESQL_TABLE_TYPE; const hasTooltipActions = - interactive && bucketAccessors.filter((a) => a !== 'metric-name').length > 0; + interactive && !isEsqlMode && bucketAccessors.filter((a) => a !== 'metric-name').length > 0; const tooltip: TooltipProps = { ...(fixedViewPort ? { boundary: fixedViewPort } : {}), diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 17eb637f77c09..58a02a1bf3215 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -863,7 +863,7 @@ export function XYChart({ xDomain={xDomain} // enable brushing only for time charts, for both ES|QL and DSL queries onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} - onElementClick={interactive && !isEsqlMode ? clickHandler : undefined} + onElementClick={interactive ? clickHandler : undefined} legendAction={ interactive ? getLegendAction( diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 4bb80fe64134b..fab4d18af400b 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -9,7 +9,10 @@ import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { dataPluginMock } from '../../mocks'; import { setIndexPatterns, setSearchService } from '../../services'; -import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; +import { + createFiltersFromValueClickAction, + appendFilterToESQLQueryFromValueClickAction, +} from './create_filters_from_value_click'; import { FieldFormatsGetConfigFn, BytesFormat } from '@kbn/field-formats-plugin/common'; import { RangeFilter } from '@kbn/es-query'; @@ -17,89 +20,156 @@ const mockField = { name: 'bytes', filterable: true, }; +describe('createFiltersFromClickEvent', () => { + const dataStart = dataPluginMock.createStartContract(); + setSearchService(dataStart.search); + setIndexPatterns({ + ...dataStart.indexPatterns, + get: async () => ({ + id: 'logstash-*', + fields: { + getByName: () => mockField, + filter: () => [mockField], + }, + getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), + }), + } as unknown as DataViewsContract); + describe('createFiltersFromValueClick', () => { + let dataPoints: Parameters[0]['data']; -describe('createFiltersFromValueClick', () => { - let dataPoints: Parameters[0]['data']; - - beforeEach(() => { - dataPoints = [ - { - table: { - columns: [ - { - name: 'test', - id: '1-1', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - indexPatternId: 'logstash-*', - type: 'histogram', - params: { - field: 'bytes', - interval: 30, - otherBucket: true, + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + indexPatternId: 'logstash-*', + type: 'histogram', + params: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, }, }, }, - }, - ], - rows: [ - { - '1-1': '2048', - }, - ], - }, - column: 0, - row: 0, - value: 'test', - }, - ]; - - const dataStart = dataPluginMock.createStartContract(); - setSearchService(dataStart.search); - setIndexPatterns({ - ...dataStart.indexPatterns, - get: async () => ({ - id: 'logstash-*', - fields: { - getByName: () => mockField, - filter: () => [mockField], + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', }, - getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), - }), - } as unknown as DataViewsContract); - }); + ]; + }); + test('ignores event when value for rows is not provided', async () => { + dataPoints[0].table.rows[0]['1-1'] = null; + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); - test('ignores event when value for rows is not provided', async () => { - dataPoints[0].table.rows[0]['1-1'] = null; - const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + expect(filters.length).toEqual(0); + }); - expect(filters.length).toEqual(0); - }); + test('handles an event when aggregations type is a terms', async () => { + (dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms'; + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); - test('handles an event when aggregations type is a terms', async () => { - (dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms'; - const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + expect(filters.length).toEqual(1); + expect(filters[0].query?.match_phrase?.bytes).toEqual('2048'); + }); - expect(filters.length).toEqual(1); - expect(filters[0].query?.match_phrase?.bytes).toEqual('2048'); - }); + test('handles an event when aggregations type is not terms', async () => { + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); - test('handles an event when aggregations type is not terms', async () => { - const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + expect(filters.length).toEqual(1); - expect(filters.length).toEqual(1); + const [rangeFilter] = filters as RangeFilter[]; + expect(rangeFilter.query.range.bytes.gte).toEqual(2048); + expect(rangeFilter.query.range.bytes.lt).toEqual(2078); + }); - const [rangeFilter] = filters as RangeFilter[]; - expect(rangeFilter.query.range.bytes.gte).toEqual(2048); - expect(rangeFilter.query.range.bytes.lt).toEqual(2078); + test('handles non-unique filters', async () => { + const [point] = dataPoints; + const filters = await createFiltersFromValueClickAction({ data: [point, point] }); + + expect(filters.length).toEqual(1); + }); }); + describe('appendFilterToESQLQueryFromValueClickAction', () => { + let dataPoints: Parameters[0]['data']; + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'columnA', + id: 'columnA', + meta: { + type: 'date', + }, + }, + ], + rows: [ + { + columnA: '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, + ]; + }); + test('should return null for date fields', async () => { + const queryString = await appendFilterToESQLQueryFromValueClickAction({ + data: dataPoints, + query: { esql: 'from meow' }, + }); + + expect(queryString).toBeUndefined(); + }); - test('handles non-unique filters', async () => { - const [point] = dataPoints; - const filters = await createFiltersFromValueClickAction({ data: [point, point] }); + test('should return null if no aggregate query is present', async () => { + dataPoints[0].table.columns[0] = { + name: 'test', + id: '1-1', + meta: { + type: 'string', + }, + }; + const queryString = await appendFilterToESQLQueryFromValueClickAction({ + data: dataPoints, + }); + + expect(queryString).toBeUndefined(); + }); + + test('should return the update query string', async () => { + dataPoints[0].table.columns[0] = { + name: 'columnA', + id: 'columnA', + meta: { + type: 'string', + }, + }; + const queryString = await appendFilterToESQLQueryFromValueClickAction({ + data: dataPoints, + query: { esql: 'from meow' }, + }); - expect(filters.length).toEqual(1); + expect(queryString).toEqual(`from meow +| where \`columnA\`=="2048"`); + }); }); }); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index 5a3464d9aefea..5493937a19cbb 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -8,7 +8,14 @@ import _ from 'lodash'; import { Datatable } from '@kbn/expressions-plugin/public'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter, toggleFilterNegated } from '@kbn/es-query'; +import { + compareFilters, + COMPARE_ALL_OPTIONS, + Filter, + toggleFilterNegated, + type AggregateQuery, +} from '@kbn/es-query'; +import { appendWhereClauseToESQLQuery } from '@kbn/esql-utils'; import { getIndexPatterns, getSearchService } from '../../services'; import { AggConfigSerialized } from '../../../common/search/aggs'; import { mapAndFlattenFilters } from '../../query'; @@ -22,6 +29,7 @@ interface ValueClickDataContext { }>; timeFieldName?: string; negate?: boolean; + query?: AggregateQuery; } /** @@ -148,3 +156,32 @@ export const createFiltersFromValueClickAction = async ({ compareFilters(a, b, COMPARE_ALL_OPTIONS) ); }; + +/** @public */ +export const appendFilterToESQLQueryFromValueClickAction = ({ + data, + query, +}: ValueClickDataContext) => { + if (!query) { + return; + } + // Do not append in case of time series, for now. We need to find a way to compute the interval + // to create the time range filter correctly. The users can brush to update the time filter instead. + const dataPoints = data.filter((point) => { + return point && point.table?.columns?.[point.column]?.meta?.type !== 'date'; + }); + + if (!dataPoints.length) { + return; + } + const { table, column: columnIndex, row: rowIndex } = dataPoints[dataPoints.length - 1]; + + if (table?.columns?.[columnIndex]) { + const column = table.columns[columnIndex]; + const value: unknown = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; + if (value == null) { + return; + } + return appendWhereClauseToESQLQuery(query.esql, column.name, value, '+', column.meta?.type); + } +}; diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index b229ee5b07a04..fd96f514688b8 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ -import type { Filter } from '@kbn/es-query'; +import { Filter, AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query'; import { Datatable } from '@kbn/expressions-plugin/public'; import { UiActionsActionDefinition, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { APPLY_FILTER_TRIGGER } from '../triggers'; -import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +import { + createFiltersFromValueClickAction, + appendFilterToESQLQueryFromValueClickAction, +} from './filters/create_filters_from_value_click'; export type ValueClickActionContext = ValueClickContext; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; @@ -28,6 +31,7 @@ export interface ValueClickContext { }>; timeFieldName?: string; negate?: boolean; + query?: AggregateQuery; }; } @@ -39,18 +43,31 @@ export function createValueClickActionDefinition( id: ACTION_VALUE_CLICK, shouldAutoExecute: async () => true, isCompatible: async (context: ValueClickContext) => { + if (context.data.query && isOfAggregateQueryType(context.data.query)) { + const queryString = await appendFilterToESQLQueryFromValueClickAction(context.data); + return queryString != null; + } const filters = await createFiltersFromValueClickAction(context.data); return filters.length > 0; }, execute: async (context: ValueClickActionContext) => { try { - const filters: Filter[] = await createFiltersFromValueClickAction(context.data); - if (filters.length > 0) { - await getStartServices().uiActions.getTrigger(APPLY_FILTER_TRIGGER).exec({ - filters, - embeddable: context.embeddable, - timeFieldName: context.data.timeFieldName, + if (context.data.query && isOfAggregateQueryType(context.data.query)) { + // ES|QL charts have a different way of applying filters, + // they are appending a where clause to the query + const queryString = appendFilterToESQLQueryFromValueClickAction(context.data); + await getStartServices().uiActions.getTrigger('UPDATE_ESQL_QUERY_TRIGGER').exec({ + queryString, }); + } else { + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); + if (filters.length > 0) { + await getStartServices().uiActions.getTrigger(APPLY_FILTER_TRIGGER).exec({ + filters, + embeddable: context.embeddable, + timeFieldName: context.data.timeFieldName, + }); + } } } catch (e) { // eslint-disable-next-line no-console diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 530d6b7325c00..ea92ad9289b13 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -56,6 +56,7 @@ "@kbn/react-kibana-mount", "@kbn/search-types", "@kbn/safer-lodash-set", + "@kbn/esql-utils", ], "exclude": [ "target/**/*", diff --git a/src/plugins/text_based_languages/kibana.jsonc b/src/plugins/text_based_languages/kibana.jsonc index 7e9aba22067e8..5bed408add15a 100644 --- a/src/plugins/text_based_languages/kibana.jsonc +++ b/src/plugins/text_based_languages/kibana.jsonc @@ -12,7 +12,8 @@ "requiredPlugins": [ "data", "expressions", - "dataViews" + "dataViews", + "uiActions", ], "requiredBundles": [ "kibanaReact", diff --git a/src/plugins/text_based_languages/public/plugin.ts b/src/plugins/text_based_languages/public/plugin.ts index dd6297750ae5b..841641c8fce25 100755 --- a/src/plugins/text_based_languages/public/plugin.ts +++ b/src/plugins/text_based_languages/public/plugin.ts @@ -9,27 +9,45 @@ import type { Plugin, CoreStart, CoreSetup } from '@kbn/core/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { IndexManagementPluginSetup } from '@kbn/index-management'; +import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + updateESQLQueryTrigger, + UpdateESQLQueryAction, + UPDATE_ESQL_QUERY_TRIGGER, +} from './triggers'; import { setKibanaServices } from './kibana_services'; interface TextBasedLanguagesPluginStart { dataViews: DataViewsPublicPluginStart; expressions: ExpressionsStart; + uiActions: UiActionsStart; + data: DataPublicPluginStart; } interface TextBasedLanguagesPluginSetup { indexManagement: IndexManagementPluginSetup; + uiActions: UiActionsSetup; } export class TextBasedLanguagesPlugin implements Plugin<{}, void> { private indexManagement?: IndexManagementPluginSetup; - public setup(_: CoreSetup, { indexManagement }: TextBasedLanguagesPluginSetup) { + public setup(_: CoreSetup, { indexManagement, uiActions }: TextBasedLanguagesPluginSetup) { this.indexManagement = indexManagement; + + uiActions.registerTrigger(updateESQLQueryTrigger); + return {}; } - public start(core: CoreStart, { dataViews, expressions }: TextBasedLanguagesPluginStart): void { + public start( + core: CoreStart, + { dataViews, expressions, data, uiActions }: TextBasedLanguagesPluginStart + ): void { + const appendESQLAction = new UpdateESQLQueryAction(data); + uiActions.addTriggerAction(UPDATE_ESQL_QUERY_TRIGGER, appendESQLAction); setKibanaServices(core, dataViews, expressions, this.indexManagement); } diff --git a/src/plugins/text_based_languages/public/triggers/index.ts b/src/plugins/text_based_languages/public/triggers/index.ts new file mode 100644 index 0000000000000..091971018adf3 --- /dev/null +++ b/src/plugins/text_based_languages/public/triggers/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { updateESQLQueryTrigger, UPDATE_ESQL_QUERY_TRIGGER } from './update_esql_query_trigger'; +export { UpdateESQLQueryAction } from './update_esql_query_actions'; diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts b/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts new file mode 100644 index 0000000000000..e1c95a3e2f5e5 --- /dev/null +++ b/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { UpdateESQLQueryAction } from './update_esql_query_actions'; + +describe('update ES|QL query action', () => { + const dataMock = dataPluginMock.createStartContract(); + describe('compatibility check', () => { + it('is incompatible if no query is applied', async () => { + const updateQueryAction = new UpdateESQLQueryAction(dataMock); + const isCompatible = await updateQueryAction.isCompatible(); + + expect(isCompatible).toBeFalsy(); + }); + + it('is incompatible if query is not ES|QL', async () => { + dataMock.query.queryString.getQuery = jest.fn().mockReturnValue({ query: 'not esql' }); + const updateQueryAction = new UpdateESQLQueryAction(dataMock); + const isCompatible = await updateQueryAction.isCompatible(); + + expect(isCompatible).toBeFalsy(); + }); + + it('is compatible if query is ES|QL', async () => { + dataMock.query.queryString.getQuery = jest.fn().mockReturnValue({ esql: 'from meow' }); + const updateQueryAction = new UpdateESQLQueryAction(dataMock); + const isCompatible = await updateQueryAction.isCompatible(); + + expect(isCompatible).toBeTruthy(); + }); + }); +}); diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts b/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts new file mode 100644 index 0000000000000..4aa7b015b366b --- /dev/null +++ b/src/plugins/text_based_languages/public/triggers/update_esql_query_actions.ts @@ -0,0 +1,49 @@ +/* + * 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'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; + +const ACTION_UPDATE_ESQL_QUERY = 'ACTION_UPDATE_ESQL_QUERY'; + +interface Context { + queryString: string; +} + +export const getHelpersAsync = async () => await import('./update_esql_query_helpers'); + +export class UpdateESQLQueryAction implements Action { + public type = ACTION_UPDATE_ESQL_QUERY; + public id = ACTION_UPDATE_ESQL_QUERY; + public order = 50; + + constructor(protected readonly data: DataPublicPluginStart) {} + + public getDisplayName(): string { + return i18n.translate('textBasedLanguages.updateESQLQueryLabel', { + defaultMessage: 'Update the ES|QL query in the editor', + }); + } + + public getIconType() { + return 'filter'; + } + + public async isCompatible() { + const { isActionCompatible } = await getHelpersAsync(); + return isActionCompatible(this.data); + } + + public async execute({ queryString }: Context) { + const { executeAction } = await getHelpersAsync(); + return executeAction({ + queryString, + data: this.data, + }); + } +} diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts b/src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts new file mode 100644 index 0000000000000..67276b1c0237f --- /dev/null +++ b/src/plugins/text_based_languages/public/triggers/update_esql_query_helpers.ts @@ -0,0 +1,34 @@ +/* + * 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 { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { isOfAggregateQueryType } from '@kbn/es-query'; + +interface Context { + data: DataPublicPluginStart; + queryString: string; +} + +export async function isActionCompatible(data: DataPublicPluginStart) { + const { query } = data; + const currentQueryString = query.queryString.getQuery(); + // we want to make sure that the current query is an ES|QL query + return currentQueryString && isOfAggregateQueryType(currentQueryString); +} + +export async function executeAction({ queryString, data }: Context) { + const isCompatibleAction = await isActionCompatible(data); + if (!isCompatibleAction) { + throw new IncompatibleActionError(); + } + + const { query } = data; + query.queryString.setQuery({ + esql: queryString, + }); +} diff --git a/src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts b/src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts new file mode 100644 index 0000000000000..13164647607ef --- /dev/null +++ b/src/plugins/text_based_languages/public/triggers/update_esql_query_trigger.ts @@ -0,0 +1,22 @@ +/* + * 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'; +import { Trigger } from '@kbn/ui-actions-plugin/public'; + +export const UPDATE_ESQL_QUERY_TRIGGER = 'UPDATE_ESQL_QUERY_TRIGGER'; + +export const updateESQLQueryTrigger: Trigger = { + id: UPDATE_ESQL_QUERY_TRIGGER, + title: i18n.translate('textBasedLanguages.triggers.updateEsqlQueryTrigger', { + defaultMessage: 'Update ES|QL query', + }), + description: i18n.translate('textBasedLanguages.triggers.updateEsqlQueryTriggerDescription', { + defaultMessage: 'Update ES|QL query with a new one', + }), +}; diff --git a/src/plugins/text_based_languages/tsconfig.json b/src/plugins/text_based_languages/tsconfig.json index 2cb4eddcbf7a0..3c7b28567816b 100644 --- a/src/plugins/text_based_languages/tsconfig.json +++ b/src/plugins/text_based_languages/tsconfig.json @@ -18,7 +18,10 @@ "@kbn/index-management", "@kbn/i18n", "@kbn/config-schema", - "@kbn/esql-utils" + "@kbn/esql-utils", + "@kbn/ui-actions-plugin", + "@kbn/data-plugin", + "@kbn/es-query" ], "exclude": [ "target/**/*", From a919cd836edde29ad1eca9a36da89a51100ab929 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 10 Jun 2024 12:07:03 -0400 Subject: [PATCH 10/14] [APM] add view in apm button to alert details page (#184687) ## Summary Relates to https://github.com/elastic/kibana/issues/184204 Relates to https://github.com/elastic/kibana/pull/184236 After adding the `View in APM` buttons to APM alert details visualizations, a bug was introduced where the share plugin was not available in the codepath. This was due to using the `useAPMPluginContext` hook. APM plugin context does not exist within the observability plugin where this component is rendered. The use of `useAPMPluginContext` did work when using the visualization embeddables, because they are wrapped in context, but when using the visualizations on their own, such as in the APM latency alert details page, the experience was broken. To fix the experience, we added some defensive code and hid the button when the `share` plugin was not available. Now, we want to introduce a permenant fix to re-introduce the View in APM button the APM alert details visualization. This fix uses `useKibana` rather than `useAPMPluginContext` to gain access to the `share` plugin. SLO APM latency SLI alert details ![image](https://github.com/elastic/kibana/assets/11356435/86fa3c4d-c0aa-4cf6-a9f1-46b8174c36d1) SLO APM error rate SLI alert details ![image](https://github.com/elastic/kibana/assets/11356435/a519606b-bb6f-49c5-9c91-733e5c466390) APM latency alert details ![image](https://github.com/elastic/kibana/assets/11356435/1c9f2037-869d-4a57-836b-63c430ca9ee5) ### Testing Both the APM SLO burn rate rules and the APM latency rules should be tested in this PR 1. Generate APM test data. For example, you can use `node scripts/synthtrace simple_trace --live` 2. Create an APM latency rule with the threshold that will trigger an alert 3. Navigate to the alert details page 4. Ensure the page loads and the `View in APM` button appears 5. Click on the View in APM button to ensure it navigates to APM appropriately 6. Create an APM latency SLI within the SLO app. Choose a threshold that will trigger a low SLI value 7. Wait for a burn rate alert to fire (the rule is created automatically) 8. Navigate to the alert details page. Ensure the page loads and the `View in APM` button appears 9. Click on the View in APM button to ensure it navigates to APM appropriately 10. Repeat steps 6 to 9 with the APM error rate SLI within the SLO app. --- .../view_in_apm_button.test.tsx | 28 +++++++++++++------ .../view_in_apm_button.tsx | 9 ++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.test.tsx index 16deb00631284..4eb65afcd9211 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.test.tsx @@ -8,7 +8,15 @@ import React from 'react'; import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers'; import { ViewInAPMButton } from './view_in_apm_button'; -import * as apmContext from '../../../../context/apm_plugin/use_apm_plugin_context'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + ...jest.requireActual('@kbn/kibana-react-plugin/public'), + useKibana: jest.fn().mockReturnValue({ + services: {}, + }), +})); + +const { useKibana } = jest.requireMock('@kbn/kibana-react-plugin/public'); describe('ViewInApmButton', () => { const config = { @@ -31,14 +39,16 @@ describe('ViewInApmButton', () => { }); it('reners correctly', () => { - jest.spyOn(apmContext, 'useApmPluginContext').mockReturnValue({ - share: { - url: { - locators: { - // @ts-ignore - get: () => ({ - navigate: jest.fn(), - }), + useKibana.mockReturnValue({ + services: { + share: { + url: { + locators: { + // @ts-ignore + get: () => ({ + navigate: jest.fn(), + }), + }, }, }, }, diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.tsx index f8935a4082ded..6594a14ab059a 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/view_in_apm_button.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; +import type { ObservabilityPublicPluginsStart } from '@kbn/observability-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonEmpty } from '@elastic/eui'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { APM_APP_LOCATOR_ID } from '../../../../locator/service_detail_locator'; export function ViewInAPMButton({ @@ -28,7 +29,11 @@ export function ViewInAPMButton({ to: string; kuery?: string; }) { - const { share } = useApmPluginContext() || {}; + // this component is rendered in the Observability Plugin, so we can use the ObservabilityPublicPluginsStart type + const { + services: { share }, + } = useKibana(); + const serviceNavigator = share?.url?.locators?.get(APM_APP_LOCATOR_ID); if (!serviceNavigator) { From 2f8d6a24bf2708a5f78769636da581bba2b80623 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 10 Jun 2024 18:14:33 +0200 Subject: [PATCH 11/14] [ES|QL] Allow to comment multiple lines (#185659) ## Summary Follow up of https://github.com/elastic/kibana/pull/184637. Allow to comment multiline selections. ![meow](https://github.com/elastic/kibana/assets/17003240/fb7a2c28-8556-46bd-baa1-be52f3f4fa35) --- .../src/text_based_languages_editor.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 65516e498ba15..dac1f609430b6 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -238,24 +238,28 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ }, [language, onTextLangQuerySubmit, abortController, isQueryLoading, allowQueryCancellation]); const onCommentLine = useCallback(() => { - const currentPosition = editor1.current?.getPosition(); - const lineNumber = currentPosition?.lineNumber; - if (lineNumber) { - const lineContent = editorModel.current?.getLineContent(lineNumber) ?? ''; - const hasComment = lineContent?.startsWith('//'); - const commentedLine = hasComment ? lineContent?.replace('//', '') : `//${lineContent}`; - // executeEdits allows to keep edit in history - editor1.current?.executeEdits('comment', [ - { - range: { - startLineNumber: lineNumber, - startColumn: 0, - endLineNumber: lineNumber, - endColumn: (lineContent?.length ?? 0) + 1, + const currentSelection = editor1?.current?.getSelection(); + const startLineNumber = currentSelection?.startLineNumber; + const endLineNumber = currentSelection?.endLineNumber; + if (startLineNumber && endLineNumber) { + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const lineContent = editorModel.current?.getLineContent(lineNumber) ?? ''; + const hasComment = lineContent?.startsWith('//'); + const commentedLine = hasComment ? lineContent?.replace('//', '') : `//${lineContent}`; + + // executeEdits allows to keep edit in history + editor1.current?.executeEdits('comment', [ + { + range: { + startLineNumber: lineNumber, + startColumn: 0, + endLineNumber: lineNumber, + endColumn: (lineContent?.length ?? 0) + 1, + }, + text: commentedLine, }, - text: commentedLine, - }, - ]); + ]); + } } }, []); @@ -466,7 +470,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ useEffect(() => { const validateQuery = async () => { - if (editorModel?.current) { + if (editor1?.current) { const parserMessages = await parseMessages(); setClientParserMessages({ errors: parserMessages?.errors ?? [], From 38897c435df38eb9138387e97cf6e0e2dc54017f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 10 Jun 2024 18:32:26 +0200 Subject: [PATCH 12/14] [flaky on mki] Increase timeout for kibanaReportCompletion (#185878) ## Summary Follow up https://github.com/elastic/kibana/pull/184508 Increase kibanaReportCompletion to 10m: this should help with recent failures like > Reporting Generate CSV from SearchSource validation Searches a large amount of data, stops at Max Size Reached https://buildkite.com/elastic/appex-qa-serverless-kibana-ftr-tests/builds/1827 A report might take longer time to complete if the report is started when nodes are migrating [slack](https://elastic.slack.com/archives/C0574PUV998/p1717063959120689?thread_ts=1717018651.895229&cid=C0574PUV998) Also see https://github.com/elastic/kibana/issues/160329#issuecomment-2158578112 --- .../test_suites/common/reporting/generate_csv_discover.ts | 6 +++--- x-pack/test_serverless/shared/config.base.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts b/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts index 07ab0cd3bc969..07e7b94d30ab4 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/reporting/generate_csv_discover.ts @@ -68,10 +68,10 @@ export default function ({ getService }: FtrProviderContext) { * Tests */ describe('Generate CSV from SearchSource', function () { - // 7 minutes timeout for each test in serverless - // This is because it may take up to 5 minutes to generate the CSV + // 12 minutes timeout for each test in serverless + // This is because it may take up to 10 minutes to generate the CSV // see kibanaReportCompletion config - this.timeout(7 * 60 * 1000); + this.timeout(12 * 60 * 1000); beforeEach(async () => { await kibanaServer.uiSettings.update({ diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 213cbd6bdc4a3..6d9e134baef6f 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -159,7 +159,7 @@ export default async () => { try: 120 * 1000, waitFor: 20 * 1000, esRequestTimeout: 30 * 1000, - kibanaReportCompletion: 300 * 1000, + kibanaReportCompletion: 600 * 1000, kibanaStabilize: 15 * 1000, navigateStatusPageCheck: 250, waitForExists: 2500, From 70df50948d81c326e12fae287f08aaeed352b712 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 10 Jun 2024 18:50:09 +0200 Subject: [PATCH 13/14] [EDR Workflows] Fix flaky osquery test (#185819) --- .../plugins/osquery/cypress/e2e/all/packs_integration.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts index d9527001f704e..ff23c462afd02 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/packs_integration.cy.ts @@ -40,8 +40,7 @@ import { cleanupPack, cleanupAgentPolicy } from '../../tasks/api_fixtures'; import { request } from '../../tasks/common'; import { ServerlessRoleName } from '../../support/roles'; -// Failing: See https://github.com/elastic/kibana/issues/176543 -describe.skip('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { +describe('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { const integration = 'Osquery Manager'; describe( @@ -165,8 +164,9 @@ describe.skip('ALL - Packs', { tags: ['@ess', '@serverless'] }, () => { cy.contains('Run a set of queries in a pack.').click(); cy.getBySel(LIVE_QUERY_EDITOR).should('not.exist'); cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('select-live-pack').click().type('osquery-monitoring{downArrow}{enter}'); selectAllAgents(); + cy.getBySel('select-live-pack').click(); + cy.getBySel('select-live-pack').type('osquery-monitoring{downArrow}{enter}'); submitQuery(); cy.getBySel('toggleIcon-events').click(); checkResults(); From 2c76ad0ce39bb6951c34d237910f477851221283 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 10 Jun 2024 11:27:10 -0600 Subject: [PATCH 14/14] [ML] AIOps Log Rate Analysis: adds controls for controlling which columns will be visible (#184262) ## Summary Related meta issue: https://github.com/elastic/kibana/issues/182714 This PR adds controls to the AIOps results table to show/hide columns. image ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...utton.tsx => item_filter_apply_button.tsx} | 2 +- ...er_popover.tsx => item_filter_popover.tsx} | 145 +++---- .../log_rate_analysis_results.tsx | 72 +++- .../log_rate_analysis_results_table/index.ts | 1 + .../log_rate_analysis_results_table.tsx | 309 +------------- ...log_rate_analysis_results_table_groups.tsx | 199 +++------ .../use_columns.tsx | 392 ++++++++++++++++++ .../translations/translations/fr-FR.json | 10 - .../translations/translations/ja-JP.json | 10 - .../translations/translations/zh-CN.json | 10 - .../apps/aiops/log_rate_analysis.ts | 30 +- .../artificial_log_data_view_test_data.ts | 2 + .../farequote_data_view_test_data.ts | 2 + ...arequote_data_view_test_data_with_query.ts | 2 + .../kibana_logs_data_view_test_data.ts | 2 + .../aiops/log_rate_analysis_anomaly_table.ts | 2 +- x-pack/test/functional/apps/aiops/types.ts | 4 +- .../services/aiops/log_rate_analysis_page.ts | 20 +- 18 files changed, 655 insertions(+), 559 deletions(-) rename x-pack/plugins/aiops/public/components/log_rate_analysis/{field_filter_apply_button.tsx => item_filter_apply_button.tsx} (93%) rename x-pack/plugins/aiops/public/components/log_rate_analysis/{field_filter_popover.tsx => item_filter_popover.tsx} (51%) create mode 100644 x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_columns.tsx diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_apply_button.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_apply_button.tsx similarity index 93% rename from x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_apply_button.tsx rename to x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_apply_button.tsx index 0243ce1877952..0d3815da64506 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_apply_button.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_apply_button.tsx @@ -17,7 +17,7 @@ interface FieldFilterApplyButtonProps { tooltipContent?: string; } -export const FieldFilterApplyButton: FC = ({ +export const ItemFilterApplyButton: FC = ({ disabled, onClick, tooltipContent, diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_popover.tsx similarity index 51% rename from x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_popover.tsx rename to x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_popover.tsx index 46f816c86543b..25fb64def8550 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/item_filter_popover.tsx @@ -27,26 +27,40 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FieldFilterApplyButton } from './field_filter_apply_button'; +import { ItemFilterApplyButton } from './item_filter_apply_button'; -interface FieldFilterPopoverProps { +interface ItemFilterPopoverProps { + dataTestSubj: string; disabled?: boolean; disabledApplyButton?: boolean; - uniqueFieldNames: string[]; - onChange: (skippedFields: string[]) => void; + disabledApplyTooltipContent?: string; + helpText: string; + itemSearchAriaLabel: string; + initialSkippedItems?: string[]; + popoverButtonTitle: string; + selectedItemLimit?: number; + uniqueItemNames: string[]; + onChange: (skippedItems: string[]) => void; } // This component is mostly inspired by EUI's Data Grid Column Selector // https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/column_selector.tsx -export const FieldFilterPopover: FC = ({ +export const ItemFilterPopover: FC = ({ + dataTestSubj, disabled, disabledApplyButton, - uniqueFieldNames, + disabledApplyTooltipContent, + helpText, + itemSearchAriaLabel, + initialSkippedItems = [], + popoverButtonTitle, + selectedItemLimit = 2, + uniqueItemNames, onChange, }) => { const euiThemeContext = useEuiTheme(); // Inspired by https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/_data_grid_column_selector.scss - const fieldSelectPopover = useMemo( + const itemSelectPopover = useMemo( () => css` ${euiYScrollWithShadows(euiThemeContext, {})} max-height: 400px; @@ -55,48 +69,49 @@ export const FieldFilterPopover: FC = ({ ); const [isTouched, setIsTouched] = useState(false); - const [fieldSearchText, setFieldSearchText] = useState(''); - const [skippedFields, setSkippedFields] = useState([]); - const setFieldsFilter = (fieldNames: string[], checked: boolean) => { - let updatedSkippedFields = [...skippedFields]; + const [itemSearchText, setItemSearchText] = useState(''); + const [skippedItems, setSkippedItems] = useState(initialSkippedItems); + const setItemsFilter = (itemNames: string[], checked: boolean) => { + let updatedSkippedItems = [...skippedItems]; if (!checked) { - updatedSkippedFields.push(...fieldNames); + updatedSkippedItems.push(...itemNames); } else { - updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d)); + updatedSkippedItems = skippedItems.filter((d) => !itemNames.includes(d)); } - setSkippedFields(updatedSkippedFields); + // Ensure there are no duplicates + setSkippedItems([...new Set(updatedSkippedItems)]); setIsTouched(true); }; - const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false); - const onFieldSelectionButtonClick = () => setIsFieldSelectionPopoverOpen((isOpen) => !isOpen); - const closePopover = () => setIsFieldSelectionPopoverOpen(false); + const [isItemSelectionPopoverOpen, setIsItemSelectionPopoverOpen] = useState(false); + const onItemSelectionButtonClick = () => setIsItemSelectionPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsItemSelectionPopoverOpen(false); - const filteredUniqueFieldNames = useMemo(() => { - return uniqueFieldNames.filter( - (d) => d.toLowerCase().indexOf(fieldSearchText.toLowerCase()) !== -1 + const filteredUniqueItemNames = useMemo(() => { + return uniqueItemNames.filter( + (d) => d.toLowerCase().indexOf(itemSearchText.toLowerCase()) !== -1 ); - }, [fieldSearchText, uniqueFieldNames]); + }, [itemSearchText, uniqueItemNames]); // If the supplied list of unique field names changes, do a sanity check to only // keep field names in the list of skipped fields that still are in the list of unique fields. useEffect(() => { - setSkippedFields((previousSkippedFields) => - previousSkippedFields.filter((d) => uniqueFieldNames.includes(d)) + setSkippedItems((previousSkippedItems) => + previousSkippedItems.filter((d) => uniqueItemNames.includes(d)) ); - }, [uniqueFieldNames]); + }, [uniqueItemNames]); - const selectedFieldCount = uniqueFieldNames.length - skippedFields.length; + const selectedItemCount = uniqueItemNames.length - skippedItems.length; return ( = ({ iconSize="s" color="text" > - + {popoverButtonTitle} } - isOpen={isFieldSelectionPopoverOpen} + isOpen={isItemSelectionPopoverOpen} closePopover={closePopover} > - + {helpText} = ({ placeholder={i18n.translate('xpack.aiops.analysis.fieldSelectorPlaceholder', { defaultMessage: 'Search', })} - aria-label={i18n.translate('xpack.aiops.analysis.fieldSelectorAriaLabel', { - defaultMessage: 'Filter fields', - })} - value={fieldSearchText} - onChange={(e: ChangeEvent) => setFieldSearchText(e.currentTarget.value)} + aria-label={itemSearchAriaLabel} + value={itemSearchText} + onChange={(e: ChangeEvent) => setItemSearchText(e.currentTarget.value)} data-test-subj="aiopsFieldSelectorSearch" /> -
- {filteredUniqueFieldNames.map((fieldName) => ( +
+ {filteredUniqueItemNames.map((fieldName) => (
setFieldsFilter([fieldName], e.target.checked)} - checked={!skippedFields.includes(fieldName)} + onChange={(e) => setItemsFilter([fieldName], e.target.checked)} + checked={!skippedItems.includes(fieldName)} />
))} @@ -162,19 +169,19 @@ export const FieldFilterPopover: FC = ({ setFieldsFilter(filteredUniqueFieldNames, true)} - disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0} + onClick={() => setItemsFilter(filteredUniqueItemNames, true)} + disabled={itemSearchText.length > 0 && filteredUniqueItemNames.length === 0} data-test-subj="aiopsFieldSelectorSelectAllFieldsButton" > - {fieldSearchText.length > 0 ? ( + {itemSearchText.length > 0 ? ( ) : ( )} @@ -183,39 +190,35 @@ export const FieldFilterPopover: FC = ({ setFieldsFilter(filteredUniqueFieldNames, false)} - disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0} + onClick={() => setItemsFilter(filteredUniqueItemNames, false)} + disabled={itemSearchText.length > 0 && filteredUniqueItemNames.length === 0} data-test-subj="aiopsFieldSelectorDeselectAllFieldsButton" > - {fieldSearchText.length > 0 ? ( + {itemSearchText.length > 0 ? ( ) : ( )} - { - onChange(skippedFields); - setFieldSearchText(''); - setIsFieldSelectionPopoverOpen(false); + onChange(skippedItems); + setItemSearchText(''); + setIsItemSelectionPopoverOpen(false); closePopover(); }} - disabled={disabledApplyButton || selectedFieldCount < 2 || !isTouched} + disabled={disabledApplyButton || selectedItemCount < selectedItemLimit || !isTouched} tooltipContent={ - selectedFieldCount < 2 - ? i18n.translate('xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', { - defaultMessage: 'Grouping requires at least 2 fields to be selected.', - }) - : undefined + selectedItemCount < selectedItemLimit ? disabledApplyTooltipContent : undefined } /> diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx index cee76add52fe9..8ff224b65f678 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx @@ -41,6 +41,10 @@ import { useLogRateAnalysisStateContext } from '@kbn/aiops-components'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useDataSource } from '../../hooks/use_data_source'; +import { + commonColumns, + significantItemColumns, +} from '../log_rate_analysis_results_table/use_columns'; import { getGroupTableItems, @@ -48,8 +52,10 @@ import { LogRateAnalysisResultsGroupsTable, } from '../log_rate_analysis_results_table'; -import { FieldFilterPopover } from './field_filter_popover'; +import { ItemFilterPopover as FieldFilterPopover } from './item_filter_popover'; +import { ItemFilterPopover as ColumnFilterPopover } from './item_filter_popover'; import { LogRateAnalysisTypeCallOut } from './log_rate_analysis_type_callout'; +import type { ColumnNames } from '../log_rate_analysis_results_table'; const groupResultsMessage = i18n.translate( 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResults', @@ -77,6 +83,37 @@ const groupResultsOnMessage = i18n.translate( ); const resultsGroupedOffId = 'aiopsLogRateAnalysisGroupingOff'; const resultsGroupedOnId = 'aiopsLogRateAnalysisGroupingOn'; +const fieldFilterHelpText = i18n.translate('xpack.aiops.logRateAnalysis.page.fieldFilterHelpText', { + defaultMessage: + 'Deselect non-relevant fields to remove them from groups and click the Apply button to rerun the grouping. Use the search bar to filter the list, then select/deselect multiple fields with the actions below.', +}); +const columnsFilterHelpText = i18n.translate( + 'xpack.aiops.logRateAnalysis.page.columnsFilterHelpText', + { + defaultMessage: 'Configure visible columns.', + } +); +const disabledFieldFilterApplyButtonTooltipContent = i18n.translate( + 'xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', + { + defaultMessage: 'Grouping requires at least 2 fields to be selected.', + } +); +const disabledColumnFilterApplyButtonTooltipContent = i18n.translate( + 'xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected', + { + defaultMessage: 'At least one column must be selected.', + } +); +const columnSearchAriaLabel = i18n.translate('xpack.aiops.analysis.columnSelectorAriaLabel', { + defaultMessage: 'Filter columns', +}); +const columnsButton = i18n.translate('xpack.aiops.logRateAnalysis.page.columnsFilterButtonLabel', { + defaultMessage: 'Columns', +}); +const fieldsButton = i18n.translate('xpack.aiops.analysis.fieldFilterButtonLabel', { + defaultMessage: 'Filter fields', +}); /** * Interface for log rate analysis results data. @@ -157,6 +194,7 @@ export const LogRateAnalysisResults: FC = ({ ); const [shouldStart, setShouldStart] = useState(false); const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId); + const [skippedColumns, setSkippedColumns] = useState(['p-value']); const onGroupResultsToggle = (optionId: string) => { setToggleIdSelected(optionId); @@ -179,6 +217,10 @@ export const LogRateAnalysisResults: FC = ({ startHandler(true, false); }; + const onVisibleColumnsChange = (columns: ColumnNames[]) => { + setSkippedColumns(columns); + }; + const { cancel, start, @@ -378,12 +420,36 @@ export const LogRateAnalysisResults: FC = ({ + + void} + /> + {showLogRateAnalysisResultsTable && currentAnalysisType !== undefined && ( <> @@ -481,6 +547,7 @@ export const LogRateAnalysisResults: FC = ({ > {showLogRateAnalysisResultsTable && groupResults ? ( = ({ ) : null} {showLogRateAnalysisResultsTable && !groupResults ? ( = timeRangeMs, barColorOverride, barHighlightColorOverride, + skippedColumns, zeroDocsFallback = false, }) => { const euiTheme = useEuiTheme(); const primaryBackgroundColor = useEuiBackgroundColor('primary'); - const { dataView } = useDataSource(); - const dataViewId = dataView.id; const { pinnedGroup, @@ -100,263 +71,17 @@ export const LogRateAnalysisResultsTable: FC = zeroDocsFallback ? DEFAULT_SORT_DIRECTION_ZERO_DOCS_FALLBACK : DEFAULT_SORT_DIRECTION ); - const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext(); - - const fieldStatsServices: FieldStatsServices = useMemo(() => { - return { - uiSettings, - dataViews: data.dataViews, - data, - fieldFormats, - charts, - }; - }, [uiSettings, data, fieldFormats, charts]); - - const copyToClipBoardAction = useCopyToClipboardAction(); - const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); - const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); - - const columns: Array> = [ - { - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldName', - field: 'fieldName', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldNameLabel', { - defaultMessage: 'Field name', - }), - render: (_, { fieldName, fieldValue, key, type, doc_count: count }) => { - const dslQuery = - type === SIGNIFICANT_ITEM_TYPE.KEYWORD - ? searchQuery - : getCategoryQuery(fieldName, [ - { - key, - count, - examples: [], - regex: '', - }, - ]); - return ( - <> - {type === SIGNIFICANT_ITEM_TYPE.KEYWORD && ( - - )} - {type === SIGNIFICANT_ITEM_TYPE.LOG_PATTERN && ( - - )} - - - {fieldName} - - - ); - }, - sortable: true, - valign: 'middle', - }, - { - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldValue', - field: 'fieldValue', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldValueLabel', { - defaultMessage: 'Field value', - }), - render: (_, { fieldValue, type }) => ( - - {type === 'keyword' ? ( - String(fieldValue) - ) : ( - - - {String(fieldValue)} - - - )} - - ), - sortable: true, - textOnly: true, - truncateText: { lines: TRUNCATE_TEXT_LINES }, - valign: 'middle', - }, - { - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnLogRate', - width: NARROW_COLUMN_WIDTH, - field: 'pValue', - name: ( - <> - -   - - - ), - render: (_, { histogram, fieldName, fieldValue }) => ( - - ), - sortable: false, - valign: 'middle', - }, - { - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnDocCount', - width: NARROW_COLUMN_WIDTH, - field: 'doc_count', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountLabel', { - defaultMessage: 'Doc count', - }), - sortable: true, - valign: 'middle', - }, - ]; - - if (!zeroDocsFallback) { - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnPValue', - width: NARROW_COLUMN_WIDTH, - field: 'pValue', - name: ( - <> - -   - - - ), - render: (pValue: number | null) => pValue?.toPrecision(3) ?? NOT_AVAILABLE, - sortable: true, - valign: 'middle', - }); - - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnImpact', - width: NARROW_COLUMN_WIDTH, - field: 'pValue', - name: ( - <> - -   - - - ), - render: (_, { pValue }) => { - if (typeof pValue !== 'number') return NOT_AVAILABLE; - const label = getFailedTransactionsCorrelationImpactLabel(pValue); - return label ? {label.impact} : null; - }, - sortable: true, - valign: 'middle', - }); - } - - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnAction', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnName', { - defaultMessage: 'Actions', - }), - actions: [ - ...(viewInDiscoverAction ? [viewInDiscoverAction] : []), - ...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []), - copyToClipBoardAction, - ], - width: ACTIONS_COLUMN_WIDTH, - valign: 'middle', - }); - - if (isExpandedRow === true) { - columns.unshift({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnUnique', - width: UNIQUE_COLUMN_WIDTH, - field: 'unique', - name: '', - render: (_, { unique }) => { - if (unique) { - return ( - - ); - } - return ''; - }, - sortable: false, - valign: 'middle', - }); - } + const columns = useColumns( + SIG_ITEMS_TABLE, + skippedColumns, + searchQuery, + timeRangeMs, + loading, + zeroDocsFallback, + barColorOverride, + barHighlightColorOverride, + isExpandedRow + ); const onChange = useCallback((tableSettings) => { if (tableSettings.page) { diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx index c388555bcf2ca..d0935ebb96ff0 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx @@ -32,20 +32,13 @@ import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import { stringHash } from '@kbn/ml-string-hash'; import { useLogRateAnalysisStateContext, type GroupTableItem } from '@kbn/aiops-components'; -import { useDataSource } from '../../hooks/use_data_source'; +import usePrevious from 'react-use/lib/usePrevious'; +import useMountedState from 'react-use/lib/useMountedState'; -import { MiniHistogram } from '../mini_histogram'; - -import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; import { LogRateAnalysisResultsTable } from './log_rate_analysis_results_table'; -import { useCopyToClipboardAction } from './use_copy_to_clipboard_action'; -import { useViewInDiscoverAction } from './use_view_in_discover_action'; -import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action'; +import { GROUPS_TABLE, useColumns } from './use_columns'; -const NARROW_COLUMN_WIDTH = '120px'; const EXPAND_COLUMN_WIDTH = '40px'; -const ACTIONS_COLUMN_WIDTH = '60px'; -const NOT_AVAILABLE = '--'; const MAX_GROUP_BADGES = 5; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; @@ -55,6 +48,7 @@ const DEFAULT_SORT_DIRECTION = 'asc'; const DEFAULT_SORT_DIRECTION_ZERO_DOCS_FALLBACK = 'desc'; interface LogRateAnalysisResultsTableProps { + skippedColumns: string[]; significantItems: SignificantItem[]; groupTableItems: GroupTableItem[]; loading: boolean; @@ -68,6 +62,7 @@ interface LogRateAnalysisResultsTableProps { } export const LogRateAnalysisResultsGroupsTable: FC = ({ + skippedColumns, significantItems, groupTableItems, loading, @@ -77,7 +72,7 @@ export const LogRateAnalysisResultsGroupsTable: FC { - const { dataView } = useDataSource(); + const prevSkippedColumns = usePrevious(skippedColumns); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -97,7 +92,7 @@ export const LogRateAnalysisResultsGroupsTable: FC { const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; @@ -106,6 +101,7 @@ export const LogRateAnalysisResultsGroupsTable: FC( (p, groupItem) => { const st = significantItems.find( @@ -135,11 +131,7 @@ export const LogRateAnalysisResultsGroupsTable: FC> = [ + const groupColumns: Array> = [ { align: RIGHT_ALIGNMENT, width: EXPAND_COLUMN_WIDTH, @@ -199,7 +191,7 @@ export const LogRateAnalysisResultsGroupsTable: FC ), render: (_, { uniqueItemsCount, groupItemsSortedByUniqueness }) => { - const valuesBadges = []; + const valuesBadges: React.ReactNode[] = []; for (const groupItem of groupItemsSortedByUniqueness) { const { fieldName, fieldValue, duplicate } = groupItem; @@ -259,141 +251,20 @@ export const LogRateAnalysisResultsGroupsTable: FC - -   - - - ), - render: (_, { histogram, id }) => ( - - ), - sortable: false, - valign: 'top', - }, - { - 'data-test-subj': 'aiopsLogRateAnalysisResultsGroupsTableColumnDocCount', - width: NARROW_COLUMN_WIDTH, - field: 'docCount', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountLabel', { - defaultMessage: 'Doc count', - }), - sortable: true, - valign: 'top', - }, ]; - if (!zeroDocsFallback) { - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsGroupsTableColumnPValue', - width: NARROW_COLUMN_WIDTH, - field: 'pValue', - name: ( - <> - -   - - - ), - render: (pValue: number | null) => pValue?.toPrecision(3) ?? NOT_AVAILABLE, - sortable: true, - valign: 'top', - }); + const columns = useColumns( + GROUPS_TABLE, + skippedColumns, + searchQuery, + timeRangeMs, + loading, + zeroDocsFallback, + barColorOverride, + barHighlightColorOverride + ) as Array>; - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnImpact', - width: NARROW_COLUMN_WIDTH, - field: 'pValue', - name: ( - <> - -   - - - ), - render: (_, { pValue }) => { - if (!pValue) return NOT_AVAILABLE; - const label = getFailedTransactionsCorrelationImpactLabel(pValue); - return label ? {label.impact} : null; - }, - sortable: true, - valign: 'top', - }); - } - - columns.push({ - 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnAction', - name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnName', { - defaultMessage: 'Actions', - }), - actions: [ - ...(viewInDiscoverAction ? [viewInDiscoverAction] : []), - ...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []), - copyToClipBoardAction, - ], - width: ACTIONS_COLUMN_WIDTH, - valign: 'top', - }); + groupColumns.push(...columns); const onChange = useCallback((tableSettings) => { if (tableSettings.page) { @@ -481,6 +352,32 @@ export const LogRateAnalysisResultsGroupsTable: FC 0 + ) { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + for (const itemId in itemIdToExpandedRowMapValues) { + if (itemIdToExpandedRowMapValues.hasOwnProperty(itemId)) { + const component = itemIdToExpandedRowMapValues[itemId]; + itemIdToExpandedRowMapValues[itemId] = ( + + ); + } + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [prevSkippedColumns, skippedColumns, itemIdToExpandedRowMap] + ); + const getRowStyle = (group: GroupTableItem) => { if (pinnedGroup && pinnedGroup.id === group.id) { return { @@ -503,7 +400,7 @@ export const LogRateAnalysisResultsGroupsTable: FC> => { + const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext(); + const { dataView } = useDataSource(); + const euiTheme = useEuiTheme(); + const viewInDiscoverAction = useViewInDiscoverAction(dataView.id); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataView.id); + const copyToClipBoardAction = useCopyToClipboardAction(); + + const isGroupsTable = tableType === GROUPS_TABLE; + + const fieldStatsServices: FieldStatsServices = useMemo(() => { + return { + uiSettings, + dataViews: data.dataViews, + data, + fieldFormats, + charts, + }; + }, [uiSettings, data, fieldFormats, charts]); + + const columnsMap: Record> = useMemo( + () => ({ + ['Field name']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldName', + field: 'fieldName', + name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldNameLabel', { + defaultMessage: 'Field name', + }), + render: (_, { fieldName, fieldValue, key, type, doc_count: count }) => { + const dslQuery = + type === SIGNIFICANT_ITEM_TYPE.KEYWORD + ? searchQuery + : getCategoryQuery(fieldName, [ + { + key, + count, + examples: [], + regex: '', + }, + ]); + return ( + <> + {type === SIGNIFICANT_ITEM_TYPE.KEYWORD && ( + + )} + {type === SIGNIFICANT_ITEM_TYPE.LOG_PATTERN && ( + + )} + + + {fieldName} + + + ); + }, + sortable: true, + valign: 'middle', + }, + ['Field value']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldValue', + field: 'fieldValue', + name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldValueLabel', { + defaultMessage: 'Field value', + }), + render: (_, { fieldValue, type }) => ( + + {type === 'keyword' ? ( + String(fieldValue) + ) : ( + + + {String(fieldValue)} + + + )} + + ), + sortable: true, + textOnly: true, + truncateText: { lines: TRUNCATE_TEXT_LINES }, + valign: 'middle', + }, + ['Log rate']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnLogRate', + width: NARROW_COLUMN_WIDTH, + field: 'pValue', + name: ( + <> + +   + + + ), + render: (_, { histogram, fieldName, fieldValue }) => ( + + ), + sortable: false, + valign: 'middle', + }, + ['Impact']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnImpact', + width: NARROW_COLUMN_WIDTH, + field: 'pValue', + name: ( + <> + +   + + + ), + render: (_, { pValue }) => { + if (typeof pValue !== 'number') return NOT_AVAILABLE; + const label = getFailedTransactionsCorrelationImpactLabel(pValue); + return label ? {label.impact} : null; + }, + sortable: true, + valign: 'middle', + }, + ['p-value']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnPValue', + width: NARROW_COLUMN_WIDTH, + field: 'pValue', + name: ( + <> + +   + + + ), + render: (pValue: number | null) => pValue?.toPrecision(3) ?? NOT_AVAILABLE, + sortable: true, + valign: 'middle', + }, + ['Doc count']: { + 'data-test-subj': isGroupsTable + ? 'aiopsLogRateAnalysisResultsGroupsTableColumnDocCount' + : 'aiopsLogRateAnalysisResultsTableColumnDocCount', + width: NARROW_COLUMN_WIDTH, + field: isGroupsTable ? 'docCount' : 'doc_count', + name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountLabel', { + defaultMessage: 'Doc count', + }), + sortable: true, + valign: 'middle', + }, + ['Actions']: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnAction', + name: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions: [ + ...(viewInDiscoverAction ? [viewInDiscoverAction] : []), + ...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []), + copyToClipBoardAction, + ], + width: ACTIONS_COLUMN_WIDTH, + valign: 'middle', + }, + unique: { + 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnUnique', + width: UNIQUE_COLUMN_WIDTH, + field: 'unique', + name: '', + render: (_, { unique }) => { + if (unique) { + return ( + + ); + } + return ''; + }, + sortable: false, + valign: 'middle', + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + barColorOverride, + barHighlightColorOverride, + copyToClipBoardAction, + dataView?.id, + euiTheme, + fieldStatsServices, + loading, + searchQuery, + timeRangeMs, + viewInDiscoverAction, + viewInLogPatternAnalysisAction, + ] + ); + + const columns = useMemo(() => { + const columnNamesToReturn: Partial> = isGroupsTable + ? commonColumns + : significantItemColumns; + const columnsToReturn = []; + + for (const columnName in columnNamesToReturn) { + if ( + columnNamesToReturn.hasOwnProperty(columnName) === false || + skippedColumns.includes(columnNamesToReturn[columnName as ColumnNames] as string) || + ((columnName === 'p-value' || columnName === 'Impact') && zeroDocsFallback) + ) + continue; + + columnsToReturn.push(columnsMap[columnName as ColumnNames]); + } + + if (isExpandedRow === true) { + columnsToReturn.unshift(columnsMap.unique); + } + + return columnsToReturn; + }, [isGroupsTable, skippedColumns, zeroDocsFallback, isExpandedRow, columnsMap]); + + return columns; +}; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f4067130e1346..59bdf5efef3e0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -8200,7 +8200,6 @@ "xpack.aiops.analysis.analysisTypeSpikeCallOutContentFallback": "La plage temporelle de référence de base ne contient aucun document. Les résultats montrent donc les catégories de message des meilleurs logs et les valeurs des champs pour la plage temporelle de déviation.", "xpack.aiops.analysis.analysisTypeSpikeCallOutTitle": "Type d'analyse : Pic du taux de log", "xpack.aiops.analysis.analysisTypeSpikeFallbackCallOutTitle": "Type d'analyse : Meilleurs éléments pour la plage temporelle de déviation", - "xpack.aiops.analysis.fieldSelectorAriaLabel": "Champs de filtre", "xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected": "Le regroupement nécessite la sélection d'au moins 2 champs.", "xpack.aiops.analysis.fieldSelectorPlaceholder": "Recherche", "xpack.aiops.analysisCompleteLabel": "Analyse terminée", @@ -8336,12 +8335,7 @@ "xpack.aiops.logRateAnalysis.page.emptyPromptBody": "La fonction d'analyse des pics de taux de log identifie les combinaisons champ/valeur statistiquement significatives qui contribuent à un pic ou une baisse de taux de log.", "xpack.aiops.logRateAnalysis.page.emptyPromptTitle": "Cliquez sur un pic ou une baisse dans l'histogramme pour lancer l'analyse.", "xpack.aiops.logRateAnalysis.page.fieldFilterApplyButtonLabel": "Appliquer", - "xpack.aiops.logRateAnalysis.page.fieldFilterButtonLabel": "Champs de filtre", "xpack.aiops.logRateAnalysis.page.fieldFilterHelpText": "Désélectionnez les champs non pertinents pour les supprimer des groupes et cliquez sur le bouton Appliquer pour réexécuter le regroupement. Utilisez la barre de recherche pour filtrer la liste, puis sélectionnez/désélectionnez plusieurs champs avec les actions ci-dessous.", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllFields": "Désélectionner tous les champs", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedFields": "Désélectionner les champs filtrés", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllFields": "Sélectionner tous les champs", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllSearchedFields": "Sélectionner les champs filtrés", "xpack.aiops.logRateAnalysis.page.noResultsPromptBody": "Essayez d'ajuster la référence de base et les plages temporelles d'écart-type, et réexécutez l'analyse. Si vous n'obtenez toujours aucun résultat, il se peut qu'il n'y ait aucune entité statistiquement significative contribuant à cet écart dans les taux de log.", "xpack.aiops.logRateAnalysis.page.noResultsPromptTitle": "L'analyse n'a retourné aucun résultat.", "xpack.aiops.logRateAnalysis.page.tryToContinueAnalysisButtonText": "Essayer de continuer l'analyse", @@ -8375,12 +8369,8 @@ "xpack.aiops.logRateAnalysis.resultsTable.pValueLabel": "valeur-p", "xpack.aiops.logRateAnalysis.resultsTable.uniqueColumnTooltip": "Cette paire champ/valeur apparaît uniquement dans ce groupe", "xpack.aiops.logRateAnalysis.resultsTableGroups.groupLabel": "Regrouper", - "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabel": "Impact", "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabelColumnTooltip": "Niveau d'impact du groupe sur la différence de taux de messages", "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateColumnTooltip": "Représentation visuelle de l'impact du groupe sur la différence de taux de messages.", - "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateLabel": "Taux du log", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueColumnTooltip": "Une différence de fréquence des valeurs, surtout si celles-ci sont plus faibles, indique un changement important. Ordonner un tri de cette colonne entraînera automatiquement un tri secondaire sur la colonne \"nombre de documents\"..", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueLabel": "valeur-p", "xpack.aiops.logRateAnalysisTimeSeriesWarning.description": "L'analyse des taux de log ne fonctionne que sur des index temporels.", "xpack.aiops.miniHistogram.noDataLabel": "S. O.", "xpack.aiops.navMenu.mlAppNameText": "Machine Learning", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f18f09518aa37..16c093425cc5a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8188,7 +8188,6 @@ "xpack.aiops.analysis.analysisTypeSpikeCallOutContentFallback": "ベースライン時間範囲にはドキュメントが含まれていません。したがって、結果は、偏差時間範囲の上位のログメッセージカテゴリーとフィールド値を示しています。", "xpack.aiops.analysis.analysisTypeSpikeCallOutTitle": "分析タイプ:ログレートスパイク", "xpack.aiops.analysis.analysisTypeSpikeFallbackCallOutTitle": "分析タイプ:偏差時間範囲の上位のアイテム", - "xpack.aiops.analysis.fieldSelectorAriaLabel": "フィールドのフィルタリング", "xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected": "グループ化するには、2つ以上のフィールドを選択する必要があります。", "xpack.aiops.analysis.fieldSelectorPlaceholder": "検索", "xpack.aiops.analysisCompleteLabel": "分析完了", @@ -8324,12 +8323,7 @@ "xpack.aiops.logRateAnalysis.page.emptyPromptBody": "ログレート分析機能は、ログレートのスパイクまたはディップに寄与する統計的に有意なフィールド/値の組み合わせを特定します。", "xpack.aiops.logRateAnalysis.page.emptyPromptTitle": "ヒストグラム図のスパイクまたはディップをクリックすると、分析が開始します。", "xpack.aiops.logRateAnalysis.page.fieldFilterApplyButtonLabel": "適用", - "xpack.aiops.logRateAnalysis.page.fieldFilterButtonLabel": "フィールドのフィルタリング", "xpack.aiops.logRateAnalysis.page.fieldFilterHelpText": "関連しないフィールドを選択解除すると、グループから削除されます。[適用]ボタンをクリックすると、グループ化が再実行されます。 検索バーを使用してリストをフィルタリングしてから、下のアクションを使用して複数のフィールドを選択/選択解除します。", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllFields": "すべてのフィールドを選択解除", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedFields": "フィルタリングされたフィールドを選択解除", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllFields": "すべてのフィールドを選択", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllSearchedFields": "フィルタリングされたフィールドを選択", "xpack.aiops.logRateAnalysis.page.noResultsPromptBody": "ベースラインと時間範囲のずれを調整し、分析を再実行してください。結果が得られない場合は、このログレートの偏差に寄与する統計的に有意なエンティティが存在しない可能性があります。", "xpack.aiops.logRateAnalysis.page.noResultsPromptTitle": "分析の結果が返されませんでした。", "xpack.aiops.logRateAnalysis.page.tryToContinueAnalysisButtonText": "分析を続行してください", @@ -8363,12 +8357,8 @@ "xpack.aiops.logRateAnalysis.resultsTable.pValueLabel": "p値", "xpack.aiops.logRateAnalysis.resultsTable.uniqueColumnTooltip": "このフィールド/値の組み合わせはこのグループでのみ出現します", "xpack.aiops.logRateAnalysis.resultsTableGroups.groupLabel": "グループ", - "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabel": "インパクト", "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabelColumnTooltip": "メッセージレート差異に対するグループの影響のレベル", "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateColumnTooltip": "メッセージレート差異に対するグループの影響の視覚的な表示。", - "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateLabel": "ログレート", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueColumnTooltip": "値の頻度の変化の重要性。値が小さいほど変化が大きいことを示します。この列をソートすると、自動的にdoc count列が二次ソートされます。", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueLabel": "p値", "xpack.aiops.logRateAnalysisTimeSeriesWarning.description": "ログレートは、時間ベースのインデックスに対してのみ実行されます。", "xpack.aiops.miniHistogram.noDataLabel": "N/A", "xpack.aiops.navMenu.mlAppNameText": "機械学習", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90699177a60b4..a44779b8047d1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8204,7 +8204,6 @@ "xpack.aiops.analysis.analysisTypeSpikeCallOutContentFallback": "基线时间范围不包含任何文档。因此,结果将显示偏差时间范围的主要日志消息类别和字段值。", "xpack.aiops.analysis.analysisTypeSpikeCallOutTitle": "分析类型:日志速率峰值", "xpack.aiops.analysis.analysisTypeSpikeFallbackCallOutTitle": "分析类型:偏差时间范围的主要项目", - "xpack.aiops.analysis.fieldSelectorAriaLabel": "筛选字段", "xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected": "分组至少需要 2 个供选择的字段。", "xpack.aiops.analysis.fieldSelectorPlaceholder": "搜索", "xpack.aiops.analysisCompleteLabel": "分析已完成", @@ -8340,12 +8339,7 @@ "xpack.aiops.logRateAnalysis.page.emptyPromptBody": "“日志速率分析”功能会识别有助于达到日志速率峰值或谷值的具有统计意义的字段/值组合。", "xpack.aiops.logRateAnalysis.page.emptyPromptTitle": "单击直方图中的某个峰值或谷值可开始分析。", "xpack.aiops.logRateAnalysis.page.fieldFilterApplyButtonLabel": "应用", - "xpack.aiops.logRateAnalysis.page.fieldFilterButtonLabel": "筛选字段", "xpack.aiops.logRateAnalysis.page.fieldFilterHelpText": "取消选择非相关字段以将其从组中移除,然后单击“应用”按钮返回分组。 使用搜索栏筛选列表,然后通过以下操作选择/取消选择多个字段。", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllFields": "取消选择所有字段", - "xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedFields": "取消选择已筛选字段", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllFields": "选择所有字段", - "xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllSearchedFields": "选择已筛选字段", "xpack.aiops.logRateAnalysis.page.noResultsPromptBody": "尝试调整基线和偏差时间范围,然后重新运行分析。如果仍然没有结果,可能没有具有统计意义的实体导致了该日志速率偏差。", "xpack.aiops.logRateAnalysis.page.noResultsPromptTitle": "分析未返回任何结果。", "xpack.aiops.logRateAnalysis.page.tryToContinueAnalysisButtonText": "尝试继续分析", @@ -8379,12 +8373,8 @@ "xpack.aiops.logRateAnalysis.resultsTable.pValueLabel": "p-value", "xpack.aiops.logRateAnalysis.resultsTable.uniqueColumnTooltip": "此字段/值对只在该分组中出现", "xpack.aiops.logRateAnalysis.resultsTableGroups.groupLabel": "组", - "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabel": "影响", "xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabelColumnTooltip": "组对消息速率差异的影响水平", "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateColumnTooltip": "组对消息速率差异的影响的视觉表示形式。", - "xpack.aiops.logRateAnalysis.resultsTableGroups.logRateLabel": "日志速率", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueColumnTooltip": "值的频率更改的意义;值越小表示变化越大;对此列排序会自动在文档计数列上进行二次排序。", - "xpack.aiops.logRateAnalysis.resultsTableGroups.pValueLabel": "p-value", "xpack.aiops.logRateAnalysisTimeSeriesWarning.description": "日志速率分析仅在基于时间的索引上运行。", "xpack.aiops.miniHistogram.noDataLabel": "不可用", "xpack.aiops.navMenu.mlAppNameText": "Machine Learning", diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts index 7308d561109be..e9f245df27514 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts @@ -215,6 +215,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await aiops.logRateAnalysisResultsGroupsTable.expandRow(); await aiops.logRateAnalysisResultsGroupsTable.scrollAnalysisTableIntoView(); + await ml.testExecution.logTestStep('open the column filter'); + await aiops.logRateAnalysisPage.assertFilterPopoverButtonExists( + 'aiopsColumnFilterButton', + false + ); + await aiops.logRateAnalysisPage.clickFilterPopoverButton('aiopsColumnFilterButton', true); + await aiops.logRateAnalysisPage.assertFieldSelectorFieldNameList( + testData.expected.columnSelectorPopover + ); + + await ml.testExecution.logTestStep('filter columns'); + await aiops.logRateAnalysisPage.setFieldSelectorSearch(testData.columnSelectorSearch); + await aiops.logRateAnalysisPage.assertFieldSelectorFieldNameList([ + testData.columnSelectorSearch, + ]); + await aiops.logRateAnalysisPage.clickFieldSelectorListItem( + 'aiopsFieldSelectorFieldNameListItem' + ); + await aiops.logRateAnalysisPage.assertFieldFilterApplyButtonExists(false); + await aiops.logRateAnalysisPage.clickFieldFilterApplyButton('aiopsColumnFilterButton'); + const analysisTable = await aiops.logRateAnalysisResultsTable.parseAnalysisTable(); const actualAnalysisTable = orderBy(analysisTable, ['fieldName', 'fieldValue']); @@ -231,8 +252,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); await ml.testExecution.logTestStep('open the field filter'); - await aiops.logRateAnalysisPage.assertFieldFilterPopoverButtonExists(false); - await aiops.logRateAnalysisPage.clickFieldFilterPopoverButton(true); + await aiops.logRateAnalysisPage.assertFilterPopoverButtonExists( + 'aiopsFieldFilterButton', + false + ); + await aiops.logRateAnalysisPage.clickFilterPopoverButton('aiopsFieldFilterButton', true); await aiops.logRateAnalysisPage.assertFieldSelectorFieldNameList( testData.expected.fieldSelectorPopover ); @@ -249,7 +273,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { if (testData.fieldSelectorApplyAvailable) { await ml.testExecution.logTestStep('regroup results'); - await aiops.logRateAnalysisPage.clickFieldFilterApplyButton(); + await aiops.logRateAnalysisPage.clickFieldFilterApplyButton('aiopsFieldFilterButton'); const filteredAnalysisGroupsTable = await aiops.logRateAnalysisResultsGroupsTable.parseAnalysisTable(); diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/artificial_log_data_view_test_data.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/artificial_log_data_view_test_data.ts index ff6bfd4f9f19f..d2c3f1987667b 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/artificial_log_data_view_test_data.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/artificial_log_data_view_test_data.ts @@ -143,6 +143,7 @@ export const getArtificialLogDataViewTestData = ({ brushDeviationTargetTimestamp: getBrushDeviationTargetTimestamp(), brushIntervalFactor: zeroDocsFallback ? 1 : 10, chartClickCoordinates: [-200, 30], + columnSelectorSearch: 'p-value', fieldSelectorSearch: 'user', fieldSelectorApplyAvailable: true, expected: { @@ -150,6 +151,7 @@ export const getArtificialLogDataViewTestData = ({ analysisGroupsTable: getAnalysisGroupsTable(), filteredAnalysisGroupsTable: getFilteredAnalysisGroupsTable(), analysisTable: getAnalysisTable(), + columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'], fieldSelectorPopover: getFieldSelectorPopover(), globalState: { refreshInterval: { pause: true, value: 60000 }, diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data.ts index 07b116e544e20..84e5cbed3e400 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data.ts @@ -19,11 +19,13 @@ export const farequoteDataViewTestData: TestData = { brushDeviationTargetTimestamp: 1455033600000, brushIntervalFactor: 1, chartClickCoordinates: [0, 0], + columnSelectorSearch: 'p-value', fieldSelectorSearch: 'airline', fieldSelectorApplyAvailable: false, expected: { totalDocCountFormatted: '86,374', sampleProbabilityFormatted: '0.5', + columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'], fieldSelectorPopover: ['airline', 'custom_field.keyword'], globalState: { refreshInterval: { pause: true, value: 60000 }, diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts index 14e754550f608..42fddac191988 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/farequote_data_view_test_data_with_query.ts @@ -19,6 +19,7 @@ export const farequoteDataViewTestDataWithQuery: TestData = { brushDeviationTargetTimestamp: 1455033600000, brushIntervalFactor: 1, chartClickCoordinates: [0, 0], + columnSelectorSearch: 'p-value', fieldSelectorSearch: 'airline', fieldSelectorApplyAvailable: false, query: 'NOT airline:("SWR" OR "ACA" OR "AWE" OR "BAW" OR "JAL" OR "JBU" OR "JZA" OR "KLM")', @@ -43,6 +44,7 @@ export const farequoteDataViewTestDataWithQuery: TestData = { impact: 'High', }, ], + columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'], fieldSelectorPopover: ['airline', 'custom_field.keyword'], globalState: { refreshInterval: { pause: true, value: 60000 }, diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts index 4a5f9dddb6f1f..9645863ed1e82 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis/test_data/kibana_logs_data_view_test_data.ts @@ -19,6 +19,7 @@ export const kibanaLogsDataViewTestData: TestData = { sourceIndexOrSavedSearch: 'kibana_sample_data_logstsdb', brushIntervalFactor: 1, chartClickCoordinates: [235, 0], + columnSelectorSearch: 'p-value', fieldSelectorSearch: 'referer', fieldSelectorApplyAvailable: true, action: { @@ -69,6 +70,7 @@ export const kibanaLogsDataViewTestData: TestData = { logRate: 'Chart type:bar chart', impact: 'High', })), + columnSelectorPopover: ['Log rate', 'Doc count', 'p-value', 'Impact', 'Actions'], fieldSelectorPopover: [ 'agent.keyword', 'clientip', diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts index 6ec612e6a66b2..fe9904bce21dd 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis_anomaly_table.ts @@ -98,7 +98,7 @@ const testData: TestData[] = [ fieldValue: 'AAL', impact: 'High', logRate: 'Chart type:bar chart', - pValue: '8.96e-49', + pValue: '', }, ], }, diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 0f8f4a4d07d22..aeac2ce33f718 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -44,9 +44,10 @@ interface TestDataExpectedWithoutSampleProbability { fieldName: string; fieldValue: string; logRate: string; - pValue: string; + pValue?: string; impact: string; }>; + columnSelectorPopover: string[]; fieldSelectorPopover: string[]; prompt: 'empty' | 'change-point'; } @@ -63,6 +64,7 @@ export interface TestData { brushDeviationTargetTimestamp?: number; brushIntervalFactor: number; chartClickCoordinates: [number, number]; + columnSelectorSearch: string; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; query?: string; diff --git a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts index 1a33ede2e700c..8ef2f02d4eb03 100644 --- a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts +++ b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts @@ -184,9 +184,9 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr }); }, - async assertFieldFilterPopoverButtonExists(isOpen: boolean) { + async assertFilterPopoverButtonExists(selector: string, isOpen: boolean) { await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('aiopsFieldFilterButton'); + await testSubjects.existOrFail(selector); if (isOpen) { await testSubjects.existOrFail('aiopsFieldSelectorSearch'); @@ -196,11 +196,11 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr }); }, - async clickFieldFilterPopoverButton(expectPopoverToBeOpen: boolean) { - await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsFieldFilterButton'); + async clickFilterPopoverButton(selector: string, expectPopoverToBeOpen: boolean) { + await testSubjects.clickWhenNotDisabledWithoutRetry(selector); await retry.tryForTime(30 * 1000, async () => { - await this.assertFieldFilterPopoverButtonExists(expectPopoverToBeOpen); + await this.assertFilterPopoverButtonExists(selector, expectPopoverToBeOpen); }); }, @@ -246,11 +246,17 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr }); }, - async clickFieldFilterApplyButton() { + async clickFieldFilterApplyButton(selector: string) { await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsFieldFilterApplyButton'); await retry.tryForTime(30 * 1000, async () => { - await this.assertFieldFilterPopoverButtonExists(false); + await this.assertFilterPopoverButtonExists(selector, false); + }); + }, + + async clickFieldSelectorListItem(selector: string) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.click(selector); }); },