From 6df1c38fc31a72708b0d8f2a23edd76704e10729 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:47:55 +1100 Subject: [PATCH] [8.x] [ResponseOps][Flapping] Add Rule Specific Flapping Form to New Rule Form Page (#194516) (#195697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Flapping] Add Rule Specific Flapping Form to New Rule Form Page (#194516)](https://github.com/elastic/kibana/pull/194516) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> --- packages/kbn-alerting-types/index.ts | 1 + packages/kbn-alerting-types/rule_settings.ts | 46 ++ .../fetch_flapping_settings.test.ts | 44 ++ .../fetch_flapping_settings.ts | 21 + .../apis/fetch_flapping_settings/index.ts | 10 + ...ansform_flapping_settings_response.test.ts | 36 ++ .../transform_flapping_settings_response.ts | 29 ++ .../src/common/constants/rule_flapping.ts | 11 + .../use_fetch_flapping_settings.test.tsx | 106 +++++ .../hooks/use_fetch_flapping_settings.ts | 43 ++ .../src/rule_form/create_rule_form.tsx | 3 + .../src/rule_form/edit_rule_form.tsx | 3 + .../hooks/use_load_dependencies.test.tsx | 20 + .../rule_form/hooks/use_load_dependencies.ts | 18 + .../rule_definition/rule_definition.test.tsx | 133 ++++++ .../rule_definition/rule_definition.tsx | 62 ++- .../src/rule_form/translations.ts | 15 + .../src/rule_form/types.ts | 4 +- .../rule_settings_flapping_form.tsx | 318 +++++++++++++ .../rule_settings_flapping_message.tsx | 31 +- .../rule_settings_flapping_title_tooltip.tsx | 140 ++++++ .../plugins/alerting/common/rules_settings.ts | 50 +-- .../rules_settings_flapping_form_section.tsx | 1 + .../rules_settings_link.test.tsx | 12 +- .../rules_settings_modal.test.tsx | 18 +- .../rules_setting/rules_settings_modal.tsx | 6 +- .../hooks/use_get_flapping_settings.ts | 41 -- .../lib/rule_api/get_flapping_settings.ts | 28 -- .../sections/rule_form/rule_add.test.tsx | 4 +- .../sections/rule_form/rule_add.tsx | 2 +- .../sections/rule_form/rule_edit.test.tsx | 4 +- .../sections/rule_form/rule_edit.tsx | 2 +- .../sections/rule_form/rule_form.test.tsx | 4 +- .../sections/rule_form/rule_form.tsx | 9 +- .../rule_form_advanced_options.test.tsx | 8 +- .../rule_form/rule_form_advanced_options.tsx | 416 +----------------- .../public/common/constants/index.ts | 3 - 37 files changed, 1162 insertions(+), 540 deletions(-) create mode 100644 packages/kbn-alerting-types/rule_settings.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts index 0a930e6a9319c..b2288900a1248 100644 --- a/packages/kbn-alerting-types/index.ts +++ b/packages/kbn-alerting-types/index.ts @@ -18,4 +18,5 @@ export * from './r_rule_types'; export * from './rule_notify_when_type'; export * from './rule_type_types'; export * from './rule_types'; +export * from './rule_settings'; export * from './search_strategy_types'; diff --git a/packages/kbn-alerting-types/rule_settings.ts b/packages/kbn-alerting-types/rule_settings.ts new file mode 100644 index 0000000000000..b25ad201c2dc0 --- /dev/null +++ b/packages/kbn-alerting-types/rule_settings.ts @@ -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". + */ + +export interface RulesSettingsModificationMetadata { + createdBy: string | null; + updatedBy: string | null; + createdAt: string; + updatedAt: string; +} + +export interface RulesSettingsFlappingProperties { + enabled: boolean; + lookBackWindow: number; + statusChangeThreshold: number; +} + +export interface RuleSpecificFlappingProperties { + lookBackWindow: number; + statusChangeThreshold: number; +} + +export type RulesSettingsFlapping = RulesSettingsFlappingProperties & + RulesSettingsModificationMetadata; + +export interface RulesSettingsQueryDelayProperties { + delay: number; +} + +export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties & + RulesSettingsModificationMetadata; + +export interface RulesSettingsProperties { + flapping?: RulesSettingsFlappingProperties; + queryDelay?: RulesSettingsQueryDelayProperties; +} + +export interface RulesSettings { + flapping?: RulesSettingsFlapping; + queryDelay?: RulesSettingsQueryDelay; +} diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts new file mode 100644 index 0000000000000..d5feaa731335a --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/public/mocks'; +import { fetchFlappingSettings } from './fetch_flapping_settings'; + +const http = httpServiceMock.createStartContract(); + +describe('fetchFlappingSettings', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('should call fetch rule flapping API', async () => { + const now = new Date().toISOString(); + http.get.mockResolvedValue({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + + const result = await fetchFlappingSettings({ http }); + + expect(result).toEqual({ + createdBy: 'test', + updatedBy: 'test', + createdAt: now, + updatedAt: now, + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts new file mode 100644 index 0000000000000..6ad702ebc945e --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/fetch_flapping_settings.ts @@ -0,0 +1,21 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { AsApiContract } from '@kbn/actions-types'; +import { RulesSettingsFlapping } from '@kbn/alerting-types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { transformFlappingSettingsResponse } from './transform_flapping_settings_response'; + +export const fetchFlappingSettings = async ({ http }: { http: HttpSetup }) => { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` + ); + return transformFlappingSettingsResponse(res); +}; diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/index.ts new file mode 100644 index 0000000000000..68ff193255403 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/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", 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". + */ + +export * from './fetch_flapping_settings'; diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts new file mode 100644 index 0000000000000..e53d133f6838b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 { transformFlappingSettingsResponse } from './transform_flapping_settings_response'; + +describe('transformFlappingSettingsResponse', () => { + test('should transform flapping settings response', () => { + const now = new Date().toISOString(); + + const result = transformFlappingSettingsResponse({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + + expect(result).toEqual({ + createdBy: 'test', + updatedBy: 'test', + createdAt: now, + updatedAt: now, + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts new file mode 100644 index 0000000000000..a628829927a3b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_flapping_settings/transform_flapping_settings_response.ts @@ -0,0 +1,29 @@ +/* + * 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 { AsApiContract } from '@kbn/actions-types'; +import { RulesSettingsFlapping } from '@kbn/alerting-types'; + +export const transformFlappingSettingsResponse = ({ + look_back_window: lookBackWindow, + status_change_threshold: statusChangeThreshold, + created_at: createdAt, + created_by: createdBy, + updated_at: updatedAt, + updated_by: updatedBy, + ...rest +}: AsApiContract): RulesSettingsFlapping => ({ + ...rest, + lookBackWindow, + statusChangeThreshold, + createdAt, + createdBy, + updatedAt, + updatedBy, +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.ts new file mode 100644 index 0000000000000..49ea5a63b3fca --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/constants/rule_flapping.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", 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". + */ + +// Feature flag for frontend rule specific flapping in rule flyout +export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx new file mode 100644 index 0000000000000..10e1869b9e64c --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.test.tsx @@ -0,0 +1,106 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import { testQueryClientConfig } from '../test_utils/test_query_client_config'; +import { useFetchFlappingSettings } from './use_fetch_flapping_settings'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; + +const queryClient = new QueryClient(testQueryClientConfig); + +const wrapper: FunctionComponent> = ({ children }) => ( + {children} +); + +const http = httpServiceMock.createStartContract(); + +const now = new Date().toISOString(); + +describe('useFetchFlappingSettings', () => { + beforeEach(() => { + http.get.mockResolvedValue({ + created_by: 'test', + updated_by: 'test', + created_at: now, + updated_at: now, + enabled: true, + look_back_window: 20, + status_change_threshold: 20, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + queryClient.clear(); + }); + + test('should call fetchFlappingSettings with the correct parameters', async () => { + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: true }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(result.current.data).toEqual({ + createdAt: now, + createdBy: 'test', + updatedAt: now, + updatedBy: 'test', + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); + + test('should not call fetchFlappingSettings if enabled is false', async () => { + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: false }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(http.get).not.toHaveBeenCalled(); + }); + + test('should call onSuccess when the fetching was successful', async () => { + const onSuccessMock = jest.fn(); + const { result, waitFor } = renderHook( + () => useFetchFlappingSettings({ http, enabled: true, onSuccess: onSuccessMock }), + { + wrapper, + } + ); + + await waitFor(() => { + return expect(result.current.isInitialLoading).toEqual(false); + }); + + expect(onSuccessMock).toHaveBeenCalledWith({ + createdAt: now, + createdBy: 'test', + updatedAt: now, + updatedBy: 'test', + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 20, + }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts new file mode 100644 index 0000000000000..6b72c2fea734b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings.ts @@ -0,0 +1,43 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { HttpStart } from '@kbn/core-http-browser'; +import { RulesSettingsFlapping } from '@kbn/alerting-types/rule_settings'; +import { fetchFlappingSettings } from '../apis/fetch_flapping_settings'; + +interface UseFetchFlappingSettingsProps { + http: HttpStart; + enabled: boolean; + onSuccess?: (settings: RulesSettingsFlapping) => void; +} + +export const useFetchFlappingSettings = (props: UseFetchFlappingSettingsProps) => { + const { http, enabled, onSuccess } = props; + + const queryFn = () => { + return fetchFlappingSettings({ http }); + }; + + const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({ + queryKey: ['fetchFlappingSettings'], + queryFn, + onSuccess, + enabled, + refetchOnWindowFocus: false, + retry: false, + }); + + return { + isInitialLoading, + isLoading: isLoading || isFetching, + isError: isError || isLoadingError, + data, + }; +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx index 71aeb2bcaab77..fc96ae214a7a8 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx @@ -92,6 +92,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, + flappingSettings, } = useLoadDependencies({ http, toasts: notifications.toasts, @@ -117,6 +118,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { actions: newFormData.actions, notifyWhen: newFormData.notifyWhen, alertDelay: newFormData.alertDelay, + flapping: newFormData.flapping, }, }); }, @@ -173,6 +175,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { selectedRuleTypeModel: ruleTypeModel, selectedRuleType: ruleType, validConsumers, + flappingSettings, canShowConsumerSelection, showMustacheAutocompleteSwitch, multiConsumerSelection: getInitialMultiConsumer({ diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx index 5091444276873..6e92b94cc2e0d 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx @@ -69,6 +69,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { connectors, connectorTypes, aadTemplateFields, + flappingSettings, } = useLoadDependencies({ http, toasts: notifications.toasts, @@ -89,6 +90,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { actions: newFormData.actions, notifyWhen: newFormData.notifyWhen, alertDelay: newFormData.alertDelay, + flapping: newFormData.flapping, }, }); }, @@ -160,6 +162,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleType: ruleType, selectedRuleTypeModel: ruleTypeModel, + flappingSettings, showMustacheAutocompleteSwitch, }} > diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx index 263c9e2118056..9d2ce3b6f1211 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.test.tsx @@ -50,6 +50,10 @@ jest.mock('../utils/get_authorized_rule_types', () => ({ getAvailableRuleTypes: jest.fn(), })); +jest.mock('../../common/hooks/use_fetch_flapping_settings', () => ({ + useFetchFlappingSettings: jest.fn(), +})); + const { useLoadUiConfig } = jest.requireMock('../../common/hooks/use_load_ui_config'); const { useHealthCheck } = jest.requireMock('../../common/hooks/use_health_check'); const { useResolveRule } = jest.requireMock('../../common/hooks/use_resolve_rule'); @@ -60,6 +64,9 @@ const { useLoadRuleTypeAadTemplateField } = jest.requireMock( ); const { useLoadRuleTypesQuery } = jest.requireMock('../../common/hooks/use_load_rule_types_query'); const { getAvailableRuleTypes } = jest.requireMock('../utils/get_authorized_rule_types'); +const { useFetchFlappingSettings } = jest.requireMock( + '../../common/hooks/use_fetch_flapping_settings' +); const uiConfigMock = { isUsingSecurity: true, @@ -103,6 +110,15 @@ useResolveRule.mockReturnValue({ data: ruleMock, }); +useFetchFlappingSettings.mockReturnValue({ + isLoading: false, + isInitialLoading: false, + data: { + lookBackWindow: 20, + statusChangeThreshold: 20, + }, +}); + const indexThresholdRuleType = { enabledInLicense: true, recoveryActionGroup: { @@ -260,6 +276,10 @@ describe('useLoadDependencies', () => { uiConfig: uiConfigMock, healthCheckError: null, fetchedFormData: ruleMock, + flappingSettings: { + lookBackWindow: 20, + statusChangeThreshold: 20, + }, connectors: [mockConnector], connectorTypes: [mockConnectorType], aadTemplateFields: [mockAadTemplateField], diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts index da59e85a933a1..5e0c52b1089ba 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts @@ -22,6 +22,8 @@ import { } from '../../common/hooks'; import { getAvailableRuleTypes } from '../utils'; import { RuleTypeRegistryContract } from '../../common'; +import { useFetchFlappingSettings } from '../../common/hooks/use_fetch_flapping_settings'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; import { useLoadRuleTypeAadTemplateField } from '../../common/hooks/use_load_rule_type_aad_template_fields'; export interface UseLoadDependencies { @@ -81,6 +83,15 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { filteredRuleTypes, }); + const { + data: flappingSettings, + isLoading: isLoadingFlappingSettings, + isInitialLoading: isInitialLoadingFlappingSettings, + } = useFetchFlappingSettings({ + http, + enabled: IS_RULE_SPECIFIC_FLAPPING_ENABLED, + }); + const { data: connectors = [], isLoading: isLoadingConnectors, @@ -144,6 +155,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingUiConfig || isLoadingHealthCheck || isLoadingRuleTypes || + isLoadingFlappingSettings || isLoadingConnectors || isLoadingConnectorTypes || isLoadingAadtemplateFields @@ -156,6 +168,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingHealthCheck || isLoadingRule || isLoadingRuleTypes || + isLoadingFlappingSettings || isLoadingConnectors || isLoadingConnectorTypes || isLoadingAadtemplateFields @@ -166,6 +179,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isLoadingHealthCheck, isLoadingRule, isLoadingRuleTypes, + isLoadingFlappingSettings, isLoadingConnectors, isLoadingConnectorTypes, isLoadingAadtemplateFields, @@ -178,6 +192,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingUiConfig || isInitialLoadingHealthCheck || isInitialLoadingRuleTypes || + isInitialLoadingFlappingSettings || isInitialLoadingConnectors || isInitialLoadingConnectorTypes || isInitialLoadingAadTemplateField @@ -190,6 +205,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingHealthCheck || isInitialLoadingRule || isInitialLoadingRuleTypes || + isInitialLoadingFlappingSettings || isInitialLoadingConnectors || isInitialLoadingConnectorTypes || isInitialLoadingAadTemplateField @@ -200,6 +216,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { isInitialLoadingHealthCheck, isInitialLoadingRule, isInitialLoadingRuleTypes, + isInitialLoadingFlappingSettings, isInitialLoadingConnectors, isInitialLoadingConnectorTypes, isInitialLoadingAadTemplateField, @@ -213,6 +230,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { uiConfig, healthCheckError, fetchedFormData, + flappingSettings, connectors, connectorTypes, aadTemplateFields, diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx index 01f9f39e9d086..b91148c220844 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx @@ -19,12 +19,37 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { RuleDefinition } from './rule_definition'; import { RuleType } from '@kbn/alerting-types'; import { RuleTypeModel } from '../../common/types'; +import { RuleSettingsFlappingFormProps } from '../../rule_settings/rule_settings_flapping_form'; +import { ALERT_FLAPPING_DETECTION_TITLE } from '../translations'; +import userEvent from '@testing-library/user-event'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), useRuleFormDispatch: jest.fn(), })); +jest.mock('../../common/constants/rule_flapping', () => ({ + IS_RULE_SPECIFIC_FLAPPING_ENABLED: true, +})); + +jest.mock('../../rule_settings/rule_settings_flapping_form', () => ({ + RuleSettingsFlappingForm: (props: RuleSettingsFlappingFormProps) => ( +
+ +
+ ), +})); + const ruleType = { id: '.es-query', name: 'Test', @@ -73,6 +98,13 @@ const plugins = { dataViews: {} as DataViewsPublicPluginStart, unifiedSearch: {} as UnifiedSearchPublicPluginStart, docLinks: {} as DocLinksStart, + application: { + capabilities: { + rulesSettings: { + writeFlappingSettingsUI: true, + }, + }, + }, }; const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); @@ -279,4 +311,105 @@ describe('Rule Definition', () => { }, }); }); + + test('should render rule flapping settings correctly', () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + expect(screen.getByText(ALERT_FLAPPING_DETECTION_TITLE)).toBeInTheDocument(); + expect(screen.getByTestId('ruleSettingsFlappingForm')).toBeInTheDocument(); + }); + + test('should allow flapping to be changed', async () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render(); + + await userEvent.click(screen.getByText('onFlappingChange')); + expect(mockOnChange).toHaveBeenCalledWith({ + payload: { + property: 'flapping', + value: { + lookBackWindow: 15, + statusChangeThreshold: 15, + }, + }, + type: 'setRuleProperty', + }); + }); + + test('should open and close flapping popover when button icon is clicked', async () => { + useRuleFormState.mockReturnValue({ + plugins, + formData: { + id: 'test-id', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + }, + selectedRuleType: ruleType, + selectedRuleTypeModel: ruleModel, + canShowConsumerSelection: true, + validConsumers: ['logs', 'stackAlerts'], + }); + + render( + + + + ); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton')); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingTitleTooltipButton')); + + expect(screen.queryByTestId('ruleSettingsFlappingTooltipTitle')).not.toBeVisible(); + }); }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx index fe4812436144a..3b404edc5d029 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -25,6 +25,7 @@ import { useEuiTheme, COLOR_MODES_STANDARD, } from '@elastic/eui'; +import { RuleSpecificFlappingProperties } from '@kbn/alerting-types'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { @@ -39,6 +40,8 @@ import { ADVANCED_OPTIONS_TITLE, ALERT_DELAY_DESCRIPTION_TEXT, ALERT_DELAY_HELP_TEXT, + ALERT_FLAPPING_DETECTION_TITLE, + ALERT_FLAPPING_DETECTION_DESCRIPTION, } from '../translations'; import { RuleAlertDelay } from './rule_alert_delay'; import { RuleConsumerSelection } from './rule_consumer_selection'; @@ -46,6 +49,9 @@ import { RuleSchedule } from './rule_schedule'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; import { getAuthorizedConsumers } from '../utils'; +import { RuleSettingsFlappingTitleTooltip } from '../../rule_settings/rule_settings_flapping_title_tooltip'; +import { RuleSettingsFlappingForm } from '../../rule_settings/rule_settings_flapping_form'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../common/constants/rule_flapping'; export const RuleDefinition = () => { const { @@ -58,17 +64,26 @@ export const RuleDefinition = () => { selectedRuleTypeModel, validConsumers, canShowConsumerSelection = false, + flappingSettings, } = useRuleFormState(); const { colorMode } = useEuiTheme(); const dispatch = useRuleFormDispatch(); - const { charts, data, dataViews, unifiedSearch, docLinks } = plugins; + const { charts, data, dataViews, unifiedSearch, docLinks, application } = plugins; - const { params, schedule, notifyWhen } = formData; + const { + capabilities: { rulesSettings }, + } = application; + + const { writeFlappingSettingsUI } = rulesSettings || {}; + + const { params, schedule, notifyWhen, flapping } = formData; const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false); + const [isFlappingPopoverOpen, setIsFlappingPopoverOpen] = useState(false); + const authorizedConsumers = useMemo(() => { if (!validConsumers?.length) { return []; @@ -143,6 +158,19 @@ export const RuleDefinition = () => { [dispatch] ); + const onSetFlapping = useCallback( + (value: RuleSpecificFlappingProperties | null) => { + dispatch({ + type: 'setRuleProperty', + payload: { + property: 'flapping', + value, + }, + }); + }, + [dispatch] + ); + return ( @@ -243,7 +271,10 @@ export const RuleDefinition = () => { { + setIsAdvancedOptionsVisible(isOpen); + setIsFlappingPopoverOpen(false); + }} initialIsOpen={isAdvancedOptionsVisible} buttonProps={{ 'data-test-subj': 'advancedOptionsAccordionButton', @@ -274,6 +305,31 @@ export const RuleDefinition = () => { > + {IS_RULE_SPECIFIC_FLAPPING_ENABLED && ( + {ALERT_FLAPPING_DETECTION_TITLE}} + description={ + +

+ {ALERT_FLAPPING_DETECTION_DESCRIPTION} + +

+
+ } + > + +
+ )}
diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index e7b060dce9831..20e87c66f10f4 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -85,6 +85,21 @@ export const ALERT_DELAY_TITLE_PREFIX = i18n.translate( } ); +export const ALERT_FLAPPING_DETECTION_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionTitle', + { + defaultMessage: 'Alert flapping detection', + } +); + +export const ALERT_FLAPPING_DETECTION_DESCRIPTION = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertFlappingDetectionDescription', + { + defaultMessage: + 'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts', + } +); + export const SCHEDULE_TITLE_PREFIX = i18n.translate( 'alertsUIShared.ruleForm.ruleSchedule.scheduleTitlePrefix', { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts index ac81f45de19e6..d33c74da528db 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts @@ -20,7 +20,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import { ActionType } from '@kbn/actions-types'; -import { ActionVariable } from '@kbn/alerting-types'; +import { ActionVariable, RulesSettingsFlapping } from '@kbn/alerting-types'; import { ActionConnector, ActionTypeRegistryContract, @@ -46,6 +46,7 @@ export interface RuleFormData { alertDelay?: Rule['alertDelay']; notifyWhen?: Rule['notifyWhen']; ruleTypeId?: Rule['ruleTypeId']; + flapping?: Rule['flapping']; } export interface RuleFormPlugins { @@ -83,6 +84,7 @@ export interface RuleFormState { minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; validConsumers?: RuleCreationValidConsumer[]; + flappingSettings?: RulesSettingsFlapping; } export type InitialRule = Partial & diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx new file mode 100644 index 0000000000000..99f64f0a3977f --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_form.tsx @@ -0,0 +1,318 @@ +/* + * 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, { useCallback, useMemo, useRef, useState } from 'react'; +import { + EuiBadge, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiPopover, + EuiSpacer, + EuiSplitPanel, + EuiSwitch, + EuiText, + EuiOutsideClickDetector, + useEuiTheme, + useIsWithinMinBreakpoint, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { RuleSpecificFlappingProperties, RulesSettingsFlapping } from '@kbn/alerting-types'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleSettingsFlappingMessage } from './rule_settings_flapping_message'; +import { RuleSettingsFlappingInputs } from './rule_settings_flapping_inputs'; + +const flappingLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.flappingLabel', { + defaultMessage: 'Flapping Detection', +}); + +const flappingOnLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.onLabel', { + defaultMessage: 'ON', +}); + +const flappingOffLabel = i18n.translate('alertsUIShared.ruleSettingsFlappingForm.offLabel', { + defaultMessage: 'OFF', +}); + +const flappingOverrideLabel = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.overrideLabel', + { + defaultMessage: 'Custom', + } +); + +const flappingOffContentRules = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentRules', + { + defaultMessage: 'Rules', + } +); + +const flappingOffContentSettings = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOffContentSettings', + { + defaultMessage: 'Settings', + } +); + +const flappingExternalLinkLabel = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingExternalLinkLabel', + { + defaultMessage: "What's this?", + } +); + +const flappingOverrideConfiguration = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingForm.flappingOverrideConfiguration', + { + defaultMessage: 'Customize Configuration', + } +); + +const clampFlappingValues = (flapping: RuleSpecificFlappingProperties) => { + return { + ...flapping, + statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold), + }; +}; + +export interface RuleSettingsFlappingFormProps { + flappingSettings?: RuleSpecificFlappingProperties | null; + spaceFlappingSettings?: RulesSettingsFlapping; + canWriteFlappingSettingsUI: boolean; + onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void; +} + +export const RuleSettingsFlappingForm = (props: RuleSettingsFlappingFormProps) => { + const { flappingSettings, spaceFlappingSettings, canWriteFlappingSettingsUI, onFlappingChange } = + props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const cachedFlappingSettings = useRef(); + + const isDesktop = useIsWithinMinBreakpoint('xl'); + + const { euiTheme } = useEuiTheme(); + + const onFlappingToggle = useCallback(() => { + if (!spaceFlappingSettings) { + return; + } + if (flappingSettings) { + cachedFlappingSettings.current = flappingSettings; + return onFlappingChange(null); + } + const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings; + onFlappingChange({ + lookBackWindow: initialFlappingSettings.lookBackWindow, + statusChangeThreshold: initialFlappingSettings.statusChangeThreshold, + }); + }, [spaceFlappingSettings, flappingSettings, onFlappingChange]); + + const internalOnFlappingChange = useCallback( + (flapping: RuleSpecificFlappingProperties) => { + const clampedValue = clampFlappingValues(flapping); + onFlappingChange(clampedValue); + cachedFlappingSettings.current = clampedValue; + }, + [onFlappingChange] + ); + + const onLookBackWindowChange = useCallback( + (value: number) => { + if (!flappingSettings) { + return; + } + internalOnFlappingChange({ + ...flappingSettings, + lookBackWindow: value, + }); + }, + [flappingSettings, internalOnFlappingChange] + ); + + const onStatusChangeThresholdChange = useCallback( + (value: number) => { + if (!flappingSettings) { + return; + } + internalOnFlappingChange({ + ...flappingSettings, + statusChangeThreshold: value, + }); + }, + [flappingSettings, internalOnFlappingChange] + ); + + const flappingOffTooltip = useMemo(() => { + if (!spaceFlappingSettings) { + return null; + } + const { enabled } = spaceFlappingSettings; + if (enabled) { + return null; + } + + if (canWriteFlappingSettingsUI) { + return ( + setIsPopoverOpen(false)}> + setIsPopoverOpen(!isPopoverOpen)} + /> + } + > + + {flappingOffContentRules}, + settings: {flappingOffContentSettings}, + }} + /> + + + + ); + } + // TODO: Add the external doc link here! + return ( + + {flappingExternalLinkLabel} + + ); + }, [canWriteFlappingSettingsUI, isPopoverOpen, spaceFlappingSettings]); + + const flappingFormHeader = useMemo(() => { + if (!spaceFlappingSettings) { + return null; + } + const { enabled } = spaceFlappingSettings; + + return ( + + + + + {flappingLabel} + + + {enabled ? flappingOnLabel : flappingOffLabel} + + {flappingSettings && enabled && ( + {flappingOverrideLabel} + )} + + + {enabled && ( + + )} + {flappingOffTooltip} + + + {flappingSettings && enabled && ( + <> + + + + )} + + ); + }, [ + isDesktop, + euiTheme, + spaceFlappingSettings, + flappingSettings, + flappingOffTooltip, + onFlappingToggle, + ]); + + const flappingFormBody = useMemo(() => { + if (!flappingSettings) { + return null; + } + if (!spaceFlappingSettings?.enabled) { + return null; + } + return ( + + + + ); + }, [ + flappingSettings, + spaceFlappingSettings, + onLookBackWindowChange, + onStatusChangeThresholdChange, + ]); + + const flappingFormMessage = useMemo(() => { + if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { + return null; + } + const settingsToUse = flappingSettings || spaceFlappingSettings; + return ( + + + + ); + }, [spaceFlappingSettings, flappingSettings, euiTheme]); + + return ( + + + + {flappingFormHeader} + {flappingFormBody} + + + {flappingFormMessage} + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx index b7c8681ef221b..d6d488e08f0c1 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_message.tsx @@ -37,21 +37,34 @@ export const flappingOffMessage = i18n.translate( export interface RuleSettingsFlappingMessageProps { lookBackWindow: number; statusChangeThreshold: number; + isUsingRuleSpecificFlapping: boolean; } export const RuleSettingsFlappingMessage = (props: RuleSettingsFlappingMessageProps) => { - const { lookBackWindow, statusChangeThreshold } = props; + const { lookBackWindow, statusChangeThreshold, isUsingRuleSpecificFlapping } = props; return ( - {getLookBackWindowLabelRuleRuns(lookBackWindow)}, - statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, - }} - /> + {!isUsingRuleSpecificFlapping && ( + {getLookBackWindowLabelRuleRuns(lookBackWindow)}, + statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, + }} + /> + )} + {isUsingRuleSpecificFlapping && ( + {getLookBackWindowLabelRuleRuns(lookBackWindow)}, + statusChangeThreshold: {getStatusChangeThresholdRuleRuns(statusChangeThreshold)}, + }} + /> + )} ); }; diff --git a/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx new file mode 100644 index 0000000000000..2a5cc4186013d --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip.tsx @@ -0,0 +1,140 @@ +/* + * 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 { + EuiButtonIcon, + EuiPopover, + EuiPopoverProps, + EuiPopoverTitle, + EuiSpacer, + EuiText, + EuiOutsideClickDetector, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const tooltipTitle = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.tooltipTitle', + { + defaultMessage: 'Alert flapping detection', + } +); + +const flappingTitlePopoverFlappingDetection = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverFlappingDetection', + { + defaultMessage: 'flapping detection', + } +); + +const flappingTitlePopoverAlertStatus = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverAlertStatus', + { + defaultMessage: 'alert status change threshold', + } +); + +const flappingTitlePopoverLookBack = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingTitlePopoverLookBack', + { + defaultMessage: 'rule run look back window', + } +); + +const flappingOffContentRules = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentRules', + { + defaultMessage: 'Rules', + } +); + +const flappingOffContentSettings = i18n.translate( + 'alertsUIShared.ruleSettingsFlappingTitleTooltip.flappingOffContentSettings', + { + defaultMessage: 'Settings', + } +); + +interface RuleSettingsFlappingTitleTooltipProps { + isOpen: boolean; + setIsPopoverOpen: (isOpen: boolean) => void; + anchorPosition?: EuiPopoverProps['anchorPosition']; +} + +export const RuleSettingsFlappingTitleTooltip = (props: RuleSettingsFlappingTitleTooltipProps) => { + const { isOpen, setIsPopoverOpen, anchorPosition = 'leftCenter' } = props; + + return ( + setIsPopoverOpen(false)}> + setIsPopoverOpen(!isOpen)} + /> + } + > + + {tooltipTitle} + + + {flappingTitlePopoverFlappingDetection}, + }} + /> + + + + {flappingTitlePopoverAlertStatus}, + }} + /> + + + + {flappingTitlePopoverLookBack}, + }} + /> + + + + {flappingOffContentRules}, + settings: {flappingOffContentSettings}, + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts index 2a4162ca2c5d3..6dcfd377eeb7c 100644 --- a/x-pack/plugins/alerting/common/rules_settings.ts +++ b/x-pack/plugins/alerting/common/rules_settings.ts @@ -5,38 +5,28 @@ * 2.0. */ -export interface RulesSettingsModificationMetadata { - createdBy: string | null; - updatedBy: string | null; - createdAt: string; - updatedAt: string; -} +import type { + RulesSettingsFlappingProperties, + RulesSettingsQueryDelayProperties, +} from '@kbn/alerting-types'; -export interface RulesSettingsFlappingProperties { - enabled: boolean; - lookBackWindow: number; - statusChangeThreshold: number; -} +export { + MIN_LOOK_BACK_WINDOW, + MAX_LOOK_BACK_WINDOW, + MIN_STATUS_CHANGE_THRESHOLD, + MAX_STATUS_CHANGE_THRESHOLD, +} from '@kbn/alerting-types/flapping/latest'; -export type RulesSettingsFlapping = RulesSettingsFlappingProperties & - RulesSettingsModificationMetadata; - -export interface RulesSettingsQueryDelayProperties { - delay: number; -} - -export type RulesSettingsQueryDelay = RulesSettingsQueryDelayProperties & - RulesSettingsModificationMetadata; - -export interface RulesSettingsProperties { - flapping?: RulesSettingsFlappingProperties; - queryDelay?: RulesSettingsQueryDelayProperties; -} - -export interface RulesSettings { - flapping?: RulesSettingsFlapping; - queryDelay?: RulesSettingsQueryDelay; -} +export type { + RulesSettingsModificationMetadata, + RulesSettingsFlappingProperties, + RulesSettingsQueryDelayProperties, + RuleSpecificFlappingProperties, + RulesSettingsFlapping, + RulesSettingsQueryDelay, + RulesSettingsProperties, + RulesSettings, +} from '@kbn/alerting-types'; export const MIN_QUERY_DELAY = 0; export const MAX_QUERY_DELAY = 60; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx index a78658044a192..1b38eede40e68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/flapping/rules_settings_flapping_form_section.tsx @@ -82,6 +82,7 @@ export const RulesSettingsFlappingFormSection = memo( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx index 8d32eb2c9940c..e1cdf5a8ee150 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_link.test.tsx @@ -14,12 +14,12 @@ import { coreMock } from '@kbn/core/public/mocks'; import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common'; import { RulesSettingsLink } from './rules_settings_link'; import { useKibana } from '../../../common/lib/kibana'; -import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings'; +import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings'; import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn(), +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn(), })); jest.mock('../../lib/rule_api/get_query_delay_settings', () => ({ getQueryDelaySettings: jest.fn(), @@ -38,8 +38,8 @@ const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); -const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction< - typeof getFlappingSettings +const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction< + typeof fetchFlappingSettings >; const getQueryDelaySettingsMock = getQueryDelaySettings as unknown as jest.MockedFunction< typeof getQueryDelaySettings @@ -88,7 +88,7 @@ describe('rules_settings_link', () => { readQueryDelaySettingsUI: true, }, }; - getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); + fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx index 592705b56984d..1dea8bdf88a6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.test.tsx @@ -15,14 +15,14 @@ import { IToasts } from '@kbn/core/public'; import { RulesSettingsFlapping, RulesSettingsQueryDelay } from '@kbn/alerting-plugin/common'; import { RulesSettingsModal, RulesSettingsModalProps } from './rules_settings_modal'; import { useKibana } from '../../../common/lib/kibana'; -import { getFlappingSettings } from '../../lib/rule_api/get_flapping_settings'; +import { fetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings'; import { updateFlappingSettings } from '../../lib/rule_api/update_flapping_settings'; import { getQueryDelaySettings } from '../../lib/rule_api/get_query_delay_settings'; import { updateQueryDelaySettings } from '../../lib/rule_api/update_query_delay_settings'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn(), +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn(), })); jest.mock('../../lib/rule_api/update_flapping_settings', () => ({ updateFlappingSettings: jest.fn(), @@ -47,8 +47,8 @@ const useKibanaMock = useKibana as jest.Mocked; const mocks = coreMock.createSetup(); -const getFlappingSettingsMock = getFlappingSettings as unknown as jest.MockedFunction< - typeof getFlappingSettings +const fetchFlappingSettingsMock = fetchFlappingSettings as unknown as jest.MockedFunction< + typeof fetchFlappingSettings >; const updateFlappingSettingsMock = updateFlappingSettings as unknown as jest.MockedFunction< typeof updateFlappingSettings @@ -142,7 +142,7 @@ describe('rules_settings_modal', () => { useKibanaMock().services.isServerless = true; - getFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); + fetchFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); updateFlappingSettingsMock.mockResolvedValue(mockFlappingSetting); getQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); updateQueryDelaySettingsMock.mockResolvedValue(mockQueryDelaySetting); @@ -156,7 +156,7 @@ describe('rules_settings_modal', () => { test('renders flapping settings correctly', async () => { const result = render(); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); await waitForModalLoad(); expect( result.getByTestId('rulesSettingsFlappingEnableSwitch').getAttribute('aria-checked') @@ -204,7 +204,7 @@ describe('rules_settings_modal', () => { test('reset flapping settings to initial state on cancel without triggering another server reload', async () => { const result = render(); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); await waitForModalLoad(); @@ -228,7 +228,7 @@ describe('rules_settings_modal', () => { expect(lookBackWindowInput.getAttribute('value')).toBe('10'); expect(statusChangeThresholdInput.getAttribute('value')).toBe('10'); - expect(getFlappingSettingsMock).toHaveBeenCalledTimes(1); + expect(fetchFlappingSettingsMock).toHaveBeenCalledTimes(1); expect(getQueryDelaySettingsMock).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx index 4431f05975906..09828e067369b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/rules_setting/rules_settings_modal.tsx @@ -26,8 +26,8 @@ import { EuiSpacer, EuiEmptyPrompt, } from '@elastic/eui'; +import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings'; import { useKibana } from '../../../common/lib/kibana'; -import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings'; import { RulesSettingsFlappingSection } from './flapping/rules_settings_flapping_section'; import { RulesSettingsQueryDelaySection } from './query_delay/rules_settings_query_delay_section'; import { useGetQueryDelaySettings } from '../../hooks/use_get_query_delay_settings'; @@ -93,6 +93,7 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { const { application: { capabilities }, isServerless, + http, } = useKibana().services; const { rulesSettings: { @@ -109,7 +110,8 @@ export const RulesSettingsModal = memo((props: RulesSettingsModalProps) => { const [queryDelaySettings, hasQueryDelayChanged, setQueryDelaySettings, resetQueryDelaySettings] = useResettableState(); - const { isLoading: isFlappingLoading, isError: hasFlappingError } = useGetFlappingSettings({ + const { isLoading: isFlappingLoading, isError: hasFlappingError } = useFetchFlappingSettings({ + http, enabled: isVisible, onSuccess: (fetchedSettings) => { if (!flappingSettings) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts deleted file mode 100644 index 26b9fdcaeb1c2..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_get_flapping_settings.ts +++ /dev/null @@ -1,41 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useQuery } from '@tanstack/react-query'; -import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; -import { useKibana } from '../../common/lib/kibana'; -import { getFlappingSettings } from '../lib/rule_api/get_flapping_settings'; - -interface UseGetFlappingSettingsProps { - enabled: boolean; - onSuccess?: (settings: RulesSettingsFlapping) => void; -} - -export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => { - const { enabled, onSuccess } = props; - const { http } = useKibana().services; - - const queryFn = () => { - return getFlappingSettings({ http }); - }; - - const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({ - queryKey: ['getFlappingSettings'], - queryFn, - onSuccess, - enabled, - refetchOnWindowFocus: false, - retry: false, - }); - - return { - isInitialLoading, - isLoading: isLoading || isFetching, - isError: isError || isLoadingError, - data, - }; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts deleted file mode 100644 index 931b1037ef729..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_flapping_settings.ts +++ /dev/null @@ -1,28 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HttpSetup } from '@kbn/core/public'; -import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; -import { RulesSettingsFlapping } from '@kbn/alerting-plugin/common'; -import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; - -const rewriteBodyRes: RewriteRequestCase = ({ - look_back_window: lookBackWindow, - status_change_threshold: statusChangeThreshold, - ...rest -}: any) => ({ - ...rest, - lookBackWindow, - statusChangeThreshold, -}); - -export const getFlappingSettings = async ({ http }: { http: HttpSetup }) => { - const res = await http.get>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rules/settings/_flapping` - ); - return rewriteBodyRes(res); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index af8bda5704b0f..c7b2876d83d84 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -67,8 +67,8 @@ jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 8657248a29df3..ccdca1bd1250d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -14,6 +14,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { createRule, CreateRuleBody } from '@kbn/alerts-ui-shared/src/common/apis/create_rule'; import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { Rule, RuleTypeParams, @@ -37,7 +38,6 @@ import { hasShowActionsCapability } from '../../lib/capabilities'; import RuleAddFooter from './rule_add_footer'; import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants'; import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed'; import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx index 331b10505a5d7..243236d7f6b93 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx @@ -63,8 +63,8 @@ jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index 72eab243ad0c8..a24fd0eec2eb1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -30,7 +30,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { updateRule } from '@kbn/alerts-ui-shared/src/common/apis/update_rule'; import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { Rule, RuleFlyoutCloseReason, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index 38ee1c73ac40b..17bdcc92997ca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -71,8 +71,8 @@ jest.mock('../../lib/capabilities', () => ({ hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); -jest.mock('../../lib/rule_api/get_flapping_settings', () => ({ - getFlappingSettings: jest.fn().mockResolvedValue({ +jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ + fetchFlappingSettings: jest.fn().mockResolvedValue({ lookBackWindow: 20, statusChangeThreshold: 20, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index c3f79c3458374..665dd93325c2b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -62,9 +62,11 @@ import { isActionGroupDisabledForActionTypeId, RuleActionAlertsFilterProperty, RuleActionKey, + Flapping, } from '@kbn/alerting-plugin/common'; import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; import { RuleReducerAction, InitialRule } from './rule_reducer'; import { RuleTypeModel, @@ -91,10 +93,7 @@ import { ruleTypeGroupCompare, ruleTypeUngroupedCompare, } from '../../lib/rule_type_compare'; -import { - IS_RULE_SPECIFIC_FLAPPING_ENABLED, - VIEW_LICENSE_OPTIONS_LINK, -} from '../../../common/constants'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; import { SectionLoading } from '../../components/section_loading'; import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection'; @@ -882,7 +881,7 @@ export const RuleForm = ({ alertDelay={alertDelay} flappingSettings={rule.flapping} onAlertDelayChange={onAlertDelayChange} - onFlappingChange={(flapping) => setRuleProperty('flapping', flapping)} + onFlappingChange={(flapping) => setRuleProperty('flapping', flapping as Flapping)} enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx index f6534f7451405..25c6de0225edb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx @@ -88,7 +88,7 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked(); expect(screen.queryByText('Custom')).not.toBeInTheDocument(); expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - 'An alert is flapping if it changes status at least 3 times in the last 10 rule runs.' + 'All rules (in this space) detect an alert is flapping when it changes status at least 3 times in the last 10 rule runs.' ); await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')); @@ -121,7 +121,7 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6'); expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4'); expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - 'An alert is flapping if it changes status at least 4 times in the last 6 rule runs.' + 'This rule detects an alert is flapping if it changes status at least 4 times in the last 6 rule runs.' ); await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')); @@ -157,6 +157,10 @@ describe('ruleFormAdvancedOptions', () => { expect(screen.queryByText('Custom')).not.toBeInTheDocument(); expect(screen.queryByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeInTheDocument(); expect(screen.queryByTestId('ruleSettingsFlappingMessage')).not.toBeInTheDocument(); + + await userEvent.click(screen.getByTestId('ruleSettingsFlappingFormTooltipButton')); + + expect(screen.getByTestId('ruleSettingsFlappingFormTooltipContent')).toBeInTheDocument(); }); test('should allow for flapping inputs to be modified', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx index ca6e17451c1aa..00ad6186d58e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx @@ -5,36 +5,21 @@ * 2.0. */ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiPanel, - EuiSwitch, - EuiText, - useIsWithinMinBreakpoint, - useEuiTheme, - EuiHorizontalRule, - EuiSpacer, - EuiSplitPanel, EuiLoadingSpinner, - EuiLink, - EuiButtonIcon, - EuiPopover, - EuiPopoverTitle, - EuiOutsideClickDetector, } from '@elastic/eui'; -import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs'; -import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message'; -import { Rule } from '@kbn/alerts-ui-shared'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { Flapping } from '@kbn/alerting-plugin/common'; -import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings'; +import { RuleSpecificFlappingProperties } from '@kbn/alerting-types/rule_settings'; +import { RuleSettingsFlappingForm } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_form'; +import { RuleSettingsFlappingTitleTooltip } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip'; +import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings'; import { useKibana } from '../../../common/lib/kibana'; const alertDelayFormRowLabel = i18n.translate( @@ -66,45 +51,6 @@ const alertDelayAppendLabel = i18n.translate( } ); -const flappingLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingLabel', - { - defaultMessage: 'Flapping Detection', - } -); - -const flappingOnLabel = i18n.translate('xpack.triggersActionsUI.ruleFormAdvancedOptions.onLabel', { - defaultMessage: 'ON', -}); - -const flappingOffLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.offLabel', - { - defaultMessage: 'OFF', - } -); - -const flappingOverrideLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.overrideLabel', - { - defaultMessage: 'Custom', - } -); - -const flappingOverrideConfiguration = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOverrideConfiguration', - { - defaultMessage: 'Override Configuration', - } -); - -const flappingExternalLinkLabel = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingExternalLinkLabel', - { - defaultMessage: "What's this?", - } -); - const flappingFormRowLabel = i18n.translate( 'xpack.triggersActionsUI.sections.ruleForm.flappingLabel', { @@ -112,58 +58,13 @@ const flappingFormRowLabel = i18n.translate( } ); -const flappingOffContentRules = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentRules', - { - defaultMessage: 'Rules', - } -); - -const flappingOffContentSettings = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOffContentSettings', - { - defaultMessage: 'Settings', - } -); - -const flappingTitlePopoverFlappingDetection = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverFlappingDetection', - { - defaultMessage: 'flapping detection', - } -); - -const flappingTitlePopoverAlertStatus = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverAlertStatus', - { - defaultMessage: 'alert status change threshold', - } -); - -const flappingTitlePopoverLookBack = i18n.translate( - 'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingTitlePopoverLookBack', - { - defaultMessage: 'rule run look back window', - } -); - -const clampFlappingValues = (flapping: Rule['flapping']) => { - if (!flapping) { - return; - } - return { - ...flapping, - statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold), - }; -}; - const INTEGER_REGEX = /^[1-9][0-9]*$/; export interface RuleFormAdvancedOptionsProps { alertDelay?: number; - flappingSettings?: Flapping | null; + flappingSettings?: RuleSpecificFlappingProperties | null; onAlertDelayChange: (value: string) => void; - onFlappingChange: (value: Flapping | null) => void; + onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void; enabledFlapping?: boolean; } @@ -180,20 +81,15 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => application: { capabilities: { rulesSettings }, }, + http, } = useKibana().services; - const { writeFlappingSettingsUI = false } = rulesSettings || {}; + const { writeFlappingSettingsUI } = rulesSettings || {}; - const [isFlappingOffPopoverOpen, setIsFlappingOffPopoverOpen] = useState(false); const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState(false); - const cachedFlappingSettings = useRef(); - - const isDesktop = useIsWithinMinBreakpoint('xl'); - - const { euiTheme } = useEuiTheme(); - - const { data: spaceFlappingSettings, isInitialLoading } = useGetFlappingSettings({ + const { data: spaceFlappingSettings, isInitialLoading } = useFetchFlappingSettings({ + http, enabled: enabledFlapping, }); @@ -207,274 +103,6 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => [onAlertDelayChange] ); - const internalOnFlappingChange = useCallback( - (flapping: Flapping) => { - const clampedValue = clampFlappingValues(flapping); - if (!clampedValue) { - return; - } - onFlappingChange(clampedValue); - cachedFlappingSettings.current = clampedValue; - }, - [onFlappingChange] - ); - - const onLookBackWindowChange = useCallback( - (value: number) => { - if (!flappingSettings) { - return; - } - internalOnFlappingChange({ - ...flappingSettings, - lookBackWindow: value, - }); - }, - [flappingSettings, internalOnFlappingChange] - ); - - const onStatusChangeThresholdChange = useCallback( - (value: number) => { - if (!flappingSettings) { - return; - } - internalOnFlappingChange({ - ...flappingSettings, - statusChangeThreshold: value, - }); - }, - [flappingSettings, internalOnFlappingChange] - ); - - const onFlappingToggle = useCallback(() => { - if (!spaceFlappingSettings) { - return; - } - if (flappingSettings) { - cachedFlappingSettings.current = flappingSettings; - return onFlappingChange(null); - } - const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings; - onFlappingChange({ - lookBackWindow: initialFlappingSettings.lookBackWindow, - statusChangeThreshold: initialFlappingSettings.statusChangeThreshold, - }); - }, [spaceFlappingSettings, flappingSettings, onFlappingChange]); - - const flappingTitleTooltip = useMemo(() => { - return ( - setIsFlappingTitlePopoverOpen(false)}> - setIsFlappingTitlePopoverOpen(!isFlappingTitlePopoverOpen)} - /> - } - > - Alert flapping detection - - {flappingTitlePopoverFlappingDetection}, - }} - /> - - - - {flappingTitlePopoverAlertStatus}, - }} - /> - - - - {flappingTitlePopoverLookBack}, - }} - /> - - - - {flappingOffContentRules}, - settings: {flappingOffContentSettings}, - }} - /> - - - - ); - }, [isFlappingTitlePopoverOpen]); - - const flappingOffTooltip = useMemo(() => { - if (!spaceFlappingSettings) { - return null; - } - const { enabled } = spaceFlappingSettings; - if (enabled) { - return null; - } - - if (writeFlappingSettingsUI) { - return ( - setIsFlappingOffPopoverOpen(false)}> - setIsFlappingOffPopoverOpen(!isFlappingOffPopoverOpen)} - /> - } - > - - {flappingOffContentRules}, - settings: {flappingOffContentSettings}, - }} - /> - - - - ); - } - // TODO: Add the external doc link here! - return ( - - {flappingExternalLinkLabel} - - ); - }, [writeFlappingSettingsUI, isFlappingOffPopoverOpen, spaceFlappingSettings]); - - const flappingFormHeader = useMemo(() => { - if (!spaceFlappingSettings) { - return null; - } - const { enabled } = spaceFlappingSettings; - - return ( - - - - - {flappingLabel} - - - {enabled ? flappingOnLabel : flappingOffLabel} - - {flappingSettings && enabled && ( - {flappingOverrideLabel} - )} - - - {enabled && ( - - )} - {flappingOffTooltip} - - - {flappingSettings && enabled && ( - <> - - - - )} - - ); - }, [ - isDesktop, - euiTheme, - spaceFlappingSettings, - flappingSettings, - flappingOffTooltip, - onFlappingToggle, - ]); - - const flappingFormBody = useMemo(() => { - if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { - return null; - } - if (!flappingSettings) { - return null; - } - return ( - - - - ); - }, [ - flappingSettings, - spaceFlappingSettings, - onLookBackWindowChange, - onStatusChangeThresholdChange, - ]); - - const flappingFormMessage = useMemo(() => { - if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) { - return null; - } - const settingsToUse = flappingSettings || spaceFlappingSettings; - return ( - - - - ); - }, [spaceFlappingSettings, flappingSettings, euiTheme]); - return ( @@ -512,21 +140,23 @@ export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => label={ {flappingFormRowLabel} - {flappingTitleTooltip} + + + } data-test-subj="alertFlappingFormRow" display="rowCompressed" > - - - - {flappingFormHeader} - {flappingFormBody} - - - {flappingFormMessage} - + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index 2d6548062eed9..ca87ba3522042 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -25,9 +25,6 @@ export { I18N_WEEKDAY_OPTIONS_DDD, } from '@kbn/alerts-ui-shared/src/common/constants/i18n_weekdays'; -// Feature flag for frontend rule specific flapping in rule flyout -export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false; - export const builtInComparators: { [key: string]: Comparator } = { [COMPARATORS.GREATER_THAN]: { text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isAboveLabel', {