From 7807e8d53da93ff5f1bc9cae186f85f1501e6b77 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 29 May 2025 11:49:20 -0400 Subject: [PATCH] [Incident Management] Investigation guide frontend (#217106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary 🌹 Resolves #213024. The frontend changes for [#216377](https://github.com/elastic/kibana/pull/216377). Depends on #216377 and https://github.com/elastic/kibana/pull/216292. ## Testing these changes 🌸 This adds frontend integration with the API changes we previously merged in #216377. There is a new editor in the Rule Create/Edit Detail view, below the pre-existing field for naming the rule. To test that this feature is working you should: - This is easiest to test if you have actual data that will trigger an alert in your cluster. If you need some fake data, you can use the nifty `data-forge` utility with a command like: ```shell node x-pack/scripts/data_forge.js --events-per-cycle 200 --lookback now-1h --ephemeral-project-ids 10 --dataset fake_stack --install-kibana-assets --kibana-url http://localhost:5601 --event-template bad ``` - Create a rule with an investigation guide specified. This is easy. Write some Markdown text into the editor and save the rule. My favorite rule for testing the feature is Custom Threshold, because it's easy to configure an alert that will fire. But this works for any rule. image - After you create your rule, it should fire at some point, ideally. Using the Observability -> Alerts view, drill into the Alert Details page. There, you should find a spiffy new tab called _Investigation Guide_. Confirm the contents on that tab are your markdown, properly rendered. image - Repeat step 1-2 as many times as you like with different rule types, if you desire. - Edit your rule, using the edit page or flyout. image - When you save the rule you should be able to refresh the alert details page and see the modified Investigation Guide reflected in the tab. --------- Co-authored-by: Panagiota Mitsopoulou Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 6b556d593fb8a906290f00cfb55268a6a8fbed46) --- .../rule_form/src/constants/index.ts | 2 + .../rule_definition/rule_definition.test.tsx | 6 +- .../src/rule_details/rule_details.test.tsx | 65 +++++- .../src/rule_details/rule_details.tsx | 64 +++++- .../rule_investigation_guide_editor.test.tsx | 46 ++++ .../rule_investigation_guide_editor.tsx | 68 ++++++ .../rule_form/src/translations.ts | 14 ++ .../rule_form/src/validation/validate_form.ts | 13 ++ .../transform_update_body/v1.test.ts | 200 ++++++++++++++++++ .../pages/alert_details/alert_details.tsx | 39 +++- .../alert_create_flyout.ts | 6 +- 11 files changed, 513 insertions(+), 10 deletions(-) create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.test.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_investigation_guide_editor.tsx create mode 100644 x-pack/platform/plugins/shared/alerting/server/routes/rule/apis/update/transforms/transform_update_body/v1.test.ts 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 7c66ce787612a..77c61702e4818 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: *');