diff --git a/src/platform/packages/shared/response-ops/rule_form/src/constants/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/constants/index.ts index d9359c0138acf..01a081cb95cd2 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/constants/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/constants/index.ts @@ -80,3 +80,5 @@ export enum RuleFormStepId { ACTIONS = 'rule-actions', DETAILS = 'rule-details', } + +export const MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH = 1000; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_definition.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_definition.test.tsx index 9ee39ca93f1be..2d274a02d5ff4 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_definition.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_definition.test.tsx @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import React, { type ReactNode } from 'react'; +import { fireEvent, render as rtlRender, screen } from '@testing-library/react'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -112,6 +112,8 @@ const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); const mockOnChange = jest.fn(); +const render = (toRender: ReactNode) => rtlRender(toRender, { wrapper: IntlProvider }); + describe('Rule Definition', () => { beforeEach(() => { useRuleFormDispatch.mockReturnValue(mockOnChange); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.test.tsx index 0348b89262147..5e2117ed54f46 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.test.tsx @@ -8,13 +8,12 @@ */ import React from 'react'; -import { fireEvent, render, screen, within } from '@testing-library/react'; +import { fireEvent, render as rtlRender, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { RuleDetails } from './rule_details'; -const mockOnChange = jest.fn(); - jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), useRuleFormDispatch: jest.fn(), @@ -22,6 +21,13 @@ jest.mock('../hooks', () => ({ const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); +const render = (toRender: React.ReactElement) => + rtlRender(toRender, { + wrapper: ({ children }) => {children}, + }); + +const mockOnChange = jest.fn(); + describe('RuleDetails', () => { beforeEach(() => { useRuleFormState.mockReturnValue({ @@ -88,4 +94,57 @@ describe('RuleDetails', () => { expect(screen.getByText('name is invalid')).toBeInTheDocument(); expect(screen.getByText('tags is invalid')).toBeInTheDocument(); }); + + test('should call dispatch with artifacts object when investigation guide is added', async () => { + useRuleFormState.mockReturnValue({ + plugins: { + contentManagement: {} as ContentManagementPublicStart, + }, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + ruleTypeId: '.es-query', + }, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + render(); + + const investigationGuideEditor = screen.getByTestId('investigationGuideEditor'); + const investigationGuideTextArea = screen.getByLabelText( + 'Add guidelines for addressing alerts created by this rule' + ); + expect(investigationGuideEditor).toBeInTheDocument(); + expect(investigationGuideEditor).toBeVisible(); + expect( + screen.getByPlaceholderText('Add guidelines for addressing alerts created by this rule') + ); + + fireEvent.change(investigationGuideTextArea, { + target: { + value: '# Example investigation guide', + }, + }); + + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setRuleProperty', + payload: { + property: 'artifacts', + value: { + investigation_guide: { + blob: '# Example investigation guide', + }, + }, + }, + }); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.tsx index 4c0be541f9f9e..8d0267acdda46 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_details.tsx @@ -15,11 +15,24 @@ import { EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, + EuiSpacer, + EuiIconTip, } from '@elastic/eui'; -import { RULE_NAME_INPUT_TITLE, RULE_TAG_INPUT_TITLE, RULE_TAG_PLACEHOLDER } from '../translations'; +import { i18n } from '@kbn/i18n'; + +import { + RULE_INVESTIGATION_GUIDE_LABEL, + RULE_NAME_INPUT_TITLE, + RULE_TAG_INPUT_TITLE, + RULE_TAG_PLACEHOLDER, +} from '../translations'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; import { OptionalFieldLabel } from '../optional_field_label'; +import { InvestigationGuideEditor } from './rule_investigation_guide_editor'; import { RuleDashboards } from './rule_dashboards'; +import { MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH } from '../constants'; + +export const RULE_DETAIL_MIN_ROW_WIDTH = 600; export const RuleDetails = () => { const { formData, baseErrors, plugins } = useRuleFormState(); @@ -72,6 +85,19 @@ export const RuleDetails = () => { } }, [dispatch, tags]); + const onSetArtifacts = useCallback( + (value: object) => { + dispatch({ + type: 'setRuleProperty', + payload: { + property: 'artifacts', + value: formData.artifacts ? { ...formData.artifacts, ...value } : value, + }, + }); + }, + [dispatch, formData.artifacts] + ); + return ( <> @@ -113,7 +139,43 @@ export const RuleDetails = () => { + + + {RULE_INVESTIGATION_GUIDE_LABEL} + + + {i18n.translate( + 'responseOpsRuleForm.ruleDetails.investigationGuideFormRow.toolTip.content', + { + defaultMessage: + 'These details will be included in a new tab on the alert details page for every alert triggered by this rule.', + } + )} +

+ } + /> +
+ + } + labelAppend={OptionalFieldLabel} + isInvalid={ + (formData.artifacts?.investigation_guide?.blob?.length ?? 0) > + MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH + } + > + +
{contentManagement && } + ); }; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.test.tsx new file mode 100644 index 0000000000000..8605a68a94093 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.test.tsx @@ -0,0 +1,46 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { render as rtlRender, screen } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { InvestigationGuideEditor } from './rule_investigation_guide_editor'; +import { userEvent } from '@testing-library/user-event'; + +const render = (toRender: any) => rtlRender(toRender, { wrapper: IntlProvider }); + +describe('RuleInvestigationGuide', () => { + it('should render the investigation guide when provided', () => { + const setRuleParams = jest.fn(); + render(); + const editorElement = screen.getByLabelText( + 'Add guidelines for addressing alerts created by this rule' + ); + expect(editorElement).toBeInTheDocument(); + }); + + it('should call setRuleParams when the value changes', async () => { + const setRuleParams = jest.fn(); + render(); + const editorElement = screen.getByLabelText( + 'Add guidelines for addressing alerts created by this rule' + ); + expect(editorElement).toBeInTheDocument(); + expect(editorElement).toHaveValue('# Markdown Summary'); + expect(setRuleParams).toHaveBeenCalledTimes(0); + + await userEvent.type(editorElement!, '!'); + + expect(setRuleParams).toHaveBeenCalled(); + expect(setRuleParams.mock.calls[0]).toHaveLength(1); + expect(setRuleParams.mock.calls[0][0]).toEqual({ + investigation_guide: { blob: '# Markdown Summary!' }, + }); + }); +}); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.tsx new file mode 100644 index 0000000000000..ad4bfcd9319df --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { EuiMarkdownAstNode, EuiMarkdownEditor, EuiMarkdownParseError } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback } from 'react'; +import { MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH } from '../constants'; + +interface Props { + setRuleParams: (v: { investigation_guide: { blob: string } }) => void; + value: string; +} + +export function InvestigationGuideEditor({ setRuleParams, value }: Props) { + const [errorMessages, setErrorMessages] = React.useState([]); + const onParse = useCallback( + (_: EuiMarkdownParseError | null, { ast }: { ast: EuiMarkdownAstNode }) => { + const length = ast.position?.end.offset ?? 0; + if (length > MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH) { + setErrorMessages([ + i18n.translate('responseOpsRuleForm.investigationGuide.editor.errorMessage', { + defaultMessage: + 'The Investigation Guide is too long. Please shorten it.\nCurrent length: {length}.\nMax length: {maxLength}.', + values: { length, maxLength: MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH }, + }), + ]); + } else if (errorMessages.length) { + setErrorMessages([]); + } + }, + [errorMessages] + ); + return ( + setRuleParams({ investigation_guide: { blob } })} + onParse={onParse} + errors={errorMessages} + height={200} + data-test-subj="investigationGuideEditor" + initialViewMode="editing" + /> + ); +} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/translations.ts b/src/platform/packages/shared/response-ops/rule_form/src/translations.ts index df3e1d8e8c5f3..b3eb526a973e5 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/translations.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/translations.ts @@ -227,6 +227,13 @@ export const RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT = i18n.translate( } ); +export const RULE_INVESTIGATION_GUIDE_TOO_LONG_TEXT = (length: number, maxLength: number) => + i18n.translate('responseOpsRuleForm.ruleForm.error.investigationGuideTooLongText', { + defaultMessage: + 'Investigation guide is too long. Current length: {length}. Max length: {maxLength}.', + values: { length, maxLength }, + }); + export const INTERVAL_MINIMUM_TEXT = (minimum: string) => i18n.translate('responseOpsRuleForm.ruleForm.error.belowMinimumText', { defaultMessage: 'Interval must be at least {minimum}.', @@ -297,6 +304,13 @@ export const RULE_TAG_PLACEHOLDER = i18n.translate( } ); +export const RULE_INVESTIGATION_GUIDE_LABEL = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleDetails.investigationGuide.editor.title', + { + defaultMessage: 'Investigation guide', + } +); + export const RULE_NAME_ARIA_LABEL_TEXT = i18n.translate( 'responseOpsRuleForm.ruleForm.rulePage.ruleNameAriaLabelText', { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/validation/validate_form.ts b/src/platform/packages/shared/response-ops/rule_form/src/validation/validate_form.ts index 6139d3504f918..6abf6828ac4fd 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/validation/validate_form.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/validation/validate_form.ts @@ -18,6 +18,7 @@ import { INTERVAL_REQUIRED_TEXT, INTERVAL_MINIMUM_TEXT, RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT, + RULE_INVESTIGATION_GUIDE_TOO_LONG_TEXT, } from '../translations'; import type { MinimumScheduleInterval, @@ -27,6 +28,7 @@ import type { RuleTypeModel, RuleUiAction, } from '../common'; +import { MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH } from '../constants'; export const validateAction = ({ action }: { action: RuleUiAction }): RuleFormActionsErrors => { const errors = { @@ -64,6 +66,7 @@ export function validateRuleBase({ actionConnectors: new Array(), alertDelay: new Array(), tags: new Array(), + artifacts: new Array(), }; if (!formData.name) { @@ -94,6 +97,16 @@ export function validateRuleBase({ errors.alertDelay.push(RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT); } + const investigationGuideLength = formData.artifacts?.investigation_guide?.blob.length ?? 0; + if (investigationGuideLength > MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH) { + errors.artifacts.push( + RULE_INVESTIGATION_GUIDE_TOO_LONG_TEXT( + investigationGuideLength, + MAX_ARTIFACTS_INVESTIGATION_GUIDE_LENGTH + ) + ); + } + return errors; } diff --git a/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/update/transforms/transform_update_body/v1.test.ts b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/update/transforms/transform_update_body/v1.test.ts new file mode 100644 index 0000000000000..7dd15a4c913ec --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/update/transforms/transform_update_body/v1.test.ts @@ -0,0 +1,200 @@ +/* + * 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 type { UpdateRuleRequestBodyV1 } from '../../../../../../../common/routes/rule/apis/update'; +import { transformUpdateBody } from './v1'; + +describe('transformUpdateBody', () => { + let baseUpdateBody: UpdateRuleRequestBodyV1<{}>; + let baseActions: UpdateRuleRequestBodyV1<{}>['actions']; + let baseSystemActions: UpdateRuleRequestBodyV1<{}>['actions']; + beforeEach(() => { + baseUpdateBody = { + name: 'Test Rule', + tags: ['tag1', 'tag2'], + throttle: '1m', + params: { param1: 'value1' }, + schedule: { interval: '1m' }, + notify_when: 'onActionGroupChange' as 'onActionGroupChange', + alert_delay: { active: 5 }, + flapping: { + look_back_window: 10, + status_change_threshold: 5, + }, + artifacts: { + dashboards: [{ id: 'dashboard1' }], + investigation_guide: { blob: 'guide-content' }, + }, + actions: [], + }; + baseActions = [ + { + group: 'default', + id: 'action1', + params: { key: 'value' }, + frequency: { + notify_when: 'onThrottleInterval', + throttle: '1m', + summary: true, + }, + alerts_filter: {}, + use_alert_data_for_template: true, + }, + ]; + baseSystemActions = [ + { + id: 'systemAction1', + params: { key: 'value' }, + }, + ]; + }); + + it('should transform the update body with all fields populated', () => { + const result = transformUpdateBody({ + updateBody: baseUpdateBody, + actions: baseActions, + systemActions: baseSystemActions, + }); + + expect(result).toEqual({ + name: 'Test Rule', + tags: ['tag1', 'tag2'], + throttle: '1m', + params: { param1: 'value1' }, + schedule: { interval: '1m' }, + notifyWhen: 'onActionGroupChange', + alertDelay: { active: 5 }, + flapping: { + lookBackWindow: 10, + statusChangeThreshold: 5, + }, + artifacts: { + dashboards: [{ id: 'dashboard1' }], + investigation_guide: { blob: 'guide-content' }, + }, + actions: [ + { + group: 'default', + id: 'action1', + params: { key: 'value' }, + frequency: { + throttle: '1m', + summary: true, + notifyWhen: 'onThrottleInterval', + }, + alertsFilter: {}, + useAlertDataForTemplate: true, + }, + ], + systemActions: [ + { + id: 'systemAction1', + params: { key: 'value' }, + }, + ], + }); + }); + + it('should handle missing optional fields', () => { + const result = transformUpdateBody({ + updateBody: { + ...baseUpdateBody, + name: 'Test Rule', + tags: ['tag1'], + params: { param1: 'value1' }, + schedule: { interval: '1m' }, + }, + actions: [], + systemActions: [], + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [], + "alertDelay": Object { + "active": 5, + }, + "artifacts": Object { + "dashboards": Array [ + Object { + "id": "dashboard1", + }, + ], + "investigation_guide": Object { + "blob": "guide-content", + }, + }, + "flapping": Object { + "lookBackWindow": 10, + "statusChangeThreshold": 5, + }, + "name": "Test Rule", + "notifyWhen": "onActionGroupChange", + "params": Object { + "param1": "value1", + }, + "schedule": Object { + "interval": "1m", + }, + "systemActions": Array [], + "tags": Array [ + "tag1", + ], + "throttle": "1m", + } + `); + }); + + it('should omit flapping when undefined', () => { + const result = transformUpdateBody({ + updateBody: { + ...baseUpdateBody, + name: 'Test Rule', + tags: ['tag1'], + params: { param1: 'value1' }, + schedule: { interval: '1m' }, + flapping: undefined, + }, + actions: [], + systemActions: [], + }); + + expect(result.flapping).not.toBeDefined(); + }); + + it('should handle missing frequency in actions', () => { + const result = transformUpdateBody({ + updateBody: { + ...baseUpdateBody, + name: 'Test Rule', + tags: ['tag1'], + params: { param1: 'value1' }, + schedule: { interval: '1m' }, + }, + actions: [ + { + group: 'default', + id: 'action1', + params: { key: 'value' }, + }, + ], + systemActions: baseSystemActions, + }); + + expect(result.actions).toMatchInlineSnapshot(` + Array [ + Object { + "group": "default", + "id": "action1", + "params": Object { + "key": "value", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx index 612197669380d..e5bb0c2333502 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -20,6 +20,7 @@ import { EuiTabbedContentTab, useEuiTheme, EuiFlexGroup, + EuiMarkdownFormat, EuiNotificationBadge, } from '@elastic/eui'; import { @@ -75,9 +76,14 @@ export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory const OVERVIEW_TAB_ID = 'overview'; const METADATA_TAB_ID = 'metadata'; const RELATED_ALERTS_TAB_ID = 'related_alerts'; +const INVESTIGATION_GUIDE_TAB_ID = 'investigation_guide'; const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId'; const RELATED_DASHBOARDS_TAB_ID = 'related_dashboards'; -type TabId = typeof OVERVIEW_TAB_ID | typeof METADATA_TAB_ID | typeof RELATED_ALERTS_TAB_ID; +type TabId = + | typeof OVERVIEW_TAB_ID + | typeof METADATA_TAB_ID + | typeof RELATED_ALERTS_TAB_ID + | typeof INVESTIGATION_GUIDE_TAB_ID; export const getPageTitle = (ruleCategory: string) => { return i18n.translate('xpack.observability.pages.alertDetails.pageTitle.title', { @@ -123,7 +129,13 @@ export function AlertDetails() { const searchParams = new URLSearchParams(search); const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY); - return urlTabId && [OVERVIEW_TAB_ID, METADATA_TAB_ID, RELATED_ALERTS_TAB_ID].includes(urlTabId) + return urlTabId && + [ + OVERVIEW_TAB_ID, + METADATA_TAB_ID, + RELATED_ALERTS_TAB_ID, + INVESTIGATION_GUIDE_TAB_ID, + ].includes(urlTabId) ? (urlTabId as TabId) : OVERVIEW_TAB_ID; }); @@ -317,6 +329,29 @@ export function AlertDetails() { 'data-test-subj': 'metadataTab', content: metadataTab, }, + { + id: 'investigation_guide', + name: ( + + ), + 'data-test-subj': 'investigationGuideTab', + disabled: !rule?.artifacts?.investigation_guide?.blob, + content: ( + <> + + + {rule?.artifacts?.investigation_guide?.blob ?? ''} + + + ), + }, { id: RELATED_ALERTS_TAB_ID, name: ( diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index e9e35657e242e..7496381f65214 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -411,13 +411,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); await firstDropdown.click(); await firstDropdown.type('kibana.alert.action_group'); - await find.clickByButtonText('kibana.alert.action_group'); + const filterKeyOptionsList = await find.byCssSelector('.euiComboBoxOptionsList'); + await find.clickByButtonText('kibana.alert.action_group', filterKeyOptionsList); const secondDropdown = await find.byCssSelector( '[data-test-subj="filter-0.1"] [data-test-subj="filterOperatorList"] [data-test-subj="comboBoxSearchInput"]' ); await secondDropdown.click(); await secondDropdown.type('exists'); - await find.clickByButtonText('exists'); + const filterOperationOptionsList = await find.byCssSelector('.euiComboBoxOptionsList'); + await find.clickByButtonText('exists', filterOperationOptionsList); await testSubjects.click('saveFilter'); await testSubjects.setValue('queryInput', '_id: *');