From 1ef801cb2bcad0a7aed52e575b41dd0a393636d1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 9 Jan 2025 16:36:02 -0600 Subject: [PATCH 01/62] Hide close button when show request is open --- .../response-ops/rule_form/src/rule_flyout/rule_flyout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 4262319d4bda3..467e5c5306530 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -56,6 +56,8 @@ export const RuleFlyout = ({ [isConnectorsScreenVisible, isShowRequestScreenVisible] ); + const hideCloseButton = useMemo(() => isShowRequestOpen, [isShowRequestOpen]); + return ( Date: Wed, 15 Jan 2025 09:58:16 -0600 Subject: [PATCH 02/62] Use EUI breakpoint variables for container breakpoints --- .../shared/response-ops/rule_form/src/rule_form.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss index 0564dc5847979..878b88f2ef5b1 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss @@ -10,7 +10,12 @@ container-type: inline-size; } -@container (max-width: 767px) { +/* EUI breakpoint mixins only support @media screen, we need to manually configure them for @container */ +$euiBreakpointMMax: map-get($euiBreakpoints, 'm') - 1px; +$euiBreakpointSMax: map-get($euiBreakpoints, 's') - 1px; +$euiBreakpointSMin: map-get($euiBreakpoints, 's'); + +@container (max-width: $euiBreakpointMMax) { .euiDescribedFormGroup { flex-direction: column; } @@ -34,7 +39,7 @@ display: none; } -@container (max-width: 767px) and (min-width: 575px) { +@container (max-width: $euiBreakpointMMax) and (min-width: $euiBreakpointSMin) { .hideForContainer--s { display: none; } @@ -44,7 +49,7 @@ } } -@container (max-width: 574px) { +@container (max-width: $euiBreakpointSMax) { .hideForContainer--xs { display: none; } From 4cb5b1d3a8190a08cfa1536bad7c5e2a96abc9a9 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 17 Jan 2025 11:35:12 -0600 Subject: [PATCH 03/62] Revert "Use EUI breakpoint variables for container breakpoints" This reverts commit 8d58b2875dda63a56ebf6fa6fca5348b532a4a1c. --- .../shared/response-ops/rule_form/src/rule_form.scss | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss index 878b88f2ef5b1..0564dc5847979 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss @@ -10,12 +10,7 @@ container-type: inline-size; } -/* EUI breakpoint mixins only support @media screen, we need to manually configure them for @container */ -$euiBreakpointMMax: map-get($euiBreakpoints, 'm') - 1px; -$euiBreakpointSMax: map-get($euiBreakpoints, 's') - 1px; -$euiBreakpointSMin: map-get($euiBreakpoints, 's'); - -@container (max-width: $euiBreakpointMMax) { +@container (max-width: 767px) { .euiDescribedFormGroup { flex-direction: column; } @@ -39,7 +34,7 @@ $euiBreakpointSMin: map-get($euiBreakpoints, 's'); display: none; } -@container (max-width: $euiBreakpointMMax) and (min-width: $euiBreakpointSMin) { +@container (max-width: 767px) and (min-width: 575px) { .hideForContainer--s { display: none; } @@ -49,7 +44,7 @@ $euiBreakpointSMin: map-get($euiBreakpoints, 's'); } } -@container (max-width: $euiBreakpointSMax) { +@container (max-width: 574px) { .hideForContainer--xs { display: none; } From e5b287aa344c1021293d19108df91dce458f218c Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 13 Jan 2025 13:17:39 -0600 Subject: [PATCH 04/62] Decouple RuleForm component from react router --- .../rule_form/src/create_rule_form.tsx | 12 +++++++++- .../rule_form/src/edit_rule_form.tsx | 12 +++++++++- .../response-ops/rule_form/src/rule_form.tsx | 22 +++++++++++++------ .../top_nav/app_menu_actions/get_alerts.tsx | 3 +++ .../sections/rule_form/rule_form_route.tsx | 2 ++ .../public/common/get_add_rule_flyout.tsx | 15 +++++-------- .../triggers_actions_ui/public/plugin.ts | 9 ++++---- 7 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 7b0a6f8fb3d6b..996cf09d8a3df 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -16,6 +16,7 @@ import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from './common/hooks'; import { RulePage } from './rule_page'; +import { RuleFlyout } from './rule_flyout'; import { RuleFormCircuitBreakerError, RuleFormErrorPromptWrapper, @@ -44,6 +45,7 @@ export interface CreateRuleFormProps { shouldUseRuleProducer?: boolean; canShowConsumerSelection?: boolean; showMustacheAutocompleteSwitch?: boolean; + isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; } @@ -60,6 +62,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { shouldUseRuleProducer = false, canShowConsumerSelection = true, showMustacheAutocompleteSwitch = false, + isFlyout, onCancel, onSubmit, } = props; @@ -156,6 +159,8 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { ); } + const RuleFormUIComponent = isFlyout ? RuleFlyout : RulePage; + return (
{ }), }} > - +
); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx index d1bdb8afab83e..3f0d06a86c057 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx @@ -14,6 +14,7 @@ import type { RuleFormData, RuleFormPlugins } from './types'; import { RuleFormStateProvider } from './rule_form_state'; import { useUpdateRule } from './common/hooks'; import { RulePage } from './rule_page'; +import { RuleFlyout } from './rule_flyout'; import { RuleFormHealthCheckError } from './rule_form_errors/rule_form_health_check_error'; import { useLoadDependencies } from './hooks/use_load_dependencies'; import { @@ -32,6 +33,7 @@ export interface EditRuleFormProps { plugins: RuleFormPlugins; showMustacheAutocompleteSwitch?: boolean; connectorFeatureId?: string; + isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; } @@ -44,6 +46,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { connectorFeatureId = 'alerting', onCancel, onSubmit, + isFlyout, } = props; const { http, notifications, docLinks, ruleTypeRegistry, application, ...deps } = plugins; const { toasts } = notifications; @@ -179,6 +182,8 @@ export const EditRuleForm = (props: EditRuleFormProps) => { return action; }); + const RuleFormUIComponent = isFlyout ? RuleFlyout : RulePage; + return (
{ showMustacheAutocompleteSwitch, }} > - +
); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 61ef0d775d505..fd8b7922fc947 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -10,7 +10,6 @@ import React, { useMemo } from 'react'; import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; import { CreateRuleForm } from './create_rule_form'; import { EditRuleForm } from './edit_rule_form'; import { @@ -25,16 +24,15 @@ const queryClient = new QueryClient(); export interface RuleFormProps { plugins: RuleFormPlugins; + id?: string; + ruleTypeId?: string; + isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; } export const RuleForm = (props: RuleFormProps) => { - const { plugins: _plugins, onCancel, onSubmit } = props; - const { id, ruleTypeId } = useParams<{ - id?: string; - ruleTypeId?: string; - }>(); + const { plugins: _plugins, onCancel, onSubmit, id, ruleTypeId, isFlyout } = props; const { http, @@ -71,7 +69,15 @@ export const RuleForm = (props: RuleFormProps) => { actionTypeRegistry, }; if (id) { - return ; + return ( + + ); } if (ruleTypeId) { return ( @@ -80,6 +86,7 @@ export const RuleForm = (props: RuleFormProps) => { plugins={plugins} onCancel={onCancel} onSubmit={onSubmit} + isFlyout={isFlyout} /> ); } @@ -112,6 +119,7 @@ export const RuleForm = (props: RuleFormProps) => { actionTypeRegistry, id, ruleTypeId, + isFlyout, onCancel, onSubmit, ]); diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index d6d8bb81bac09..ad7690d4ecc14 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; +import { RuleForm } from '@kbn/response-ops-rule-form'; import { i18n } from '@kbn/i18n'; import { AppMenuActionId, @@ -79,6 +80,8 @@ const CreateAlertFlyout: React.FC<{ [adHocDataViews] ); + return ; + return triggersActionsUi?.getAddRuleFlyout({ metadata: discoverMetadata, consumer: 'alerts', diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx index bfa4bccd49405..71ab959c369ec 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx @@ -75,6 +75,8 @@ export const RuleFormRoute = () => { actionTypeRegistry, ...startServices, }} + id={id} + ruleTypeId={ruleTypeId} onCancel={() => { if (returnApp && returnPath) { application.navigateToApp(returnApp, { path: returnPath }); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx index c6a79b4c6e82d..6a284ecb85060 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; +import { RuleForm, RuleFormPlugins } from '@kbn/response-ops-rule-form'; import { ConnectorProvider } from '../application/context/connector_context'; import { RuleAdd } from '../application/sections/rule_form'; import type { ConnectorServices, RuleAddProps, RuleTypeParams, RuleTypeMetaData } from '../types'; @@ -15,14 +16,8 @@ import { queryClient } from '../application/query_client'; export const getAddRuleFlyoutLazy = < Params extends RuleTypeParams = RuleTypeParams, MetaData extends RuleTypeMetaData = RuleTypeMetaData ->( - props: RuleAddProps & { connectorServices: ConnectorServices } -) => { - return ( - - - - - - ); +>(props: { + plugins: RuleFormPlugins; +}) => { + return ; }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index 33d8733b3b48b..ccbc052330b35 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -493,10 +493,11 @@ export class Plugin }, getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ - ...props, - actionTypeRegistry: this.actionTypeRegistry, - ruleTypeRegistry: this.ruleTypeRegistry, - connectorServices: this.connectorServices!, + plugins: { + ...props, + actionTypeRegistry: this.actionTypeRegistry, + ruleTypeRegistry: this.ruleTypeRegistry, + }, }); }, getEditRuleFlyout: (props) => { From 74f59b7eda1e45b28be9804ff1149d365c3354f6 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 13 Jan 2025 16:13:50 -0600 Subject: [PATCH 05/62] Initially replace flyout in Discover plugin --- .../shared/response-ops/rule_form/index.ts | 2 +- .../rule_form/src/create_rule_form.tsx | 31 ++-- .../response-ops/rule_form/src/rule_form.tsx | 65 +++++++- .../top_nav/app_menu_actions/get_alerts.tsx | 14 +- .../public/common/get_add_rule_flyout.tsx | 16 +- .../triggers_actions_ui/public/plugin.ts | 146 +++++++++--------- 6 files changed, 162 insertions(+), 112 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/index.ts b/src/platform/packages/shared/response-ops/rule_form/index.ts index 9a654f12bba7b..fece102d7d515 100644 --- a/src/platform/packages/shared/response-ops/rule_form/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/index.ts @@ -10,7 +10,7 @@ export * from './src/types'; export * from './src/rule_type_modal'; -export { RuleForm } from './src/rule_form'; +export { RuleForm, type RuleFormProps } from './src/rule_form'; export { fetchUiConfig, diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 996cf09d8a3df..7c5cf01f6a046 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -48,6 +48,7 @@ export interface CreateRuleFormProps { isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; + initialValues?: Partial; } export const CreateRuleForm = (props: CreateRuleFormProps) => { @@ -65,6 +66,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { isFlyout, onCancel, onSubmit, + initialValues = {}, } = props; const { http, docLinks, notifications, ruleTypeRegistry, ...deps } = plugins; @@ -165,20 +167,23 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
void; onSubmit?: (ruleId: string) => void; + consumer?: string; + connectorFeatureId?: string; + multiConsumerSelection?: RuleCreationValidConsumer | null; + hideInterval?: boolean; + validConsumers?: RuleCreationValidConsumer[]; + filteredRuleTypes?: string[]; + shouldUseRuleProducer?: boolean; + canShowConsumerSelection?: boolean; + showMustacheAutocompleteSwitch?: boolean; + initialValues?: Partial; } export const RuleForm = (props: RuleFormProps) => { - const { plugins: _plugins, onCancel, onSubmit, id, ruleTypeId, isFlyout } = props; + const { + plugins: _plugins, + onCancel, + onSubmit, + id, + ruleTypeId, + isFlyout, + consumer, + connectorFeatureId, + multiConsumerSelection, + hideInterval, + validConsumers, + filteredRuleTypes, + shouldUseRuleProducer, + canShowConsumerSelection, + showMustacheAutocompleteSwitch, + initialValues, + } = props; const { http, @@ -76,6 +104,8 @@ export const RuleForm = (props: RuleFormProps) => { onCancel={onCancel} onSubmit={onSubmit} isFlyout={isFlyout} + showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch} + connectorFeatureId={connectorFeatureId} /> ); } @@ -87,6 +117,16 @@ export const RuleForm = (props: RuleFormProps) => { onCancel={onCancel} onSubmit={onSubmit} isFlyout={isFlyout} + consumer={consumer} + connectorFeatureId={connectorFeatureId} + multiConsumerSelection={multiConsumerSelection} + hideInterval={hideInterval} + validConsumers={validConsumers} + filteredRuleTypes={filteredRuleTypes} + shouldUseRuleProducer={shouldUseRuleProducer} + canShowConsumerSelection={canShowConsumerSelection} + showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch} + initialValues={initialValues} /> ); } @@ -119,9 +159,18 @@ export const RuleForm = (props: RuleFormProps) => { actionTypeRegistry, id, ruleTypeId, - isFlyout, onCancel, onSubmit, + isFlyout, + showMustacheAutocompleteSwitch, + connectorFeatureId, + consumer, + multiConsumerSelection, + hideInterval, + validConsumers, + filteredRuleTypes, + shouldUseRuleProducer, + canShowConsumerSelection, ]); return ( diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index ad7690d4ecc14..d9ed376caa788 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -80,19 +80,19 @@ const CreateAlertFlyout: React.FC<{ [adHocDataViews] ); - return ; - return triggersActionsUi?.getAddRuleFlyout({ - metadata: discoverMetadata, + plugins: services, + // metadata: discoverMetadata, consumer: 'alerts', - onClose: (_, metadata) => { - onUpdateAdHocDataViews(metadata!.adHocDataViewList); + onCancel: (_, metadata) => { + // onUpdateAdHocDataViews(metadata!.adHocDataViewList); onFinishAction(); }, onSave: async (metadata) => { - onUpdateAdHocDataViews(metadata!.adHocDataViewList); + // onUpdateAdHocDataViews(metadata!.adHocDataViewList); + onFinishAction(); }, - canChangeTrigger: false, + // canChangeTrigger: false, ruleTypeId: ES_QUERY_ID, initialValues: { params: getParams() }, validConsumers: EsQueryValidConsumer, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx index 6a284ecb85060..bda565424ee5d 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx @@ -6,18 +6,14 @@ */ import React from 'react'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { RuleForm, RuleFormPlugins } from '@kbn/response-ops-rule-form'; -import { ConnectorProvider } from '../application/context/connector_context'; -import { RuleAdd } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleAddProps, RuleTypeParams, RuleTypeMetaData } from '../types'; -import { queryClient } from '../application/query_client'; +import { RuleForm, RuleFormProps } from '@kbn/response-ops-rule-form'; +import type { RuleTypeParams, RuleTypeMetaData } from '../types'; export const getAddRuleFlyoutLazy = < Params extends RuleTypeParams = RuleTypeParams, MetaData extends RuleTypeMetaData = RuleTypeMetaData ->(props: { - plugins: RuleFormPlugins; -}) => { - return ; +>( + props: RuleFormProps +) => { + return ; }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index ccbc052330b35..ac7f9bb69a0af 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -5,100 +5,100 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin as CorePlugin } from '@kbn/core/public'; +import { Plugin as CorePlugin, CoreSetup, CoreStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { ReactElement } from 'react'; -import { PluginInitializerContext } from '@kbn/core/public'; -import { FeaturesPluginStart } from '@kbn/features-plugin/public'; -import { KibanaFeature } from '@kbn/features-plugin/common'; -import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugin/public'; -import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { ChartsPluginStart } from '@kbn/charts-plugin/public'; -import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; +import { RuleAction } from '@kbn/alerting-plugin/common'; +import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public'; +import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry'; +import { ChartsPluginStart } from '@kbn/charts-plugin/public'; +import { CloudSetup } from '@kbn/cloud-plugin/public'; +import { PluginInitializerContext } from '@kbn/core/public'; +import { DashboardStart } from '@kbn/dashboard-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { triggersActionsRoute } from '@kbn/rule-data-utils'; -import { DashboardStart } from '@kbn/dashboard-plugin/public'; -import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; -import { ServerlessPluginStart } from '@kbn/serverless/public'; +import { KibanaFeature } from '@kbn/features-plugin/common'; +import { FeaturesPluginStart } from '@kbn/features-plugin/public'; import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; +import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { LensPublicStart } from '@kbn/lens-plugin/public'; -import { RuleAction } from '@kbn/alerting-plugin/common'; -import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry'; -import { CloudSetup } from '@kbn/cloud-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugin/public'; +import { triggersActionsRoute } from '@kbn/rule-data-utils'; +import { ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { ReactElement } from 'react'; +import type { AlertsSearchBarProps } from './application/sections/alerts_search_bar'; import { getAlertsTableDefaultAlertActionsLazy } from './common/get_alerts_table_default_row_actions'; import type { AlertActionsProps, RuleUiAction } from './types'; -import type { AlertsSearchBarProps } from './application/sections/alerts_search_bar'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; +import { getActionFormLazy } from './common/get_action_form'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; -import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; import { getAddRuleFlyoutLazy } from './common/get_add_rule_flyout'; -import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; +import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; +import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; import { getFieldBrowserLazy } from './common/get_field_browser'; +import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; -import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; +import { getRuleStatusPanelLazy } from './common/get_rule_status_panel'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; -import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; -import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRulesListLazy } from './common/get_rules_list'; -import { getActionFormLazy } from './common/get_action_form'; -import { getRuleStatusPanelLazy } from './common/get_rule_status_panel'; -import { ExperimentalFeaturesService } from './common/experimental_features_service'; -import { - ExperimentalFeatures, - parseExperimentalConfigValue, -} from '../common/experimental_features'; +import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge'; import { LazyLoadProps } from './types'; +import { RuleFormProps } from '@kbn/response-ops-rule-form'; +import { TriggersActionsUiConfigType } from '../common/types'; +import { AlertTableConfigRegistry } from './application/alert_table_config_registry'; +import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form'; +import { AlertSummaryWidgetProps } from './application/sections/alert_summary_widget'; +import { AlertSummaryWidgetDependencies } from './application/sections/alert_summary_widget/types'; +import type { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state'; +import type { FieldBrowserProps } from './application/sections/field_browser/types'; +import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel'; +import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal'; +import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID, PLUGIN_ID } from './common/constants'; +import { getAlertsSearchBarLazy } from './common/get_alerts_search_bar'; +import { getAlertsTableStateLazy } from './common/get_alerts_table_state'; +import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list'; +import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary'; +import { getRuleDefinitionLazy } from './common/get_rule_definition'; +import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal'; +import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link'; import type { ActionTypeModel, - RuleAddProps, - RuleEditProps, - RuleTypeModel, - RuleTypeParams, - RuleTypeMetaData, AlertsTableProps, + ConnectorServices, + CreateConnectorFlyoutProps, + EditConnectorFlyoutProps, + GlobalRuleEventLogListProps, + RuleDefinitionProps, + RuleEditProps, + RuleEventLogListOptions, + RuleEventLogListProps, RuleStatusDropdownProps, - RuleTagFilterProps, RuleStatusFilterProps, - RuleTagBadgeProps, RuleTagBadgeOptions, - RuleEventLogListProps, - RuleEventLogListOptions, - GlobalRuleEventLogListProps, - RulesListProps, + RuleTagBadgeProps, + RuleTagFilterProps, + RuleTypeMetaData, + RuleTypeModel, + RuleTypeParams, RulesListNotifyBadgePropsWithApi, - CreateConnectorFlyoutProps, - EditConnectorFlyoutProps, - ConnectorServices, - RuleDefinitionProps, + RulesListProps, } from './types'; -import { TriggersActionsUiConfigType } from '../common/types'; -import { PLUGIN_ID, CONNECTORS_PLUGIN_ID, ALERTS_PAGE_ID } from './common/constants'; -import type { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state'; -import { getAlertsTableStateLazy } from './common/get_alerts_table_state'; -import { getAlertsSearchBarLazy } from './common/get_alerts_search_bar'; -import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form'; -import type { FieldBrowserProps } from './application/sections/field_browser/types'; -import { getRuleDefinitionLazy } from './common/get_rule_definition'; -import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel'; -import { AlertSummaryWidgetProps } from './application/sections/alert_summary_widget'; -import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary'; -import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal'; -import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal'; -import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link'; -import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list'; -import { AlertTableConfigRegistry } from './application/alert_table_config_registry'; -import { AlertSummaryWidgetDependencies } from './application/sections/alert_summary_widget/types'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -121,12 +121,11 @@ export interface TriggersAndActionsUIPublicPluginStart { getEditConnectorFlyout: ( props: Omit ) => ReactElement; - getAddRuleFlyout: < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData - >( - props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> - ) => ReactElement>; + getAddRuleFlyout: ( + props: Omit & { + plugins: Omit; + } + ) => ReactElement; getEditRuleFlyout: < Params extends RuleTypeParams = RuleTypeParams, MetaData extends RuleTypeMetaData = RuleTypeMetaData @@ -493,8 +492,9 @@ export class Plugin }, getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ + ...props, plugins: { - ...props, + ...props.plugins, actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, }, From dd6e1677406a7f1d106aa3083c6eb63e0c9410d1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 13 Jan 2025 19:01:17 -0600 Subject: [PATCH 06/62] Replace add/edit rule flyouts with getRuleFormFlyout --- .../src/common/types/rule_types.ts | 1 + .../rule_form/src/create_rule_form.tsx | 8 +++- .../rule_form/src/edit_rule_form.tsx | 8 +++- .../rule_form/src/rule_flyout/rule_flyout.tsx | 5 ++- .../src/rule_flyout/rule_flyout_body.tsx | 22 +++++++--- .../response-ops/rule_form/src/rule_form.tsx | 19 +++++++-- .../response-ops/rule_form/src/types.ts | 8 +++- .../response-ops/rule_form/src/utils/index.ts | 1 + .../top_nav/app_menu_actions/get_alerts.tsx | 22 ++++------ .../accessors/get_app_menu.ts | 9 ++-- .../public/components/create_alert.tsx | 12 +++++- .../src/utils/storybook_decorator.stories.tsx | 2 +- .../public/alerts/configuration.tsx | 27 ++++++------ .../alerting/transform_alerting_flyout.tsx | 22 +++++----- .../ml/public/alerting/ml_alerting_flyout.tsx | 20 ++++----- .../public/common/get_edit_rule_flyout.tsx | 28 ------------- ...le_flyout.tsx => get_rule_form_flyout.tsx} | 9 ++-- .../common/validate_rule_form_plugins.ts | 41 +++++++++++++++++++ .../triggers_actions_ui/public/mocks.ts | 15 ++----- .../triggers_actions_ui/public/plugin.ts | 37 +++++------------ .../ui_components/alerting_flyout/index.tsx | 12 +++--- .../components/alert_flyout.tsx | 19 +++++---- .../inventory/components/alert_flyout.tsx | 13 +++--- .../log_threshold/components/alert_flyout.tsx | 15 ++++--- .../components/alert_flyout.tsx | 13 +++--- .../components/header_actions.test.tsx | 4 +- .../components/header_actions.tsx | 13 +++--- .../pages/rule_details/rule_details.tsx | 13 +++--- .../public/pages/rules/rules.tsx | 18 ++++---- .../observability/public/plugin.mock.tsx | 5 +-- .../kibana_react.storybook_decorator.tsx | 2 +- .../hooks/__storybook_mocks__/use_kibana.ts | 2 +- .../public/components/alerts_popover.tsx | 19 ++++++--- .../slo_details/components/header_control.tsx | 12 +++--- .../pages/slo_details/slo_details.test.tsx | 2 +- .../public/pages/slo_edit/slo_edit.test.tsx | 2 +- .../common/burn_rate_rule_flyout.tsx | 14 ++++--- .../common/edit_burn_rate_rule_flyout.tsx | 10 ++++- .../compact_view/slo_list_compact_view.tsx | 15 ++++--- .../slo/public/pages/slos/slos.test.tsx | 6 +-- .../kibana_react.storybook_decorator.tsx | 2 +- .../alerts/hooks/use_synthetics_rules.ts | 23 +++++++---- .../alerts/uptime_edit_alert_flyout.tsx | 20 +++++---- .../monitor/ml/ml_integerations.test.tsx | 2 +- .../alerts/uptime_alerts_flyout_wrapper.tsx | 9 ++-- 45 files changed, 332 insertions(+), 249 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx rename x-pack/platform/plugins/shared/triggers_actions_ui/public/common/{get_add_rule_flyout.tsx => get_rule_form_flyout.tsx} (63%) create mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts index 29eaf17552a2b..7453b0a384fb2 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts @@ -32,6 +32,7 @@ export type RuleTypeWithDescription = RuleType & { description?: export type RuleTypeIndexWithDescriptions = Map; export type RuleTypeParams = Record; +export type RuleTypeMetaData = Record; export interface RuleFormBaseErrors { name?: string[]; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 7c5cf01f6a046..8e083556358e9 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -11,7 +11,7 @@ import React, { useCallback } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils'; -import type { RuleFormData, RuleFormPlugins } from './types'; +import type { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from './common/hooks'; @@ -48,7 +48,9 @@ export interface CreateRuleFormProps { isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; + onChangeMetaData?: (metadata: RuleFormState['metadata']) => void; initialValues?: Partial; + initialMetadata?: RuleFormState['metadata']; } export const CreateRuleForm = (props: CreateRuleFormProps) => { @@ -66,6 +68,8 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { isFlyout, onCancel, onSubmit, + onChangeMetaData, + initialMetadata = {}, initialValues = {}, } = props; @@ -184,6 +188,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), ...initialValues, }, + metadata: initialMetadata, plugins, connectors, connectorTypes, @@ -213,6 +218,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { isSaving={isSaving} onCancel={onCancel} onSave={onSave} + onChangeMetaData={onChangeMetaData} />
diff --git a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx index 3f0d06a86c057..13e1ffc557461 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import type { RuleFormData, RuleFormPlugins } from './types'; +import type { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; import { RuleFormStateProvider } from './rule_form_state'; import { useUpdateRule } from './common/hooks'; import { RulePage } from './rule_page'; @@ -36,6 +36,8 @@ export interface EditRuleFormProps { isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; + onChangeMetaData?: (metadata: RuleFormState['metadata']) => void; + initialMetadata?: RuleFormState['metadata']; } export const EditRuleForm = (props: EditRuleFormProps) => { @@ -47,6 +49,8 @@ export const EditRuleForm = (props: EditRuleFormProps) => { onCancel, onSubmit, isFlyout, + onChangeMetaData, + initialMetadata = {}, } = props; const { http, notifications, docLinks, ruleTypeRegistry, application, ...deps } = plugins; const { toasts } = notifications; @@ -202,6 +206,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { actions: actionsWithFrequency, }, id, + metadata: initialMetadata, plugins, minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleType: ruleType, @@ -221,6 +226,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { isSaving={isSaving} onSave={onSave} onCancel={onCancel} + onChangeMetaData={onChangeMetaData} /> diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 467e5c5306530..38f0ae7db196b 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -9,7 +9,7 @@ import { EuiFlyout, EuiPortal } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; -import type { RuleFormData } from '../types'; +import type { RuleFormData, RuleFormState } from '../types'; import { RuleFormStepId } from '../constants'; import { RuleFlyoutBody } from './rule_flyout_body'; import { RuleFlyoutShowRequest } from './rule_flyout_show_request'; @@ -21,6 +21,7 @@ interface RuleFlyoutProps { isSaving?: boolean; onCancel?: () => void; onSave: (formData: RuleFormData) => void; + onChangeMetaData?: (metadata?: RuleFormState['metadata']) => void; } export const RuleFlyout = ({ @@ -28,6 +29,7 @@ export const RuleFlyout = ({ isEdit = false, isSaving = false, onCancel = () => {}, + onChangeMetaData = () => {}, }: RuleFlyoutProps) => { const [initialStep, setInitialStep] = useState(undefined); @@ -81,6 +83,7 @@ export const RuleFlyout = ({ isSaving={isSaving} onShowRequest={onOpenShowRequest} initialStep={initialStep} + onChangeMetaData={onChangeMetaData} /> )}
diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx index 62244c5629a98..d1be170e1c0c1 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx @@ -8,27 +8,28 @@ */ import { + EuiCallOut, EuiFlyoutBody, EuiFlyoutHeader, + EuiSpacer, EuiStepsHorizontal, EuiTitle, - EuiSpacer, - EuiCallOut, } from '@elastic/eui'; import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; -import React, { useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { RuleFormStepId } from '../constants'; import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; import { + DISABLED_ACTIONS_WARNING_TITLE, RULE_FLYOUT_HEADER_CREATE_TITLE, RULE_FLYOUT_HEADER_EDIT_TITLE, - DISABLED_ACTIONS_WARNING_TITLE, } from '../translations'; -import type { RuleFormData } from '../types'; +import type { RuleFormData, RuleFormState } from '../types'; import { hasRuleErrors } from '../validation'; import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; -import { RuleFormStepId } from '../constants'; interface RuleFlyoutBodyProps { isEdit?: boolean; @@ -36,6 +37,7 @@ interface RuleFlyoutBodyProps { onCancel: () => void; onSave: (formData: RuleFormData) => void; onShowRequest: () => void; + onChangeMetaData?: (metadata?: RuleFormState['metadata']) => void; initialStep?: RuleFormStepId; } @@ -46,6 +48,7 @@ export const RuleFlyoutBody = ({ onCancel, onSave, onShowRequest, + onChangeMetaData = () => {}, }: RuleFlyoutBodyProps) => { const { formData, @@ -56,8 +59,15 @@ export const RuleFlyoutBody = ({ paramsErrors = {}, actionsErrors = {}, actionsParamsErrors = {}, + metadata = {}, } = useRuleFormState(); + useEffect(() => { + if (!isEmpty(metadata)) { + onChangeMetaData(metadata); + } + }, [metadata, onChangeMetaData]); + const hasErrors = useMemo(() => { const hasBrokenConnectors = formData.actions.some((action) => { return !connectors.find((connector) => connector.id === action.id); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 8f85fa6a5abae..34eb1e1b19cd8 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -19,17 +19,18 @@ import { RULE_FORM_ROUTE_PARAMS_ERROR_TEXT, RULE_FORM_ROUTE_PARAMS_ERROR_TITLE, } from './translations'; -import { RuleFormData, RuleFormPlugins } from './types'; +import { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; const queryClient = new QueryClient(); -export interface RuleFormProps { +export interface RuleFormProps { plugins: RuleFormPlugins; id?: string; ruleTypeId?: string; isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; + onChangeMetaData?: (metadata: MetaData) => void; consumer?: string; connectorFeatureId?: string; multiConsumerSelection?: RuleCreationValidConsumer | null; @@ -40,13 +41,17 @@ export interface RuleFormProps { canShowConsumerSelection?: boolean; showMustacheAutocompleteSwitch?: boolean; initialValues?: Partial; + initialMetadata?: MetaData; } -export const RuleForm = (props: RuleFormProps) => { +export const RuleForm = ( + props: RuleFormProps +) => { const { plugins: _plugins, onCancel, onSubmit, + onChangeMetaData, id, ruleTypeId, isFlyout, @@ -60,6 +65,7 @@ export const RuleForm = (props: RuleFormProps) => { canShowConsumerSelection, showMustacheAutocompleteSwitch, initialValues, + initialMetadata, } = props; const { @@ -103,9 +109,11 @@ export const RuleForm = (props: RuleFormProps) => { plugins={plugins} onCancel={onCancel} onSubmit={onSubmit} + onChangeMetaData={onChangeMetaData} isFlyout={isFlyout} showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch} connectorFeatureId={connectorFeatureId} + initialMetadata={initialMetadata} /> ); } @@ -116,6 +124,7 @@ export const RuleForm = (props: RuleFormProps) => { plugins={plugins} onCancel={onCancel} onSubmit={onSubmit} + onChangeMetaData={onChangeMetaData} isFlyout={isFlyout} consumer={consumer} connectorFeatureId={connectorFeatureId} @@ -127,6 +136,7 @@ export const RuleForm = (props: RuleFormProps) => { canShowConsumerSelection={canShowConsumerSelection} showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch} initialValues={initialValues} + initialMetadata={initialMetadata} /> ); } @@ -161,9 +171,11 @@ export const RuleForm = (props: RuleFormProps) => { ruleTypeId, onCancel, onSubmit, + onChangeMetaData, isFlyout, showMustacheAutocompleteSwitch, connectorFeatureId, + initialMetadata, consumer, multiConsumerSelection, hideInterval, @@ -171,6 +183,7 @@ export const RuleForm = (props: RuleFormProps) => { filteredRuleTypes, shouldUseRuleProducer, canShowConsumerSelection, + initialValues, ]); return ( diff --git a/src/platform/packages/shared/response-ops/rule_form/src/types.ts b/src/platform/packages/shared/response-ops/rule_form/src/types.ts index 83f50891eabb5..5409c07b3a84d 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/types.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/types.ts @@ -31,6 +31,7 @@ import { RuleFormParamsErrors, RuleTypeModel, RuleTypeParams, + RuleTypeMetaData, RuleTypeRegistryContract, RuleTypeWithDescription, RuleUiAction, @@ -69,7 +70,10 @@ export interface RuleFormPlugins { actionTypeRegistry: ActionTypeRegistryContract; } -export interface RuleFormState { +export interface RuleFormState< + Params extends RuleTypeParams = RuleTypeParams, + MetaData = RuleTypeMetaData +> { id?: string; formData: RuleFormData; plugins: RuleFormPlugins; @@ -85,7 +89,7 @@ export interface RuleFormState { selectedRuleTypeModel: RuleTypeModel; multiConsumerSelection?: RuleCreationValidConsumer | null; showMustacheAutocompleteSwitch?: boolean; - metadata?: Record; + metadata?: MetaData; minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; validConsumers: RuleCreationValidConsumer[]; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts index 53c9aedda7545..15d1f575c71a8 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts @@ -18,3 +18,4 @@ export * from './has_fields_for_aad'; export * from './get_selected_action_group'; export * from './get_initial_consumer'; export * from './get_default_params'; +export * from './validate_rule_form_plugins'; diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index d9ed376caa788..405f9be1f715a 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -9,7 +9,6 @@ import React, { useCallback, useMemo } from 'react'; import type { DataView } from '@kbn/data-plugin/common'; -import { RuleForm } from '@kbn/response-ops-rule-form'; import { i18n } from '@kbn/i18n'; import { AppMenuActionId, @@ -80,25 +79,20 @@ const CreateAlertFlyout: React.FC<{ [adHocDataViews] ); - return triggersActionsUi?.getAddRuleFlyout({ + return triggersActionsUi?.getRuleFormFlyout({ plugins: services, - // metadata: discoverMetadata, + initialMetadata: discoverMetadata, consumer: 'alerts', - onCancel: (_, metadata) => { - // onUpdateAdHocDataViews(metadata!.adHocDataViewList); - onFinishAction(); - }, - onSave: async (metadata) => { - // onUpdateAdHocDataViews(metadata!.adHocDataViewList); - onFinishAction(); - }, - // canChangeTrigger: false, + onCancel: onFinishAction, + onSubmit: onFinishAction, + onChangeMetaData: (metadata: EsQueryAlertMetaData) => + onUpdateAdHocDataViews(metadata.adHocDataViewList), ruleTypeId: ES_QUERY_ID, initialValues: { params: getParams() }, validConsumers: EsQueryValidConsumer, - useRuleProducer: true, + shouldUseRuleProducer: true, // Default to the Logs consumer if it's available. This should fall back to Stack Alerts if it's not. - initialSelectedConsumer: AlertConsumers.LOGS, + multiConsumerSelection: AlertConsumers.LOGS, }); }; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts index 759765e93767f..902d94e8c02d4 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts @@ -74,7 +74,7 @@ const registerDatasetQualityLink = ( const registerCustomThresholdRuleAction = ( registry: AppMenuRegistry, - { data, triggersActionsUi }: ProfileProviderServices, + { data, triggersActionsUi, ...services }: ProfileProviderServices, { dataView }: AppMenuExtensionParams ) => { registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { @@ -91,10 +91,10 @@ const registerCustomThresholdRuleAction = ( const index = dataView?.toMinimalSpec(); const { filters, query } = data.query.getState(); - return triggersActionsUi.getAddRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ + plugins: { data, ...services }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - canChangeTrigger: false, initialValues: { params: { searchConfiguration: { @@ -104,7 +104,8 @@ const registerCustomThresholdRuleAction = ( }, }, }, - onClose: onFinishAction, + onCancel: onFinishAction, + onSubmit: onFinishAction, }); }, }, diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx index ab4b7c229933f..7c0e665c20b2e 100644 --- a/x-pack/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -7,16 +7,19 @@ import React, { useState, useCallback } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; import { AlertingExampleComponentParams } from '../application'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; export const CreateAlert = ({ - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, + triggersActionsUi: { getRuleFormFlyout: AddRuleFlyout }, }: Pick) => { const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); + const { services } = useKibana(); + const onCloseAlertFlyout = useCallback( () => setRuleFlyoutVisibility(false), [setRuleFlyoutVisibility] @@ -34,7 +37,12 @@ export const CreateAlert = ({ {ruleFlyoutVisible ? ( - + ) : null} diff --git a/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx b/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx index d6292803b42af..a898decae4b97 100644 --- a/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx +++ b/x-pack/platform/packages/shared/kbn-ai-assistant/src/utils/storybook_decorator.stories.tsx @@ -42,7 +42,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { ObservabilityAIAssistantMultipaneFlyoutContext, service: mockService, }, - triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, + triggersActionsUi: { getRuleFormFlyout: {}, getAddConnectorFlyout: {} }, }} > diff --git a/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx b/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx index a963f129fa965..db2bbb7110704 100644 --- a/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx +++ b/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { Fragment, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import React, { Fragment, useCallback, useMemo } from 'react'; import { CommonAlert } from '../../common/types/alerts'; import { Legacy } from '../legacy_shims'; import { hideBottomBar, showBottomBar } from '../lib/setup_mode'; @@ -25,6 +26,8 @@ export const AlertConfiguration: React.FC = (props: Props) => { const [isMuted, setIsMuted] = React.useState(alert.muteAll); const [isSaving, setIsSaving] = React.useState(false); + const { services } = useKibana(); + async function disableAlert() { setIsSaving(true); try { @@ -82,18 +85,18 @@ export const AlertConfiguration: React.FC = (props: Props) => { setIsSaving(false); } + const onClose = useCallback(() => { + setShowFlyout(false); + showBottomBar(); + }, []); const flyoutUi = useMemo( () => showFlyout && - Legacy.shims.triggersActionsUi.getEditRuleFlyout({ - initialRule: { - ...alert, - ruleTypeId: alert.alertTypeId, - }, - onClose: () => { - setShowFlyout(false); - showBottomBar(); - }, + Legacy.shims.triggersActionsUi.getRuleFormFlyout({ + plugins: services, + id: alert.id, + onSubmit: onClose, + onCancel: onClose, }), // eslint-disable-next-line react-hooks/exhaustive-deps [showFlyout] diff --git a/x-pack/platform/plugins/private/transform/public/alerting/transform_alerting_flyout.tsx b/x-pack/platform/plugins/private/transform/public/alerting/transform_alerting_flyout.tsx index e83ecfa606762..7b4fae43f194e 100644 --- a/x-pack/platform/plugins/private/transform/public/alerting/transform_alerting_flyout.tsx +++ b/x-pack/platform/plugins/private/transform/public/alerting/transform_alerting_flyout.tsx @@ -32,44 +32,42 @@ export const TransformAlertFlyout: FC = ({ onCloseFlyout, onSave, }) => { - const { triggersActionsUi } = useAppDependencies(); + const { triggersActionsUi, ...plugins } = useAppDependencies(); const AlertFlyout = useMemo(() => { if (!triggersActionsUi) return; const commonProps = { - onClose: () => { + plugins, + onCancel: () => { onCloseFlyout(); }, - onSave: async () => { + onSubmit: async () => { if (onSave) { onSave(); } + onCloseFlyout(); }, }; if (initialAlert) { - return triggersActionsUi.getEditRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ ...commonProps, - initialRule: { - ...initialAlert, - ruleTypeId: initialAlert.alertTypeId, - }, + id: initialAlert.id, }); } - return triggersActionsUi.getAddRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ ...commonProps, consumer: 'stackAlerts', - canChangeTrigger: false, ruleTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, - metadata: {}, + initialMetadata: {}, initialValues: { params: ruleParams!, }, }); // deps on id to avoid re-rendering on auto-refresh - }, [triggersActionsUi, initialAlert, ruleParams, onCloseFlyout, onSave]); + }, [triggersActionsUi, plugins, initialAlert, ruleParams, onCloseFlyout, onSave]); return <>{AlertFlyout}; }; diff --git a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx index d0d90f7b46745..a30a75ca606e1 100644 --- a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx @@ -38,39 +38,37 @@ export const MlAnomalyAlertFlyout: FC = ({ onSave, }) => { const { - services: { triggersActionsUi }, + services: { triggersActionsUi, ...services }, } = useMlKibana(); const AlertFlyout = useMemo(() => { if (!triggersActionsUi) return; const commonProps = { - onClose: () => { + plugins: services, + onCancel: () => { onCloseFlyout(); }, - onSave: async () => { + onSubmit: async () => { if (onSave) { onSave(); } + onCloseFlyout(); }, }; if (initialAlert) { - return triggersActionsUi.getEditRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ ...commonProps, - initialRule: { - ...initialAlert, - ruleTypeId: initialAlert.ruleTypeId ?? initialAlert.alertTypeId, - }, + id: initialAlert.id, }); } - return triggersActionsUi.getAddRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ ...commonProps, consumer: PLUGIN_ID, - canChangeTrigger: false, ruleTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION, - metadata: {}, + initialMetadata: {}, initialValues: { params: { jobSelection: { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx deleted file mode 100644 index f3fbccce267c5..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx +++ /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 React from 'react'; -import { QueryClientProvider } from '@tanstack/react-query'; -import { ConnectorProvider } from '../application/context/connector_context'; -import { RuleEdit } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleEditProps, RuleTypeParams, RuleTypeMetaData } from '../types'; -import { queryClient } from '../application/query_client'; - -export const getEditRuleFlyoutLazy = < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData ->( - props: RuleEditProps & { connectorServices: ConnectorServices } -) => { - return ( - - - - - - ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx similarity index 63% rename from x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx rename to x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index bda565424ee5d..6f3356370a832 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_add_rule_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -7,13 +7,10 @@ import React from 'react'; import { RuleForm, RuleFormProps } from '@kbn/response-ops-rule-form'; -import type { RuleTypeParams, RuleTypeMetaData } from '../types'; +import type { RuleTypeMetaData } from '../types'; -export const getAddRuleFlyoutLazy = < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData ->( - props: RuleFormProps +export const getRuleFormFlyoutLazy = ( + props: RuleFormProps & { onClose: () => void } ) => { return ; }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts new file mode 100644 index 0000000000000..cd43ca4290f44 --- /dev/null +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleFormProps } from '@kbn/response-ops-rule-form'; + +const requiredPluginNames = [ + 'http', + 'i18n', + 'theme', + 'userProfile', + 'application', + 'notifications', + 'charts', + 'settings', + 'data', + 'dataViews', + 'unifiedSearch', + 'docLinks', +]; + +type RequiredRuleFormPlugins = Omit< + RuleFormProps['plugins'], + 'actionTypeRegistry' | 'ruleTypeRegistry' +>; +export const validateRuleFormPlugins = (input: unknown): RequiredRuleFormPlugins => { + if (typeof input !== 'object' || input === null) { + throw new Error('Failed to validate Rule Form Plugins: not an object'); + } + + requiredPluginNames.forEach((pluginName) => { + if (!(pluginName in input)) { + throw new Error(`Failed to validate Rule Form Plugins: missing plugin ${pluginName}`); + } + }); + + return input as RequiredRuleFormPlugins; +}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts index 7fccc65a2b6b6..b927c6023fd79 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts @@ -14,8 +14,7 @@ import type { TriggersAndActionsUIPublicPluginStart } from './plugin'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; -import { getAddRuleFlyoutLazy } from './common/get_add_rule_flyout'; -import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; +import { getRuleFormFlyoutLazy } from './common/get_rule_form_flyout'; import { ActionTypeModel, RuleTypeModel, @@ -86,16 +85,8 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { connectorServices, }); }, - getAddRuleFlyout: (props) => { - return getAddRuleFlyoutLazy({ - ...props, - actionTypeRegistry, - ruleTypeRegistry, - connectorServices, - }); - }, - getEditRuleFlyout: (props) => { - return getEditRuleFlyoutLazy({ + getRuleFormFlyout: (props) => { + return getRuleFormFlyoutLazy({ ...props, actionTypeRegistry, ruleTypeRegistry, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index ac7f9bb69a0af..5dba99fa0a0e7 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -7,6 +7,7 @@ import { Plugin as CorePlugin, CoreSetup, CoreStart } from '@kbn/core/public'; +import { RuleFormProps } from '@kbn/response-ops-rule-form'; import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { RuleAction } from '@kbn/alerting-plugin/common'; import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public'; @@ -44,10 +45,9 @@ import { import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getActionFormLazy } from './common/get_action_form'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; -import { getAddRuleFlyoutLazy } from './common/get_add_rule_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; -import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; +import { getRuleFormFlyoutLazy } from './common/get_rule_form_flyout'; import { getFieldBrowserLazy } from './common/get_field_browser'; import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; @@ -59,7 +59,6 @@ import { getRulesListLazy } from './common/get_rules_list'; import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge'; import { LazyLoadProps } from './types'; -import { RuleFormProps } from '@kbn/response-ops-rule-form'; import { TriggersActionsUiConfigType } from '../common/types'; import { AlertTableConfigRegistry } from './application/alert_table_config_registry'; import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form'; @@ -77,6 +76,8 @@ import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary'; import { getRuleDefinitionLazy } from './common/get_rule_definition'; import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal'; import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link'; +import { validateRuleFormPlugins } from './common/validate_rule_form_plugins'; + import type { ActionTypeModel, AlertsTableProps, @@ -85,7 +86,6 @@ import type { EditConnectorFlyoutProps, GlobalRuleEventLogListProps, RuleDefinitionProps, - RuleEditProps, RuleEventLogListOptions, RuleEventLogListProps, RuleStatusDropdownProps, @@ -95,7 +95,6 @@ import type { RuleTagFilterProps, RuleTypeMetaData, RuleTypeModel, - RuleTypeParams, RulesListNotifyBadgePropsWithApi, RulesListProps, } from './types'; @@ -121,17 +120,11 @@ export interface TriggersAndActionsUIPublicPluginStart { getEditConnectorFlyout: ( props: Omit ) => ReactElement; - getAddRuleFlyout: ( - props: Omit & { - plugins: Omit; + getRuleFormFlyout: ( + props: Omit, 'plugins'> & { + plugins: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; } - ) => ReactElement; - getEditRuleFlyout: < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData - >( - props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> - ) => ReactElement>; + ) => ReactElement>; getAlertsTable: (props: AlertsTableProps) => ReactElement; getAlertsTableDefaultAlertActions:

( props: P @@ -490,24 +483,16 @@ export class Plugin connectorServices: this.connectorServices!, }); }, - getAddRuleFlyout: (props) => { - return getAddRuleFlyoutLazy({ + getRuleFormFlyout: (props) => { + return getRuleFormFlyoutLazy({ ...props, plugins: { - ...props.plugins, + ...validateRuleFormPlugins(props.plugins), actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, }, }); }, - getEditRuleFlyout: (props) => { - return getEditRuleFlyoutLazy({ - ...props, - actionTypeRegistry: this.actionTypeRegistry, - ruleTypeRegistry: this.ruleTypeRegistry, - connectorServices: this.connectorServices!, - }); - }, getAlertsStateTable: (props: AlertsTableStateProps & LazyLoadProps) => { return getAlertsTableStateLazy(props); }, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx index b56b12f883c96..7e4068500a83e 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useMemo } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ApmRuleType } from '@kbn/rule-data-utils'; -import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { APM_SERVER_FEATURE_ID } from '../../../../../common/rules/apm_rule_types'; import { getInitialAlertValues } from '../../utils/get_initial_alert_values'; import type { ApmPluginStartDeps } from '../../../../plugin'; @@ -51,13 +50,14 @@ export function AlertingFlyout(props: Props) { const addAlertFlyout = useMemo( () => ruleType && - services.triggersActionsUi.getAddRuleFlyout({ + services.triggersActionsUi.getRuleFormFlyout({ + plugins: services, consumer: APM_SERVER_FEATURE_ID, - onClose: onCloseAddFlyout, + onCancel: onCloseAddFlyout, + onSubmit: onCloseAddFlyout, ruleTypeId: ruleType, - canChangeTrigger: false, initialValues, - metadata: { + initialMetadata: { environment, serviceName, ...(ruleType === ApmRuleType.ErrorCount ? {} : { transactionType }), @@ -66,7 +66,7 @@ export function AlertingFlyout(props: Props) { start, end, }, - useRuleProducer: true, + shouldUseRuleProducer: true, }), /* eslint-disable-next-line react-hooks/exhaustive-deps */ [ diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx index b3137a173c289..6aa6f136f5721 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx @@ -5,16 +5,18 @@ * 2.0. */ -import { useContext, useMemo } from 'react'; -import type { RuleAddProps } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { useContext, useMemo } from 'react'; +import type { InfraClientStartDeps } from '../../../types'; import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; interface Props { - onClose: RuleAddProps['onClose']; + onClose: () => void; } export function AlertFlyout({ onClose }: Props) { + const { services } = useKibana(); const { triggersActionsUI } = useContext(TriggerActionsContext); const addAlertFlyout = useMemo(() => { @@ -22,12 +24,13 @@ export function AlertFlyout({ onClose }: Props) { return null; } - return triggersActionsUI.getAddRuleFlyout({ + return triggersActionsUI.getRuleFormFlyout({ + plugins: services, consumer: 'infrastructure', - onClose, - canChangeTrigger: false, + onCancel: onClose, + onSubmit: onClose, ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - metadata: { + initialMetadata: { currentOptions: { /* Setting the groupBy is currently required in custom threshold @@ -37,7 +40,7 @@ export function AlertFlyout({ onClose }: Props) { }, }, }); - }, [onClose, triggersActionsUI]); + }, [onClose, triggersActionsUI, services]); return addAlertFlyout; } diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 9d812e723163a..ac8e6b6ef62b1 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useContext, useMemo } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; -import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import type { InfraWaffleMapOptions } from '../../../common/inventory/types'; +import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; import { useAlertPrefillContext } from '../../use_alert_prefill'; interface Props { @@ -22,6 +23,7 @@ interface Props { } export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => { + const { services } = useKibana(); const { triggersActionsUI } = useContext(TriggerActionsContext); const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const { inventoryPrefill } = useAlertPrefillContext(); @@ -30,12 +32,13 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: const AddAlertFlyout = useMemo( () => triggersActionsUI && - triggersActionsUI.getAddRuleFlyout({ + triggersActionsUI.getRuleFormFlyout({ + plugins: services, consumer: 'infrastructure', - onClose: onCloseFlyout, - canChangeTrigger: false, + onCancel: onCloseFlyout, + onSubmit: onCloseFlyout, ruleTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - metadata: { + initialMetadata: { accountId, options, nodeType, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index c0ef5938e24cc..cddf96001d8f8 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -5,9 +5,10 @@ * 2.0. */ +import { useKibana } from '@kbn/kibana-react-plugin/public'; import React, { useCallback, useContext, useMemo } from 'react'; -import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types'; +import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; interface Props { visible?: boolean; @@ -15,22 +16,24 @@ interface Props { } export const AlertFlyout = (props: Props) => { + const { services } = useKibana(); const { visible, setVisible } = props; const { triggersActionsUI } = useContext(TriggerActionsContext); const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const AddAlertFlyout = useMemo( () => triggersActionsUI && - triggersActionsUI.getAddRuleFlyout({ + triggersActionsUI.getRuleFormFlyout({ + plugins: services, consumer: 'logs', - onClose: onCloseFlyout, - canChangeTrigger: false, + onSubmit: onCloseFlyout, + onCancel: onCloseFlyout, ruleTypeId: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, - metadata: { + initialMetadata: { isInternal: true, }, }), - [triggersActionsUI, onCloseFlyout] + [triggersActionsUI, services, onCloseFlyout] ); return <>{visible && AddAlertFlyout}; diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 8101f0c832dc0..07007dc040f63 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { useKibana } from '@kbn/kibana-react-plugin/public'; import React, { useCallback, useContext, useMemo } from 'react'; -import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import type { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { TriggerActionsContext } from '../../../containers/triggers_actions_context'; import type { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useAlertPrefillContext } from '../../use_alert_prefill'; @@ -20,18 +21,20 @@ interface Props { } export const AlertFlyout = (props: Props) => { + const { services } = useKibana(); const { visible, setVisible } = props; const { triggersActionsUI } = useContext(TriggerActionsContext); const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]); const AddAlertFlyout = useMemo( () => triggersActionsUI && - triggersActionsUI.getAddRuleFlyout({ + triggersActionsUI.getRuleFormFlyout({ + plugins: services, consumer: 'infrastructure', - onClose: onCloseFlyout, - canChangeTrigger: false, + onCancel: onCloseFlyout, + onSubmit: onCloseFlyout, ruleTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID, - metadata: { + initialMetadata: { currentOptions: props.options, series: props.series, }, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx index d484773e0b14a..d13e09f4ba1bf 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.test.tsx @@ -38,7 +38,7 @@ const mockNavigateToApp = { mockNavigateToApp: jest.fn(), }; -const mockGetEditRuleFlyout = jest.fn(() => ( +const mockGetRuleFormFlyout = jest.fn(() => (

mocked component
)); @@ -48,7 +48,7 @@ const mockKibana = () => { ...kibanaStartMock.startContract(), triggersActionsUi: { ...triggersActionsUiMock.createStart(), - getEditRuleFlyout: mockGetEditRuleFlyout, + getRuleFormFlyout: mockGetRuleFormFlyout, }, cases: mockCases, http: mockHttp, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx index 1808f5a4ea838..4b7bddfca798c 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -62,15 +62,16 @@ export function HeaderActions({ alertStatus, onUntrackAlert, }: HeaderActionsProps) { + const { services } = useKibana(); const { cases: { hooks: { useCasesAddToExistingCaseModal }, }, - triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout, getRuleSnoozeModal: RuleSnoozeModal }, + triggersActionsUi: { getRuleFormFlyout: EditRuleFlyout, getRuleSnoozeModal: RuleSnoozeModal }, http, application: { navigateToApp }, investigate: investigatePlugin, - } = useKibana().services; + } = services; const { rule, refetch } = useFetchRule({ ruleId: alert?.fields[ALERT_RULE_UUID] || '', @@ -344,11 +345,13 @@ export function HeaderActions({ {rule && ruleConditionsFlyoutOpen ? ( { + plugins={services} + id={rule.id} + onCancel={() => { setRuleConditionsFlyoutOpen(false); }} - onSave={async () => { + onSubmit={() => { + setRuleConditionsFlyoutOpen(false); refetch(); }} /> diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx index 11a3a736ecaeb..74a8d149b0e3f 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -50,6 +50,7 @@ interface RuleDetailsPathParams { ruleId: string; } export function RuleDetailsPage() { + const { services } = useKibana(); const { application: { capabilities, navigateToUrl }, http: { basePath }, @@ -60,12 +61,12 @@ export function RuleDetailsPage() { actionTypeRegistry, ruleTypeRegistry, getAlertSummaryWidget: AlertSummaryWidget, - getEditRuleFlyout: EditRuleFlyout, + getRuleFormFlyout: EditRuleFlyout, getRuleDefinition: RuleDefinition, getRuleStatusPanel: RuleStatusPanel, }, serverless, - } = useKibana().services; + } = services; const { ObservabilityPageTemplate } = usePluginContext(); const { ruleId } = useParams(); @@ -268,9 +269,11 @@ export function RuleDetailsPage() { {isEditRuleFlyoutVisible && ( { + plugins={services} + id={rule.id} + onCancel={handleCloseRuleFlyout} + onSubmit={() => { + handleCloseRuleFlyout(); refetch(); }} /> diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx index 2e3c6d9d9f832..632f3b62ac6b4 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx @@ -32,6 +32,7 @@ interface RulesPageProps { activeTab?: string; } export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { + const { services } = useKibana(); const { http, docLinks, @@ -39,11 +40,11 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { observabilityAIAssistant, triggersActionsUi: { ruleTypeRegistry, - getAddRuleFlyout: AddRuleFlyout, + getRuleFormFlyout: AddRuleFlyout, getRulesSettingsLink: RulesSettingsLink, }, serverless, - } = useKibana().services; + } = services; const { ObservabilityPageTemplate } = usePluginContext(); const history = useHistory(); const [ruleTypeModalVisibility, setRuleTypeModalVisibility] = useState(false); @@ -201,21 +202,20 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { {addRuleFlyoutVisibility && ( { + multiConsumerSelection={AlertConsumers.LOGS} + onCancel={() => { setAddRuleFlyoutVisibility(false); }} - onSave={() => { + onSubmit={() => { setRefresh(new Date()); - return Promise.resolve(); + setAddRuleFlyoutVisibility(false); }} - hideGrouping - useRuleProducer + shouldUseRuleProducer /> )} diff --git a/x-pack/solutions/observability/plugins/observability/public/plugin.mock.tsx b/x-pack/solutions/observability/plugins/observability/public/plugin.mock.tsx index 378bdf888df84..c7eea0d0c2437 100644 --- a/x-pack/solutions/observability/plugins/observability/public/plugin.mock.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/plugin.mock.tsx @@ -50,9 +50,8 @@ const triggersActionsUiStartMock = { ); }, - getAddRuleFlyout: jest.fn(() =>
mocked component
), - getEditRuleFlyout: jest.fn(() => ( -
mocked component
+ getRuleFormFlyout: jest.fn(() => ( +
mocked component
)), getRuleAlertsSummary: jest.fn(() => (
mocked component
diff --git a/x-pack/solutions/observability/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/solutions/observability/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx index 09740a5be71af..fc033b41ffd0d 100644 --- a/x-pack/solutions/observability/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -97,7 +97,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { theme: { theme$: createTheme$Mock(), }, - triggersActionsUi: { getAddRuleFlyout: {} }, + triggersActionsUi: { getRuleFormFlyout: {} }, uiSettings: { get: (setting: string) => { if (setting === 'dateFormat') { diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts index deaabffeeb50d..265fb7b562e07 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/__storybook_mocks__/use_kibana.ts @@ -45,7 +45,7 @@ export function useKibana() { }, }, }, - triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} }, + triggersActionsUi: { getRuleFormFlyout: {}, getAddConnectorFlyout: {} }, }, }, uiSettings: { diff --git a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx index ba20e0d4f2745..6966b886dbd5e 100644 --- a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx +++ b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx @@ -44,7 +44,7 @@ function getQuery(query?: Query | AggregateQuery): Query { export const AlertsPopover = () => { const { - services: { triggersActionsUi, slo, application, http }, + services: { triggersActionsUi, slo, application, http, ...plugins }, } = useKibanaContextForPlugin(); const manageRulesLinkProps = useLinkProps({ app: 'observability', pathname: '/alerts/rules' }); @@ -67,10 +67,10 @@ export const AlertsPopover = () => { pageState.context.allSelection ).toDataviewSpec(); - return triggersActionsUi.getAddRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ + plugins: { application, http, ...plugins }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - canChangeTrigger: false, initialValues: { params: { searchConfiguration: { @@ -84,10 +84,19 @@ export const AlertsPopover = () => { }, }, }, - onClose: closeAddRuleFlyout, + onCancel: closeAddRuleFlyout, + onSubmit: closeAddRuleFlyout, }); } - }, [closeAddRuleFlyout, triggersActionsUi, pageState, isAddRuleFlyoutOpen]); + }, [ + isAddRuleFlyoutOpen, + triggersActionsUi, + pageState, + application, + http, + plugins, + closeAddRuleFlyout, + ]); const createSLOFlyout = useMemo(() => { if (isCreateSLOFlyoutOpen && pageState.matches({ initialized: 'validLogsExplorerState' })) { diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx index fd77526197ca2..4b6c6d53ca84c 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx @@ -40,11 +40,12 @@ export interface Props { } export function HeaderControl({ slo }: Props) { + const { services } = useKibana(); const { application: { navigateToUrl, capabilities }, http: { basePath }, - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, - } = useKibana().services; + triggersActionsUi: { getRuleFormFlyout: AddRuleFlyout }, + } = services; const hasApmReadCapabilities = capabilities.apm.show; const { data: permissions } = usePermissions(); @@ -401,12 +402,13 @@ export function HeaderControl({ slo }: Props) { {isRuleFlyoutVisible ? ( ) : null} diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/slo_details.test.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/slo_details.test.tsx index a363fdccb16f8..ba1cae77492a4 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/slo_details.test.tsx @@ -113,7 +113,7 @@ const mockKibana = () => { }, }, triggersActionsUi: { - getAddRuleFlyout: jest.fn(() => ( + getRuleFormFlyout: jest.fn(() => (
mocked component
)), }, diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/slo_edit.test.tsx index 8d52ed914302c..3aaf460903de5 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/slo_edit.test.tsx @@ -122,7 +122,7 @@ const mockKibana = (license: ILicense | null = licenseMock) => { get: () => {}, }, triggersActionsUi: { - getAddRuleFlyout: jest + getRuleFormFlyout: jest .fn() .mockReturnValue(
Add Rule Flyout
), }, diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx index f8e6890483404..f4dfbe03a447a 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/burn_rate_rule_flyout.tsx @@ -26,11 +26,12 @@ export function BurnRateRuleFlyout({ canChangeTrigger?: boolean; setIsAddRuleFlyoutOpen?: (value: boolean) => void; }) { + const { services } = useKibana(); const { application: { navigateToUrl }, http: { basePath }, - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, - } = useKibana().services; + triggersActionsUi: { getRuleFormFlyout: AddRuleFlyout }, + } = services; const filteredRuleTypes = useGetFilteredRuleTypes(); @@ -39,6 +40,7 @@ export function BurnRateRuleFlyout({ const handleSavedRule = async () => { if (setIsAddRuleFlyoutOpen) { queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); + setIsAddRuleFlyoutOpen(false); } else { navigateToUrl(basePath.prepend(paths.slos)); } @@ -54,14 +56,14 @@ export function BurnRateRuleFlyout({ return isAddRuleFlyoutOpen && slo ? ( ) : null; } diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx index 89701b0922f87..3b5c0fe2e9a89 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/common/edit_burn_rate_rule_flyout.tsx @@ -22,7 +22,8 @@ export function EditBurnRateRuleFlyout({ refetchRules: () => void; }) { const { - triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout }, + triggersActionsUi: { getRuleFormFlyout: EditRuleFlyout }, + ...plugins } = useKibana().services; const handleSavedRule = async () => { @@ -35,6 +36,11 @@ export function EditBurnRateRuleFlyout({ }; return isEditRuleFlyoutOpen && rule ? ( - + ) : null; } diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx index 5e4584105c1a3..e95b962989fb7 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/components/compact_view/slo_list_compact_view.tsx @@ -64,6 +64,7 @@ export interface Props { } export function SloListCompactView({ sloList, loading, error }: Props) { + const { services } = useKibana(); const { application: { navigateToUrl }, http: { basePath }, @@ -71,8 +72,8 @@ export function SloListCompactView({ sloList, loading, error }: Props) { share: { url: { locators }, }, - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout }, - } = useKibana().services; + triggersActionsUi: { getRuleFormFlyout: AddRuleFlyout }, + } = services; const spaceId = useSpace(); const percentFormat = uiSettings.get('format:percent:defaultPattern'); @@ -135,8 +136,9 @@ export function SloListCompactView({ sloList, loading, error }: Props) { setSloToDisable(undefined); }; - const handleSavedRule = async () => { + const handleSavedRule = () => { queryClient.invalidateQueries({ queryKey: sloKeys.rules(), exact: false }); + setSloToAddRule(undefined); }; const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIdsAndInstanceIds }); @@ -531,6 +533,7 @@ export function SloListCompactView({ sloList, loading, error }: Props) { /> {sloToAddRule ? ( { + onSubmit={handleSavedRule} + onCancel={() => { setSloToAddRule(undefined); }} - useRuleProducer + shouldUseRuleProducer /> ) : null} diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx index 3a9c21c2ffa0e..f694e72995196 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx @@ -80,7 +80,7 @@ const mockNavigate = jest.fn(); const mockAddSuccess = jest.fn(); const mockAddError = jest.fn(); const mockLocator = jest.fn(); -const mockGetAddRuleFlyout = jest.fn().mockReturnValue(() =>
Add rule flyout
); +const mockGetRuleFormFlyout = jest.fn().mockReturnValue(() =>
Add rule flyout
); const mockKibana = () => { useKibanaMock.mockReturnValue({ @@ -124,7 +124,7 @@ const mockKibana = () => { storage: { get: () => {}, }, - triggersActionsUi: { getAddRuleFlyout: mockGetAddRuleFlyout }, + triggersActionsUi: { mockGetRuleFormFlyout: mockGetRuleFormFlyout }, uiSettings: { get: (settings: string) => { if (settings === 'dateFormat') return 'YYYY-MM-DD'; @@ -321,7 +321,7 @@ describe('SLOs Page', () => { button.click(); - expect(mockGetAddRuleFlyout).toBeCalled(); + expect(mockGetRuleFormFlyout).toBeCalled(); }); it('allows managing rules for an SLO', async () => { diff --git a/x-pack/solutions/observability/plugins/slo/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/solutions/observability/plugins/slo/public/utils/kibana_react.storybook_decorator.tsx index 92a9f0b03a35a..2a416f4e7e29e 100644 --- a/x-pack/solutions/observability/plugins/slo/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/utils/kibana_react.storybook_decorator.tsx @@ -88,7 +88,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { theme: { theme$: createTheme$Mock(), }, - triggersActionsUi: { getAddRuleFlyout: {} }, + triggersActionsUi: { getRuleFormFlyout: {} }, uiSettings: { get: (setting: string) => { if (setting === 'dateFormat') { diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts index 5ffb17b639768..0f733acb7330e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_rules.ts @@ -68,7 +68,9 @@ export const useSyntheticsRules = (isOpen: boolean) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, isOpen, hasMonitors, defaultRulesEnabled]); - const { triggersActionsUi } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; + + const onClose = useMemo(() => () => dispatch(setAlertFlyoutVisible(null)), [dispatch]); const EditAlertFlyout = useMemo(() => { const initialRule = @@ -76,9 +78,11 @@ export const useSyntheticsRules = (isOpen: boolean) => { if (!initialRule || isNewRule) { return null; } - return triggersActionsUi.getEditRuleFlyout({ - onClose: () => dispatch(setAlertFlyoutVisible(null)), - initialRule, + return triggersActionsUi.getRuleFormFlyout({ + plugins, + onCancel: onClose, + onSubmit: onClose, + id: initialRule.id, }); }, [ alertFlyoutVisible, @@ -86,17 +90,20 @@ export const useSyntheticsRules = (isOpen: boolean) => { defaultRules?.statusRule, isNewRule, triggersActionsUi, - dispatch, + plugins, + onClose, ]); const NewRuleFlyout = useMemo(() => { if (!isNewRule || !alertFlyoutVisible) { return null; } - return triggersActionsUi.getAddRuleFlyout({ + return triggersActionsUi.getRuleFormFlyout({ + plugins, consumer: 'uptime', ruleTypeId: alertFlyoutVisible, - onClose: () => dispatch(setAlertFlyoutVisible(null)), + onCancel: onClose, + onSubmit: onClose, initialValues: { name: alertFlyoutVisible === SYNTHETICS_TLS_RULE @@ -108,7 +115,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { }), }, }); - }, [isNewRule, triggersActionsUi, dispatch, alertFlyoutVisible]); + }, [isNewRule, alertFlyoutVisible, triggersActionsUi, plugins, onClose]); return useMemo( () => ({ loading, EditAlertFlyout, NewRuleFlyout }), diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx index 14f5787cb66db..833752afd95c1 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Rule, @@ -28,17 +28,21 @@ export const UptimeEditAlertFlyoutComponent = ({ initialAlert, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; + + const onClose = useCallback(() => { + setAlertFlyoutVisibility(false); + }, [setAlertFlyoutVisibility]); const EditAlertFlyout = useMemo( () => - triggersActionsUi.getEditRuleFlyout({ - initialRule: initialAlert, - onClose: () => { - setAlertFlyoutVisibility(false); - }, + triggersActionsUi.getRuleFormFlyout({ + id: initialAlert.id, + onCancel: onClose, + onSubmit: onClose, + plugins, }), - [initialAlert, setAlertFlyoutVisibility, triggersActionsUi] + [initialAlert, triggersActionsUi, onClose, plugins] ); return <>{alertFlyoutVisible && EditAlertFlyout}; }; diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/monitor/ml/ml_integerations.test.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/monitor/ml/ml_integerations.test.tsx index fe9fc0bbe496c..c5db4bd5ad36a 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/monitor/ml/ml_integerations.test.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/monitor/ml/ml_integerations.test.tsx @@ -31,7 +31,7 @@ describe('ML Integrations', () => { it('renders without errors', () => { const wrapper = renderWithRouter( diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 5135d6cd26a86..77d9b0856e26c 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -24,18 +24,19 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ alertTypeId, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; const onCloseAlertFlyout = useCallback( () => setAlertFlyoutVisibility(false), [setAlertFlyoutVisibility] ); const AddAlertFlyout = useMemo( () => - triggersActionsUi.getAddRuleFlyout({ + triggersActionsUi.getRuleFormFlyout({ + plugins, consumer: 'uptime', - onClose: onCloseAlertFlyout, + onCancel: onCloseAlertFlyout, + onSubmit: onCloseAlertFlyout, ruleTypeId: alertTypeId, - canChangeTrigger: !alertTypeId, }), // eslint-disable-next-line react-hooks/exhaustive-deps [onCloseAlertFlyout, alertTypeId] From 6dd013d7280c66a88c8e7a8fba406e5572bb8d81 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 13 Jan 2025 19:03:10 -0600 Subject: [PATCH 07/62] Remove missing import --- .../packages/shared/response-ops/rule_form/src/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts index 15d1f575c71a8..53c9aedda7545 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/utils/index.ts @@ -18,4 +18,3 @@ export * from './has_fields_for_aad'; export * from './get_selected_action_group'; export * from './get_initial_consumer'; export * from './get_default_params'; -export * from './validate_rule_form_plugins'; From 69eb8fa5a43294f5933b64c2797d8b108500efac Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 12:20:29 -0600 Subject: [PATCH 08/62] Add isServerless flag to rule flyout --- .../response-ops/rule_form/src/create_rule_form.tsx | 3 +++ .../shared/response-ops/rule_form/src/rule_form.tsx | 11 +++++++---- .../shared/response-ops/rule_form/src/types.ts | 5 ++++- .../rule_form/src/utils/get_initial_multi_consumer.ts | 6 ++++-- .../top_nav/app_menu_actions/get_alerts.tsx | 2 +- .../accessors/get_app_menu.ts | 2 +- .../public/components/create_alert.tsx | 2 +- .../public/alerting/transform_alerting_flyout.tsx | 6 +++--- .../sections/rule_form/rule_form_route.tsx | 4 +++- .../public/common/get_rule_form_flyout.tsx | 2 +- .../public/common/validate_rule_form_plugins.ts | 2 +- .../shared/triggers_actions_ui/public/plugin.ts | 6 +++--- .../alerting/ui_components/alerting_flyout/index.tsx | 2 +- .../custom_threshold/components/alert_flyout.tsx | 2 +- .../alerting/inventory/components/alert_flyout.tsx | 4 ++-- .../log_threshold/components/alert_flyout.tsx | 2 +- .../metric_threshold/components/alert_flyout.tsx | 2 +- .../pages/alert_details/components/header_actions.tsx | 2 +- .../public/pages/rule_details/rule_details.tsx | 2 +- .../observability/public/pages/rules/rules.tsx | 4 ++-- .../public/components/alerts_popover.tsx | 6 +++--- .../pages/slo_details/components/header_control.tsx | 2 +- .../slos/components/common/burn_rate_rule_flyout.tsx | 2 +- .../components/common/edit_burn_rate_rule_flyout.tsx | 2 +- .../components/compact_view/slo_list_compact_view.tsx | 2 +- .../components/alerts/hooks/use_synthetics_rules.ts | 10 +++++----- .../common/alerts/uptime_edit_alert_flyout.tsx | 6 +++--- .../overview/alerts/uptime_alerts_flyout_wrapper.tsx | 4 ++-- 28 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 8e083556358e9..a6bcfc8a47060 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -51,6 +51,7 @@ export interface CreateRuleFormProps { onChangeMetaData?: (metadata: RuleFormState['metadata']) => void; initialValues?: Partial; initialMetadata?: RuleFormState['metadata']; + isServerless?: boolean; } export const CreateRuleForm = (props: CreateRuleFormProps) => { @@ -71,6 +72,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { onChangeMetaData, initialMetadata = {}, initialValues = {}, + isServerless, } = props; const { http, docLinks, notifications, ruleTypeRegistry, ...deps } = plugins; @@ -210,6 +212,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { validConsumers, ruleType, ruleTypes, + isServerless, }), }} > diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 34eb1e1b19cd8..72fe66f67309a 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -19,12 +19,12 @@ import { RULE_FORM_ROUTE_PARAMS_ERROR_TEXT, RULE_FORM_ROUTE_PARAMS_ERROR_TITLE, } from './translations'; -import { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; +import { RuleFormData, RuleFormServices, RuleFormState } from './types'; const queryClient = new QueryClient(); export interface RuleFormProps { - plugins: RuleFormPlugins; + services: RuleFormServices; id?: string; ruleTypeId?: string; isFlyout?: boolean; @@ -48,7 +48,7 @@ export const RuleForm = ) => { const { - plugins: _plugins, + services: _services, onCancel, onSubmit, onChangeMetaData, @@ -83,7 +83,8 @@ export const RuleForm = { const plugins = { @@ -137,6 +138,7 @@ export const RuleForm = ); } @@ -167,6 +169,7 @@ export const RuleForm = { flapping?: Rule['flapping']; } -export interface RuleFormPlugins { +export interface RuleFormServices { http: HttpStart; i18n: I18nStart; theme: ThemeServiceStart; @@ -68,8 +68,11 @@ export interface RuleFormPlugins { docLinks: DocLinksStart; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; + isServerless?: boolean; } +export type RuleFormPlugins = Omit; + export interface RuleFormState< Params extends RuleTypeParams = RuleTypeParams, MetaData = RuleTypeMetaData diff --git a/src/platform/packages/shared/response-ops/rule_form/src/utils/get_initial_multi_consumer.ts b/src/platform/packages/shared/response-ops/rule_form/src/utils/get_initial_multi_consumer.ts index 088348c510100..e6e6e13560b6d 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/utils/get_initial_multi_consumer.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/utils/get_initial_multi_consumer.ts @@ -35,11 +35,13 @@ export const getInitialMultiConsumer = ({ validConsumers, ruleType, ruleTypes, + isServerless, }: { multiConsumerSelection?: RuleCreationValidConsumer | null; validConsumers: RuleCreationValidConsumer[]; ruleType: RuleTypeWithDescription; ruleTypes: RuleTypeWithDescription[]; + isServerless?: boolean; }): RuleCreationValidConsumer | null => { // If rule type doesn't support multi-consumer or no valid consumers exists, // return nothing @@ -52,8 +54,8 @@ export const getInitialMultiConsumer = ({ return validConsumers[0]; } - // If o11y is in the valid consumers, just use that - if (validConsumers.includes(AlertConsumers.OBSERVABILITY)) { + // If o11y is in the valid consumers on serverless, just use that + if (isServerless && validConsumers.includes(AlertConsumers.OBSERVABILITY)) { return AlertConsumers.OBSERVABILITY; } diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index 405f9be1f715a..cb0a872829ca4 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -80,7 +80,7 @@ const CreateAlertFlyout: React.FC<{ ); return triggersActionsUi?.getRuleFormFlyout({ - plugins: services, + services, initialMetadata: discoverMetadata, consumer: 'alerts', onCancel: onFinishAction, diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts index 902d94e8c02d4..2e0dd06bd3745 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts @@ -92,7 +92,7 @@ const registerCustomThresholdRuleAction = ( const { filters, query } = data.query.getState(); return triggersActionsUi.getRuleFormFlyout({ - plugins: { data, ...services }, + services: { data, ...services }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, initialValues: { diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx index 7c0e665c20b2e..1b14cfe08bd63 100644 --- a/x-pack/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -38,7 +38,7 @@ export const CreateAlert = ({ {ruleFlyoutVisible ? ( = ({ onCloseFlyout, onSave, }) => { - const { triggersActionsUi, ...plugins } = useAppDependencies(); + const { triggersActionsUi, ...services } = useAppDependencies(); const AlertFlyout = useMemo(() => { if (!triggersActionsUi) return; const commonProps = { - plugins, + services, onCancel: () => { onCloseFlyout(); }, @@ -67,7 +67,7 @@ export const TransformAlertFlyout: FC = ({ }, }); // deps on id to avoid re-rendering on auto-refresh - }, [triggersActionsUi, plugins, initialAlert, ruleParams, onCloseFlyout, onSave]); + }, [triggersActionsUi, services, initialAlert, ruleParams, onCloseFlyout, onSave]); return <>{AlertFlyout}; }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx index 71ab959c369ec..6925bfaca67d2 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx @@ -28,6 +28,7 @@ export const RuleFormRoute = () => { ruleTypeRegistry, actionTypeRegistry, chrome, + isServerless, setBreadcrumbs, ...startServices } = useKibana().services; @@ -61,7 +62,7 @@ export const RuleFormRoute = () => { return ( { docLinks, ruleTypeRegistry, actionTypeRegistry, + isServerless, ...startServices, }} id={id} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index 6f3356370a832..82db10d29af2c 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -10,7 +10,7 @@ import { RuleForm, RuleFormProps } from '@kbn/response-ops-rule-form'; import type { RuleTypeMetaData } from '../types'; export const getRuleFormFlyoutLazy = ( - props: RuleFormProps & { onClose: () => void } + props: RuleFormProps ) => { return ; }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts index cd43ca4290f44..121822f44d451 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts @@ -23,7 +23,7 @@ const requiredPluginNames = [ ]; type RequiredRuleFormPlugins = Omit< - RuleFormProps['plugins'], + RuleFormProps['services'], 'actionTypeRegistry' | 'ruleTypeRegistry' >; export const validateRuleFormPlugins = (input: unknown): RequiredRuleFormPlugins => { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index 5dba99fa0a0e7..8f618f5e89eb0 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -121,8 +121,8 @@ export interface TriggersAndActionsUIPublicPluginStart { props: Omit ) => ReactElement; getRuleFormFlyout: ( - props: Omit, 'plugins'> & { - plugins: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; + props: Omit, 'services'> & { + services: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; } ) => ReactElement>; getAlertsTable: (props: AlertsTableProps) => ReactElement; @@ -486,7 +486,7 @@ export class Plugin getRuleFormFlyout: (props) => { return getRuleFormFlyoutLazy({ ...props, - plugins: { + services: { ...validateRuleFormPlugins(props.plugins), actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx index 7e4068500a83e..8809a2e3980dc 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx @@ -51,7 +51,7 @@ export function AlertingFlyout(props: Props) { () => ruleType && services.triggersActionsUi.getRuleFormFlyout({ - plugins: services, + services, consumer: APM_SERVER_FEATURE_ID, onCancel: onCloseAddFlyout, onSubmit: onCloseAddFlyout, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx index 6aa6f136f5721..ff7ca6224e83f 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx @@ -25,7 +25,7 @@ export function AlertFlyout({ onClose }: Props) { } return triggersActionsUI.getRuleFormFlyout({ - plugins: services, + services, consumer: 'infrastructure', onCancel: onClose, onSubmit: onClose, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index ac8e6b6ef62b1..b5d917beb855a 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -33,7 +33,7 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - plugins: services, + services, consumer: 'infrastructure', onCancel: onCloseFlyout, onSubmit: onCloseFlyout, @@ -46,7 +46,7 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: customMetrics, region, }, - useRuleProducer: true, + shouldUseRuleProducer: true, }), // eslint-disable-next-line react-hooks/exhaustive-deps [triggersActionsUI, visible] diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index cddf96001d8f8..7e4d01e3298c6 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -24,7 +24,7 @@ export const AlertFlyout = (props: Props) => { () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - plugins: services, + services, consumer: 'logs', onSubmit: onCloseFlyout, onCancel: onCloseFlyout, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 07007dc040f63..151a9eda0de2b 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -29,7 +29,7 @@ export const AlertFlyout = (props: Props) => { () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - plugins: services, + services, consumer: 'infrastructure', onCancel: onCloseFlyout, onSubmit: onCloseFlyout, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx index 4b7bddfca798c..8449e5645a58e 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -345,7 +345,7 @@ export function HeaderActions({ {rule && ruleConditionsFlyoutOpen ? ( { setRuleConditionsFlyoutOpen(false); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx index 74a8d149b0e3f..96343bd69bec9 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -269,7 +269,7 @@ export function RuleDetailsPage() { {isEditRuleFlyoutVisible && ( { diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx index 632f3b62ac6b4..89340fe8c2398 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rules.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { RuleTypeModal } from '@kbn/response-ops-rule-form/src/rule_type_modal'; +import { RuleTypeModal } from '@kbn/response-ops-rule-form'; import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -202,7 +202,7 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { {addRuleFlyoutVisibility && ( { const { - services: { triggersActionsUi, slo, application, http, ...plugins }, + services: { triggersActionsUi, slo, application, http, ...services }, } = useKibanaContextForPlugin(); const manageRulesLinkProps = useLinkProps({ app: 'observability', pathname: '/alerts/rules' }); @@ -68,7 +68,7 @@ export const AlertsPopover = () => { ).toDataviewSpec(); return triggersActionsUi.getRuleFormFlyout({ - plugins: { application, http, ...plugins }, + services: { application, http, ...services }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, initialValues: { @@ -94,7 +94,7 @@ export const AlertsPopover = () => { pageState, application, http, - plugins, + services, closeAddRuleFlyout, ]); diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx index 4b6c6d53ca84c..dc95828f1df79 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx @@ -402,7 +402,7 @@ export function HeaderControl({ slo }: Props) { {isRuleFlyoutVisible ? ( {sloToAddRule ? ( { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, isOpen, hasMonitors, defaultRulesEnabled]); - const { triggersActionsUi, ...plugins } = useKibana().services; + const { triggersActionsUi, ...services } = useKibana().services; const onClose = useMemo(() => () => dispatch(setAlertFlyoutVisible(null)), [dispatch]); @@ -79,7 +79,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { return null; } return triggersActionsUi.getRuleFormFlyout({ - plugins, + services, onCancel: onClose, onSubmit: onClose, id: initialRule.id, @@ -90,7 +90,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { defaultRules?.statusRule, isNewRule, triggersActionsUi, - plugins, + services, onClose, ]); @@ -99,7 +99,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { return null; } return triggersActionsUi.getRuleFormFlyout({ - plugins, + services, consumer: 'uptime', ruleTypeId: alertFlyoutVisible, onCancel: onClose, @@ -115,7 +115,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { }), }, }); - }, [isNewRule, alertFlyoutVisible, triggersActionsUi, plugins, onClose]); + }, [isNewRule, alertFlyoutVisible, triggersActionsUi, services, onClose]); return useMemo( () => ({ loading, EditAlertFlyout, NewRuleFlyout }), diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx index 833752afd95c1..2a5b446e9161b 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -28,7 +28,7 @@ export const UptimeEditAlertFlyoutComponent = ({ initialAlert, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi, ...plugins } = useKibana().services; + const { triggersActionsUi, ...services } = useKibana().services; const onClose = useCallback(() => { setAlertFlyoutVisibility(false); @@ -40,9 +40,9 @@ export const UptimeEditAlertFlyoutComponent = ({ id: initialAlert.id, onCancel: onClose, onSubmit: onClose, - plugins, + services, }), - [initialAlert, triggersActionsUi, onClose, plugins] + [initialAlert, triggersActionsUi, onClose, services] ); return <>{alertFlyoutVisible && EditAlertFlyout}; }; diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 77d9b0856e26c..1769213fc1b86 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -24,7 +24,7 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ alertTypeId, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi, ...plugins } = useKibana().services; + const { triggersActionsUi, ...services } = useKibana().services; const onCloseAlertFlyout = useCallback( () => setAlertFlyoutVisibility(false), [setAlertFlyoutVisibility] @@ -32,7 +32,7 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ const AddAlertFlyout = useMemo( () => triggersActionsUi.getRuleFormFlyout({ - plugins, + services, consumer: 'uptime', onCancel: onCloseAlertFlyout, onSubmit: onCloseAlertFlyout, From 74d5339df98be5da3f66b59afe9aab6f22868f78 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 12:46:10 -0600 Subject: [PATCH 09/62] Fix metadata typing --- .../rule_form/src/create_rule_form.tsx | 6 +++--- .../response-ops/rule_form/src/edit_rule_form.tsx | 6 +++--- .../rule_form/src/rule_flyout/rule_flyout.tsx | 4 ++-- .../response-ops/rule_form/src/rule_form.tsx | 15 ++++++++++----- .../top_nav/app_menu_actions/get_alerts.tsx | 2 +- .../shared/triggers_actions_ui/public/plugin.ts | 4 +++- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index a6bcfc8a47060..918a87d2c9284 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -11,7 +11,7 @@ import React, { useCallback } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils'; -import type { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; +import type { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types'; import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from './common/hooks'; @@ -48,9 +48,9 @@ export interface CreateRuleFormProps { isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; - onChangeMetaData?: (metadata: RuleFormState['metadata']) => void; + onChangeMetaData?: (metadata?: RuleTypeMetaData) => void; initialValues?: Partial; - initialMetadata?: RuleFormState['metadata']; + initialMetadata?: RuleTypeMetaData; isServerless?: boolean; } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx index 13e1ffc557461..06b7406ed9388 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import type { RuleFormData, RuleFormPlugins, RuleFormState } from './types'; +import type { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types'; import { RuleFormStateProvider } from './rule_form_state'; import { useUpdateRule } from './common/hooks'; import { RulePage } from './rule_page'; @@ -36,8 +36,8 @@ export interface EditRuleFormProps { isFlyout?: boolean; onCancel?: () => void; onSubmit?: (ruleId: string) => void; - onChangeMetaData?: (metadata: RuleFormState['metadata']) => void; - initialMetadata?: RuleFormState['metadata']; + onChangeMetaData?: (metadata?: RuleTypeMetaData) => void; + initialMetadata?: RuleTypeMetaData; } export const EditRuleForm = (props: EditRuleFormProps) => { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 38f0ae7db196b..1a6d5a27272b5 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -9,7 +9,7 @@ import { EuiFlyout, EuiPortal } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; -import type { RuleFormData, RuleFormState } from '../types'; +import type { RuleFormData, RuleTypeMetaData } from '../types'; import { RuleFormStepId } from '../constants'; import { RuleFlyoutBody } from './rule_flyout_body'; import { RuleFlyoutShowRequest } from './rule_flyout_show_request'; @@ -21,7 +21,7 @@ interface RuleFlyoutProps { isSaving?: boolean; onCancel?: () => void; onSave: (formData: RuleFormData) => void; - onChangeMetaData?: (metadata?: RuleFormState['metadata']) => void; + onChangeMetaData?: (metadata?: RuleTypeMetaData) => void; } export const RuleFlyout = ({ diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 72fe66f67309a..2c08537754d01 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -19,11 +19,11 @@ import { RULE_FORM_ROUTE_PARAMS_ERROR_TEXT, RULE_FORM_ROUTE_PARAMS_ERROR_TITLE, } from './translations'; -import { RuleFormData, RuleFormServices, RuleFormState } from './types'; +import { RuleFormData, RuleFormServices, RuleTypeMetaData } from './types'; const queryClient = new QueryClient(); -export interface RuleFormProps { +export interface RuleFormProps { services: RuleFormServices; id?: string; ruleTypeId?: string; @@ -44,7 +44,7 @@ export interface RuleFormProps { initialMetadata?: MetaData; } -export const RuleForm = ( +export const RuleForm = ( props: RuleFormProps ) => { const { @@ -103,6 +103,11 @@ export const RuleForm = void; + if (id) { return ( ({ services, initialMetadata: discoverMetadata, consumer: 'alerts', diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index 8f618f5e89eb0..0029397f97353 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -122,6 +122,8 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getRuleFormFlyout: ( props: Omit, 'services'> & { + // Use Partial for compatibility with useKibana hooks that return + // possibly undefined services. Use validateRuleFormPlugins to ensure that the services are not undefined. services: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; } ) => ReactElement>; @@ -487,7 +489,7 @@ export class Plugin return getRuleFormFlyoutLazy({ ...props, services: { - ...validateRuleFormPlugins(props.plugins), + ...validateRuleFormPlugins(props.services), actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, }, From 646af0f4c546928654dd8e66c678664aaa1f5a4f Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 12:51:28 -0600 Subject: [PATCH 10/62] Fix initialMetadata error --- .../shared/response-ops/rule_form/src/create_rule_form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 918a87d2c9284..08ce6e29f3a4a 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -70,7 +70,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { onCancel, onSubmit, onChangeMetaData, - initialMetadata = {}, + initialMetadata, initialValues = {}, isServerless, } = props; From d4c91d3210e389a749acecb127ef5d5e81cd3480 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 14:12:48 -0600 Subject: [PATCH 11/62] Add flyout close confirm and no actions callout --- .../rule_form/src/create_rule_form.tsx | 1 + .../rule_form/src/edit_rule_form.tsx | 1 + .../src/hooks/use_rule_form_steps.tsx | 11 ++- .../rule_form/src/rule_flyout/rule_flyout.tsx | 31 +++++++-- .../src/rule_flyout/rule_flyout_body.tsx | 21 ++++++ .../rule_flyout_confirm_cancel.tsx | 67 +++++++++++++++++++ .../rule_form_screen_context.tsx | 5 ++ .../rule_form_state_provider.tsx | 25 ++++++- .../rule_form/src/translations.ts | 17 ++++- .../public/alerts/configuration.tsx | 2 +- .../ml/public/alerting/ml_alerting_flyout.tsx | 2 +- 11 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 08ce6e29f3a4a..5dfbe470a7d5f 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -172,6 +172,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { return (
{ return (
; + isIncomplete?: boolean; }) => { // Only apply the current status if currentStep is being tracked if (currentStep === step) return 'current'; @@ -61,6 +63,11 @@ const getStepStatus = ({ // Otherwise just mark it as incomplete return touchedSteps[step] ? 'danger' : 'incomplete'; } + + if (isIncomplete) { + return 'incomplete'; + } + // Only mark this step complete or incomplete if the currentStep flag is being used, otherwise set no status if (currentStep && isStepBefore(step, currentStep)) { return 'complete'; @@ -83,6 +90,7 @@ const useCommonRuleFormSteps = ({ paramsErrors = {}, actionsErrors = {}, actionsParamsErrors = {}, + formData: { actions }, } = useRuleFormState(); const canReadConnectors = !!application.capabilities.actions?.show; @@ -121,8 +129,9 @@ const useCommonRuleFormSteps = ({ currentStep, hasErrors: hasActionErrors, touchedSteps, + isIncomplete: actions.length === 0, }), - [hasActionErrors, currentStep, touchedSteps] + [hasActionErrors, currentStep, touchedSteps, actions] ); const ruleDetailsStatus = useMemo( diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 1a6d5a27272b5..365b4eac31a76 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -13,8 +13,9 @@ import type { RuleFormData, RuleTypeMetaData } from '../types'; import { RuleFormStepId } from '../constants'; import { RuleFlyoutBody } from './rule_flyout_body'; import { RuleFlyoutShowRequest } from './rule_flyout_show_request'; -import { useRuleFormScreenContext } from '../hooks'; +import { useRuleFormScreenContext, useRuleFormState } from '../hooks'; import { RuleFlyoutSelectConnector } from './rule_flyout_select_connector'; +import { RuleFlyoutConfirmCancel } from './rule_flyout_confirm_cancel'; interface RuleFlyoutProps { isEdit?: boolean; @@ -36,8 +37,10 @@ export const RuleFlyout = ({ const { isConnectorsScreenVisible, isShowRequestScreenVisible, + isConfirmCancelScreenVisible, setIsShowRequestScreenVisible, setIsConnectorsScreenVisible, + setIsConfirmCancelScreenVisible, } = useRuleFormScreenContext(); const onCloseConnectorsScreen = useCallback(() => { setInitialStep(RuleFormStepId.ACTIONS); @@ -53,32 +56,46 @@ export const RuleFlyout = ({ setIsShowRequestScreenVisible(false); }, [setIsShowRequestScreenVisible]); + const onCancelBack = useCallback(() => { + setIsConfirmCancelScreenVisible(false); + }, [setIsConfirmCancelScreenVisible]); + const hideCloseButton = useMemo( - () => isShowRequestScreenVisible || isConnectorsScreenVisible, - [isConnectorsScreenVisible, isShowRequestScreenVisible] + () => isShowRequestScreenVisible || isConnectorsScreenVisible || isConfirmCancelScreenVisible, + [isConnectorsScreenVisible, isShowRequestScreenVisible, isConfirmCancelScreenVisible] ); - const hideCloseButton = useMemo(() => isShowRequestOpen, [isShowRequestOpen]); + const { touched } = useRuleFormState(); + + const onCancelInternal = useCallback(() => { + if (touched) { + setIsConfirmCancelScreenVisible(true); + } else { + onCancel(); + } + }, [touched, setIsConfirmCancelScreenVisible, onCancel]); return ( - {isShowRequestScreenVisible ? ( + {isConfirmCancelScreenVisible ? ( + + ) : isShowRequestScreenVisible ? ( ) : isConnectorsScreenVisible ? ( ) : ( { if (!isEmpty(metadata)) { onChangeMetaData(metadata); @@ -118,6 +123,11 @@ export const RuleFlyoutBody = ({ }); }, [actions, connectors, connectorTypes]); + const showNoActionsCallout = useMemo( + () => !hasActionsDisabled && actions.length === 0 && (isEdit || !hasNextStep), + [actions, hasActionsDisabled, hasNextStep, isEdit] + ); + return ( <> @@ -144,6 +154,17 @@ export const RuleFlyoutBody = ({ )} {currentStepComponent} + {showNoActionsCallout && ( + +

{RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TEXT}

+
+ )} {isEdit ? ( void; + onConfirm: () => void; +} +export const RuleFlyoutConfirmCancel = ({ onBack, onConfirm }: RuleFlyoutShowRequestProps) => { + return ( + <> + + +

{RULE_FORM_CANCEL_MODAL_TITLE}

+
+
+ +

+ {RULE_FORM_CANCEL_MODAL_DESCRIPTION} +

+
+ + + + + {RULE_FORM_CANCEL_MODAL_CANCEL} + + + + + {RULE_FORM_CANCEL_MODAL_CONFIRM} + + + + + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx index 15c346266c922..a5b29ad3ab88b 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx @@ -17,8 +17,10 @@ import React, { createContext, useState } from 'react'; const initialRuleFormScreenContextState = { isConnectorsScreenVisible: false, isShowRequestScreenVisible: false, + isConfirmCancelScreenVisible: false, setIsConnectorsScreenVisible: (show: boolean) => {}, setIsShowRequestScreenVisible: (show: boolean) => {}, + setIsConfirmCancelScreenVisible: (show: boolean) => {}, }; export const RuleFormScreenContext = createContext(initialRuleFormScreenContextState); @@ -26,13 +28,16 @@ export const RuleFormScreenContext = createContext(initialRuleFormScreenContextS export const RuleFormScreenContextProvider: React.FC = ({ children }) => { const [isConnectorsScreenVisible, setIsConnectorsScreenVisible] = useState(false); const [isShowRequestScreenVisible, setIsShowRequestScreenVisible] = useState(false); + const [isConfirmCancelScreenVisible, setIsConfirmCancelScreenVisible] = useState(false); return ( {children} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx index 0ac6cc86d2549..606ffe32f58e7 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx @@ -7,7 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useReducer } from 'react'; +import React, { useReducer, useRef, useEffect } from 'react'; +import { isEqual } from 'lodash'; import { RuleFormState } from '../types'; import { RuleFormStateContext, RuleFormReducerContext } from './rule_form_state_context'; import { ruleFormStateReducer } from './rule_form_state_reducer'; @@ -15,12 +16,13 @@ import { validateRuleBase, validateRuleParams } from '../validation'; export interface RuleFormStateProviderProps { initialRuleFormState: RuleFormState; + isFlyout?: boolean; } export const RuleFormStateProvider: React.FC< React.PropsWithChildren > = (props) => { - const { children, initialRuleFormState } = props; + const { children, initialRuleFormState, isFlyout } = props; const { formData, selectedRuleTypeModel: ruleTypeModel, @@ -38,8 +40,25 @@ export const RuleFormStateProvider: React.FC< ruleTypeModel, }), }); + + // Rule form state `touched` tends to get reset improperly in the flyout when the screen is re-rendered, + // so track it based on the first returned value of the form data + const originalFormData = useRef(); + useEffect(() => { + if (!isFlyout) return; + if (!originalFormData.current && ruleFormState.formData && ruleFormState.touched === false) { + originalFormData.current = ruleFormState.formData; + } + }, [ruleFormState.formData, ruleFormState.touched, isFlyout]); + + const touched = isFlyout + ? ruleFormState.touched + : Boolean( + ruleFormState.touched || + (originalFormData.current && !isEqual(originalFormData.current, ruleFormState.formData)) + ); return ( - + {children} ); 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 91cac8e9e99f2..ef0479338eb69 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 @@ -561,6 +561,21 @@ export const CONFIRM_RULE_SAVE_MESSAGE_TEXT = i18n.translate( } ); +export const RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TITLE = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyout.noActionsCalloutTitle', + { + defaultMessage: 'No actions configured', + } +); + +export const RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyout.noActionsCalloutText', + { + defaultMessage: + 'This rule will be saved with no actions configured. You can add an action later at any time.', + } +); + export const RULE_FORM_PAGE_RULE_DEFINITION_TITLE = i18n.translate( 'responseOpsRuleForm.ruleForm.ruleDefinitionTitle', { @@ -638,7 +653,7 @@ export const RULE_FORM_CANCEL_MODAL_CONFIRM = i18n.translate( export const RULE_FORM_CANCEL_MODAL_CANCEL = i18n.translate( 'responseOpsRuleForm.ruleForm.ruleFormCancelModalCancel', { - defaultMessage: 'Cancel', + defaultMessage: 'Keep editing', } ); diff --git a/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx b/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx index db2bbb7110704..34c2cdd816ac8 100644 --- a/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx +++ b/x-pack/platform/plugins/private/monitoring/public/alerts/configuration.tsx @@ -93,7 +93,7 @@ export const AlertConfiguration: React.FC = (props: Props) => { () => showFlyout && Legacy.shims.triggersActionsUi.getRuleFormFlyout({ - plugins: services, + services, id: alert.id, onSubmit: onClose, onCancel: onClose, diff --git a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx index a30a75ca606e1..7852532fb36a9 100644 --- a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx @@ -45,7 +45,7 @@ export const MlAnomalyAlertFlyout: FC = ({ if (!triggersActionsUi) return; const commonProps = { - plugins: services, + services, onCancel: () => { onCloseFlyout(); }, From 2d61517c3dc2eb2933c847caac4c699e89e715b1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 15:10:00 -0600 Subject: [PATCH 12/62] Fix action UI display papercuts --- .../src/rule_actions/rule_actions_item.tsx | 74 +++++++++---------- .../rule_actions_system_actions_item.tsx | 51 +++++++------ .../src/rule_definition/rule_schedule.tsx | 2 +- 3 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_item.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_item.tsx index af3f98dc51277..c97f5b9b59e29 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_item.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_item.tsx @@ -584,35 +584,24 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { ) : ( - + + + )} ); - }, [connector, showActionGroupErrorIcon, actionTypeModel]); + }, [connector, showActionGroupErrorIcon, actionType?.name, actionTypeModel.iconClass]); const connectorTitle = useMemo(() => { const title = connector ? ACTION_TITLE(connector) : actionTypeModel.actionTypeTitle; return ( - - {title} + + {title} ); }, [connector, actionTypeModel]); - const actionTypeTitle = useMemo(() => { - if (!connector || !actionType) { - return null; - } - return ( - - - {actionType.name} - - - ); - }, [connector, actionType]); - const runWhenTitle = useMemo(() => { if (!connector) { return null; @@ -623,11 +612,15 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { if (selectedActionGroup || action.frequency?.summary) { return ( - - {action.frequency?.summary - ? SUMMARY_GROUP_TITLE - : RUN_WHEN_GROUP_TITLE(selectedActionGroup!.name.toLocaleLowerCase())} - + + + ); } @@ -643,9 +636,9 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { if (warning) { return ( - - {ACTION_WARNING_TITLE} - + + + ); } @@ -694,20 +687,23 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => { {accordionIcon} - {connectorTitle} - {actionTypeTitle} - {runWhenTitle} - {warningIcon} - {actionTypeModel.isExperimental && ( - - - - )} + + {connectorTitle} + {actionTypeModel.isExperimental && ( + + + + )} + + + {runWhenTitle} + {warningIcon} + } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.tsx index 01a4385a6bfde..7c8dc83eefec7 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.tsx @@ -283,35 +283,38 @@ export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItem ) : ( - + + + )} - - {connector.name} - - - - {actionType?.name} - - - {warning && !isOpen && ( + - - {ACTION_WARNING_TITLE} - + {connector.name} - )} - {actionTypeModel.isExperimental && ( - - - - )} + {actionTypeModel.isExperimental && ( + + + + )} + + + {warning && !isOpen && ( + + + + + + )} + } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_schedule.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_schedule.tsx index a70b50effb60c..8e1f061a7697c 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_schedule.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_definition/rule_schedule.tsx @@ -121,7 +121,7 @@ export const RuleSchedule = () => { isInvalid={hasIntervalError} error={baseErrors?.interval} > - + Date: Tue, 14 Jan 2025 21:44:38 +0000 Subject: [PATCH 13/62] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../observability/plugins/slo/public/pages/slos/slos.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx index f694e72995196..03197fddb3101 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx @@ -124,7 +124,7 @@ const mockKibana = () => { storage: { get: () => {}, }, - triggersActionsUi: { mockGetRuleFormFlyout: mockGetRuleFormFlyout }, + triggersActionsUi: { mockGetRuleFormFlyout }, uiSettings: { get: (settings: string) => { if (settings === 'dateFormat') return 'YYYY-MM-DD'; From 19f2fdcaa656cd746d71363f9cd35288ec37b9fc Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 16:19:11 -0600 Subject: [PATCH 14/62] Fix typecheck --- .../shared/triggers_actions_ui/public/mocks.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts index b927c6023fd79..66cd4ecb4894b 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts @@ -9,6 +9,7 @@ import { RuleAction } from '@kbn/alerting-plugin/common'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry'; import { uiSettingsServiceMock } from '@kbn/core/public/mocks'; +import { RuleFormProps } from '@kbn/response-ops-rule-form'; import { getAlertsTableDefaultAlertActionsLazy } from './common/get_alerts_table_default_row_actions'; import type { TriggersAndActionsUIPublicPluginStart } from './plugin'; @@ -88,10 +89,12 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleFormFlyout: (props) => { return getRuleFormFlyoutLazy({ ...props, - actionTypeRegistry, - ruleTypeRegistry, - connectorServices, - }); + services: { + ...props.services, + actionTypeRegistry, + ruleTypeRegistry, + }, + } as RuleFormProps); }, getAlertsStateTable: (props: AlertsTableStateProps) => { return getAlertsTableStateLazy(props); From 3e9ebe4d870fc29acea4e1ffa90e73f56f971bf2 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 14 Jan 2025 16:48:34 -0600 Subject: [PATCH 15/62] Fix bundle size --- .../use_load_rule_type_aad_template_fields.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_aad_template_fields.ts b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_aad_template_fields.ts index 90c98d3c8153d..67754cb1ce943 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_aad_template_fields.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/common/hooks/use_load_rule_type_aad_template_fields.ts @@ -7,7 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EcsFlat } from '@elastic/ecs'; +import { useRef } from 'react'; +import { isEmpty } from 'lodash'; import { ActionVariable } from '@kbn/alerting-types'; import type { HttpStart } from '@kbn/core-http-browser'; import { useQuery } from '@tanstack/react-query'; @@ -24,12 +25,24 @@ export interface UseLoadRuleTypeAadTemplateFieldProps { } export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplateFieldProps) => { + const ecsFlat = useRef>({}); const { http, ruleTypeId, enabled, cacheTime } = props; const queryFn = async () => { if (!ruleTypeId) { return; } + if (isEmpty(ecsFlat.current)) { + // If we use import { EcsFlat } from '@elastic/ecs' then it will sometimes balloon the bundle size + // by about 1MB, or not, depending on how Webpack is feeling today. If you delete this dynamic import, + // it's possible that it won't make the bundle size explode, and then you will commit that change, and + // we will all continue to live our lives safe and secure, falsely believing that the danger has passed, + // until one day somebody makes another change to this package and, for mysterious, unknowable reasons, + // Webpack decides that today the bundle size shall be engorged once again. + // To avoid all that maybe just don't delete this dynamic import. + ecsFlat.current = await import('@elastic/ecs').then((ecs) => ecs.EcsFlat); + } + return fetchRuleTypeAadTemplateFields({ http, ruleTypeId }); }; @@ -44,7 +57,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat select: (dataViewFields) => { return dataViewFields?.map((d) => ({ name: d.name, - description: getDescription(d.name, EcsFlat), + description: getDescription(d.name, ecsFlat.current), })); }, cacheTime, From 70d4dab1efe38b834ca3955cfa450b87696dd524 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 16 Jan 2025 11:55:57 -0600 Subject: [PATCH 16/62] Fix typecheck --- .../triggers_actions_ui_example/public/application.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index 638233dd47a88..5c22d7ea5c1e2 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -188,7 +188,7 @@ const TriggersActionsUiExampleApp = ({ render={() => ( ( Date: Thu, 16 Jan 2025 12:06:50 -0600 Subject: [PATCH 17/62] Lazy load rule form component from response ops package --- .../public/common/get_rule_form_flyout.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index 82db10d29af2c..65ce2bbe4349d 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -5,10 +5,30 @@ * 2.0. */ -import React from 'react'; -import { RuleForm, RuleFormProps } from '@kbn/response-ops-rule-form'; +import type { RuleFormProps } from '@kbn/response-ops-rule-form'; +import React, { useState } from 'react'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; import type { RuleTypeMetaData } from '../types'; +const RuleForm = ( + props: RuleFormProps +) => { + const [Component, setComponent] = useState> | null>( + null + ); + useEffectOnce(() => { + (async () => { + const { RuleForm: RuleFormComponent } = await import('@kbn/response-ops-rule-form'); + setComponent(RuleFormComponent); + })(); + }); + + if (!Component) { + return null; + } + return ; +}; + export const getRuleFormFlyoutLazy = ( props: RuleFormProps ) => { From 547c4dae733fc9afb23d9ae96737f3efb968fd51 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 16 Jan 2025 12:33:04 -0600 Subject: [PATCH 18/62] Fix lazy loading rule form --- .../public/common/get_rule_form_flyout.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index 65ce2bbe4349d..5b71010e9f41b 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -5,32 +5,29 @@ * 2.0. */ +import { EuiPortal, EuiOverlayMask, EuiLoadingSpinner } from '@elastic/eui'; import type { RuleFormProps } from '@kbn/response-ops-rule-form'; -import React, { useState } from 'react'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; +import React, { Suspense, lazy } from 'react'; import type { RuleTypeMetaData } from '../types'; -const RuleForm = ( +export const getRuleFormFlyoutLazy = ( props: RuleFormProps ) => { - const [Component, setComponent] = useState> | null>( - null + const RuleForm: React.LazyExoticComponent>> = lazy(() => + import('@kbn/response-ops-rule-form').then((module) => ({ default: module.RuleForm })) ); - useEffectOnce(() => { - (async () => { - const { RuleForm: RuleFormComponent } = await import('@kbn/response-ops-rule-form'); - setComponent(RuleFormComponent); - })(); - }); - - if (!Component) { - return null; - } - return ; -}; -export const getRuleFormFlyoutLazy = ( - props: RuleFormProps -) => { - return ; + return ( + + + + +
+ } + > + + + ); }; From c2174d623eb809bfb584989cd578a74bde944933 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 16 Jan 2025 16:03:24 -0600 Subject: [PATCH 19/62] Begin fixing discover tests --- .../src/hooks/use_rule_form_steps.tsx | 3 ++ .../rule_flyout/rule_flyout_edit_footer.tsx | 4 +- .../default_status_alert.journey.ts | 2 +- .../discover/search_source_alert.ts | 27 +++++++----- .../stack_alerting/es_query_rule.ts | 2 +- .../discover/search_source_alert.ts | 41 ++++++++++--------- .../test_suites/search/rules/rule_details.ts | 24 +++++++---- 7 files changed, 62 insertions(+), 41 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx index 12e724a670fa7..4c041c69faee9 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx @@ -153,12 +153,14 @@ const useCommonRuleFormSteps = ({ : RULE_FORM_PAGE_RULE_DEFINITION_TITLE, status: ruleDefinitionStatus, children: , + 'data-test-subj': 'ruleFormStep-definition', }, [RuleFormStepId.ACTIONS]: canReadConnectors ? { title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE, status: actionsStatus, children: , + 'data-test-subj': 'ruleFormStep-actions', } : null, [RuleFormStepId.DETAILS]: { @@ -167,6 +169,7 @@ const useCommonRuleFormSteps = ({ : RULE_FORM_PAGE_RULE_DETAILS_TITLE, status: ruleDetailsStatus, children: , + 'data-test-subj': 'ruleFormStep-details', }, }), [ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus, shortTitles] diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx index 5cb64b40fb180..59a43f7ee7cf7 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_edit_footer.tsx @@ -39,7 +39,7 @@ export const RuleFlyoutEditFooter = ({ - + {RULE_FLYOUT_FOOTER_CANCEL_TEXT} @@ -60,7 +60,7 @@ export const RuleFlyoutEditFooter = ({ { await page.waitForSelector('text=Monitor status rule'); await page.getByTestId('intervalInputUnit').selectOption('second'); await page.getByTestId('intervalInput').fill('20'); - await page.click(byTestId('saveEditedRuleButton')); + await page.click(byTestId('ruleFlyoutFooterCancelButton')); await page.waitForSelector("text=Updated 'Synthetics status internal rule'"); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts index 3ce3ea7ba12b6..e1102aeb2c17c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts @@ -163,17 +163,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { supertest.delete(`/api/actions/connector/${id}`).set('kbn-xsrf', 'foo').expect(204, ''); const defineSearchSourceAlert = async (alertName: string) => { - await retry.waitFor('rule name value is correct', async () => { - await testSubjects.setValue('ruleNameInput', alertName); - const ruleName = await testSubjects.getAttribute('ruleNameInput', 'value'); - return ruleName === alertName; - }); await testSubjects.click('thresholdPopover'); await testSubjects.setValue('alertThresholdInput0', '1'); await testSubjects.click('forLastExpression'); await testSubjects.setValue('timeWindowSizeNumber', '30'); + await testSubjects.click('ruleFormStep-actions'); await retry.waitFor('actions accordion to exist', async () => { await testSubjects.click('.index-alerting-ActionTypeSelectOption'); return await testSubjects.exists('alertActionAccordion-0'); @@ -186,6 +182,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { "alert_id": "{{alert.id}}", "context_link": "{{context.link}}" }`); + + await retry.waitFor('rule name value is correct', async () => { + await testSubjects.click('ruleFormStep-details'); + + await testSubjects.setValue('ruleDetailsNameInput', alertName); + const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); + return ruleName === alertName; + }); }; const openDiscoverAlertFlyout = async () => { @@ -420,7 +424,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return (await dataViewSelector.getVisibleText()) === 'DATA VIEW\nsearch-source-alert-o*'; }); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); const errorElem = await testSubjects.find('esQueryAlertExpressionError'); const errorText = await errorElem.getVisibleText(); @@ -446,9 +451,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return (await dataViewSelector.getVisibleText()) === `DATA VIEW\n${SOURCE_DATA_VIEW}`; }); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); await retry.try(async () => { - await testSubjects.missingOrFail('saveRuleButton'); + await testSubjects.missingOrFail('ruleFlyoutFooterSaveButton'); }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -550,9 +556,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // create an alert await openDiscoverAlertFlyout(); await defineSearchSourceAlert('test-adhoc-alert'); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); await retry.try(async () => { - await testSubjects.missingOrFail('saveRuleButton'); + await testSubjects.missingOrFail('ruleFlyoutFooterSaveButton'); }); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/es_query_rule.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/es_query_rule.ts index 3e1bf9093d924..d14d7c49637e6 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/es_query_rule.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_alerting/es_query_rule.ts @@ -137,7 +137,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await pageObjects.header.waitUntilLoadingHasFinished(); await commonScreenshots.takeScreenshot('cases-action', screenshotDirectories, 1400, 1024); - const cancelEditButton = await testSubjects.find('cancelSaveEditedRuleButton'); + const cancelEditButton = await testSubjects.find('ruleFlyoutFooterCancelButton'); await cancelEditButton.click(); const confirmCancelButton = await testSubjects.find('confirmModalConfirmButton'); await confirmCancelButton.click(); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index fe2fb08501450..e94ba739dd838 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -195,17 +195,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { .expect(204, ''); const defineSearchSourceAlert = async (alertName: string) => { - await retry.waitFor('rule name value is correct', async () => { - await testSubjects.setValue('ruleNameInput', alertName); - const ruleName = await testSubjects.getAttribute('ruleNameInput', 'value'); - return ruleName === alertName; - }); await testSubjects.click('thresholdPopover'); await testSubjects.setValue('alertThresholdInput0', '1'); await testSubjects.click('forLastExpression'); await testSubjects.setValue('timeWindowSizeNumber', '30'); + await testSubjects.click('ruleFormStep-actions'); await retry.waitFor('actions accordion to exist', async () => { await testSubjects.click('.index-alerting-ActionTypeSelectOption'); return await testSubjects.exists('alertActionAccordion-0'); @@ -218,6 +214,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { "alert_id": "{{alert.id}}", "context_link": "{{context.link}}" }`); + + await testSubjects.click('ruleFormStep-details'); + await retry.waitFor('rule name value is correct', async () => { + await testSubjects.setValue('ruleDetailsNameInput', alertName); + const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); + return ruleName === alertName; + }); }; const openDiscoverAlertFlyout = async () => { @@ -458,7 +461,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return (await dataViewSelector.getVisibleText()) === 'DATA VIEW\nsearch-source-alert-o*'; }); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); const errorElem = await testSubjects.find('esQueryAlertExpressionError'); const errorText = await errorElem.getVisibleText(); @@ -484,9 +488,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return (await dataViewSelector.getVisibleText()) === `DATA VIEW\n${SOURCE_DATA_VIEW}`; }); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); await retry.try(async () => { - await testSubjects.missingOrFail('saveRuleButton'); + await testSubjects.missingOrFail('ruleFlyoutFooterSaveButton'); }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -594,6 +599,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // create an alert await openDiscoverAlertFlyout(); await defineSearchSourceAlert('test-adhoc-alert'); + await testSubjects.click('ruleFormStep-details'); await testSubjects.click('saveRuleButton'); await retry.try(async () => { await testSubjects.missingOrFail('saveRuleButton'); @@ -670,15 +676,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('rule name value is correct', async () => { - let ruleName; - // Rule name input is different in serverless - if (await testSubjects.exists('ruleNameInput')) { - await testSubjects.setValue('ruleNameInput', newAlert); - ruleName = await testSubjects.getAttribute('ruleNameInput', 'value'); - } else { - await testSubjects.setValue('ruleDetailsNameInput', newAlert); - ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); - } + await testSubjects.click('ruleFormStep-details'); + + await testSubjects.setValue('ruleDetailsNameInput', newAlert); + const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); + return ruleName === newAlert; }); @@ -702,9 +704,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await comboBox.set('ruleFormConsumerSelect', 'Stack Rules'); } + await testSubjects.click('ruleFormStep-details'); // Save rule button is different in serverless - if (await testSubjects.exists('saveRuleButton')) { - await testSubjects.click('saveRuleButton'); + if (await testSubjects.exists('ruleFlyoutFooterSaveButton')) { + await testSubjects.click('ruleFlyoutFooterSaveButton'); } else { await testSubjects.click('rulePageFooterSaveButton'); } diff --git a/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts b/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts index 00363f21299de..4cf5664c35148 100644 --- a/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts +++ b/x-pack/test_serverless/functional/test_suites/search/rules/rule_details.ts @@ -292,11 +292,14 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await editButton.click(); expect(await testSubjects.exists('hasActionsDisabled')).toBe(false); - await testSubjects.setValue('ruleNameInput', updatedRuleName, { + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', updatedRuleName, { clearWithKeyboard: true, }); - await find.clickByCssSelector('[data-test-subj="saveEditedRuleButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="ruleFlyoutFooterSaveButton"]:not(disabled)' + ); await retry.try(async () => { const resultToast = await toasts.getElementByIndex(1); @@ -314,18 +317,20 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const editButton = await testSubjects.find('openEditRuleFlyoutButton'); await editButton.click(); - await testSubjects.setValue('ruleNameInput', uuidv4(), { + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', uuidv4(), { clearWithKeyboard: true, }); - await testSubjects.click('cancelSaveEditedRuleButton'); + await testSubjects.click('ruleFlyoutFooterCancelButton'); await testSubjects.existOrFail('confirmRuleCloseModal'); await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton'); - await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedRuleButton"]'); + await find.waitForDeletedByCssSelector('[data-test-subj="ruleFlyoutFooterCancelButton"]'); await editButton.click(); - const nameInputAfterCancel = await testSubjects.find('ruleNameInput'); + await testSubjects.click('ruleFormStep-details'); + const nameInputAfterCancel = await testSubjects.find('ruleDetailsNameInput'); const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); expect(textAfterCancel).toEqual(updatedRuleName); }); @@ -612,11 +617,14 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const throttleUnitInput = await testSubjects.find('throttleUnitInput'); expect(await throttleInput.getAttribute('value')).toEqual('2'); expect(await throttleUnitInput.getAttribute('value')).toEqual('d'); - await testSubjects.setValue('ruleNameInput', updatedRuleName, { + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', updatedRuleName, { clearWithKeyboard: true, }); - await find.clickByCssSelector('[data-test-subj="saveEditedRuleButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="ruleFlyoutFooterSaveButton"]:not(disabled)' + ); await retry.try(async () => { const resultToast = await toasts.getElementByIndex(1); From 4719fafb7c831e253f9e421e5552f654a57a62aa Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 16 Jan 2025 16:38:46 -0600 Subject: [PATCH 20/62] Fix button ids on discover test --- .../rule_actions/rule_actions_connectors_body.tsx | 1 + .../discover/search_source_alert.ts | 10 +++++++--- .../discover/search_source_alert.ts | 13 +++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx index a454856864fec..f232c8dcb9dee 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx @@ -365,6 +365,7 @@ export const RuleActionsConnectorsBody = ({ const connectorCard = ( { - await testSubjects.click('.index-alerting-ActionTypeSelectOption'); - return await testSubjects.exists('alertActionAccordion-0'); + await retry.waitFor('actions button to exist', async () => { + await testSubjects.click('ruleActionsAddActionButton'); + await find.clickByCssSelector('[data-action-type-id=".index"]'); + return (await testSubjects.findAll('ruleActionsItem')).length === 1; }); await monacoEditor.waitCodeEditorReady('kibanaCodeEditor'); @@ -190,6 +191,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); return ruleName === alertName; }); + + await testSubjects.click('ruleFormStep-definition'); }; const openDiscoverAlertFlyout = async () => { @@ -427,6 +430,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('ruleFormStep-details'); await testSubjects.click('ruleFlyoutFooterSaveButton'); + await testSubjects.click('ruleFormStep-definition'); const errorElem = await testSubjects.find('esQueryAlertExpressionError'); const errorText = await errorElem.getVisibleText(); expect(errorText).to.eql('Data view should have a time field.'); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index e94ba739dd838..b18f1e0162323 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -202,9 +202,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.setValue('timeWindowSizeNumber', '30'); await testSubjects.click('ruleFormStep-actions'); - await retry.waitFor('actions accordion to exist', async () => { - await testSubjects.click('.index-alerting-ActionTypeSelectOption'); - return await testSubjects.exists('alertActionAccordion-0'); + await retry.waitFor('actions button to exist', async () => { + await testSubjects.click('ruleActionsAddActionButton'); + await find.clickByCssSelector('[data-action-type-id=".index"]'); + return (await testSubjects.findAll('ruleActionsItem')).length === 1; }); await monacoEditor.waitCodeEditorReady('kibanaCodeEditor'); @@ -215,12 +216,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { "context_link": "{{context.link}}" }`); - await testSubjects.click('ruleFormStep-details'); await retry.waitFor('rule name value is correct', async () => { + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', alertName); const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); return ruleName === alertName; }); + + await testSubjects.click('ruleFormStep-definition'); }; const openDiscoverAlertFlyout = async () => { @@ -464,6 +468,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('ruleFormStep-details'); await testSubjects.click('ruleFlyoutFooterSaveButton'); + await testSubjects.click('ruleFormStep-definition'); const errorElem = await testSubjects.find('esQueryAlertExpressionError'); const errorText = await errorElem.getVisibleText(); expect(errorText).to.eql('Data view should have a time field.'); From 51e33f9f441de9d8bcd2b262051b258c2c5b98f9 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 17 Jan 2025 11:44:43 -0600 Subject: [PATCH 21/62] Fix system actions test --- .../src/rule_actions/rule_actions_system_actions_item.test.tsx | 1 - .../pages/alerts/custom_threshold_preview_chart.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.test.tsx index f6e76538eba97..e61cdd32ff8a3 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_system_actions_item.test.tsx @@ -135,7 +135,6 @@ describe('ruleActionsSystemActionsItem', () => { expect(screen.getByTestId('ruleActionsSystemActionsItem')).toBeInTheDocument(); expect(screen.getByText('connector-1')).toBeInTheDocument(); - expect(screen.getByText('actionType: 1')).toBeInTheDocument(); expect(screen.getByTestId('ruleActionsSystemActionsItemAccordionContent')).toBeVisible(); expect(screen.getByText('RuleActionsMessage')).toBeInTheDocument(); diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts index 6bc0564c711c1..188e37d237d90 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts @@ -51,7 +51,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); it('does render the correct error message', async () => { - await testSubjects.setValue('ruleNameInput', 'test custom threshold rule'); + await testSubjects.setValue('ruleDetailsNameInput', 'test custom threshold rule'); await testSubjects.click('customEquation'); const customEquationField = await find.byCssSelector( From c25c2836fd2078c3dcac94a4f905de88074c4271 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 17 Jan 2025 12:18:38 -0600 Subject: [PATCH 22/62] Fix get app menu header id --- .../observability/discover/context_awareness/_get_app_menu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts index fdbc0e6c110a0..fb78638c77ced 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/discover/context_awareness/_get_app_menu.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const createRuleButton = await testSubjects.find('discoverAppMenuCustomThresholdRule'); await createRuleButton.click(); - const ruleTitleElement = await testSubjects.find('selectedRuleTypeTitle'); + const ruleTitleElement = await testSubjects.find('ruleDefinitionHeaderRuleTypeName'); await retry.try(async () => { expect(await ruleTitleElement.getVisibleText()).to.equal('Custom threshold'); From dc8c9abe0d7fc86bc5eb9b9a8d3b10bf69f56f68 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 17 Jan 2025 12:27:47 -0600 Subject: [PATCH 23/62] Fix cancelAlertFlyout function in ML anomaly tests --- .../rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx | 2 +- .../observability/plugins/slo/public/pages/slos/slos.test.tsx | 2 +- x-pack/test/functional/services/uptime/ml_anomaly.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx index 7b0ca7307b29f..c8d75c7e65e92 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx @@ -55,7 +55,7 @@ export const RuleFlyoutConfirmCancel = ({ onBack, onConfirm }: RuleFlyoutShowReq fill color="danger" onClick={onConfirm} - data-test-subj="ruleFlyoutConfirmCancelConfigmButton" + data-test-subj="ruleFlyoutConfirmCancelConfirmButton" > {RULE_FORM_CANCEL_MODAL_CONFIRM} diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx index 03197fddb3101..1b0dca1d63253 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slos/slos.test.tsx @@ -124,7 +124,7 @@ const mockKibana = () => { storage: { get: () => {}, }, - triggersActionsUi: { mockGetRuleFormFlyout }, + triggersActionsUi: { getRuleFormFlyout: mockGetRuleFormFlyout }, uiSettings: { get: (settings: string) => { if (settings === 'dateFormat') return 'YYYY-MM-DD'; diff --git a/x-pack/test/functional/services/uptime/ml_anomaly.ts b/x-pack/test/functional/services/uptime/ml_anomaly.ts index 78cb466805417..9825245591c9e 100644 --- a/x-pack/test/functional/services/uptime/ml_anomaly.ts +++ b/x-pack/test/functional/services/uptime/ml_anomaly.ts @@ -31,6 +31,8 @@ export function UptimeMLAnomalyProvider({ getService }: FtrProviderContext) { async cancelAlertFlyout() { if (await testSubjects.exists('euiFlyoutCloseButton')) await testSubjects.click('euiFlyoutCloseButton', 60 * 1000); + if (await testSubjects.exists('ruleFlyoutConfirmCancelConfirmButton')) + await testSubjects.click('ruleFlyoutConfirmCancelConfirmButton', 60 * 1000); }, async alreadyHasJob() { From 114e98ae23aae3a396ab3496c0a79b46f5f5bae0 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 17 Jan 2025 12:30:33 -0600 Subject: [PATCH 24/62] Fix monitor alerts journey for closing rule form flyout --- .../journeys/monitor_details/monitor_alerts.journey.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts index 6e5ed114d72de..415f69b508797 100644 --- a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts +++ b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts @@ -69,7 +69,14 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = }); step('close anomaly detection flyout', async () => { - await page.click(byTestId('cancelSaveRuleButton')); + await page.click(byTestId('ruleFlyoutFooterCancelButton')); + if ( + await page.waitForSelector(byTestId('ruleFlyoutConfirmCancelConfirmButton'), { + timeout: 1000, + }) + ) { + await page.click(byTestId('ruleFlyoutConfirmCancelConfirmButton')); + } }); step('open anomaly detection alert', async () => { From e3a0e7e933b3c4993b3dacf8b1762298667d9fb4 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 22 Jan 2025 13:03:04 -0600 Subject: [PATCH 25/62] Create /components folder for common components --- .../confirm_create_rule/confirm_create_rule.test.tsx} | 0 .../confirm_create_rule/confirm_create_rule.tsx} | 0 .../src/components/confirm_create_rule/index.ts | 10 ++++++++++ .../response-ops/rule_form/src/components/index.ts | 11 +++++++++++ .../src/{ => components}/request_code_block/index.ts | 0 .../request_code_block/request_code_block.tsx | 10 +++++----- .../src/rule_flyout/rule_flyout_show_request.tsx | 2 +- .../rule_form/src/rule_page/rule_page_footer.tsx | 2 +- .../src/rule_page/rule_page_show_request_modal.tsx | 2 +- 9 files changed, 29 insertions(+), 8 deletions(-) rename src/platform/packages/shared/response-ops/rule_form/src/{rule_page/rule_page_confirm_create_rule.test.tsx => components/confirm_create_rule/confirm_create_rule.test.tsx} (100%) rename src/platform/packages/shared/response-ops/rule_form/src/{rule_page/rule_page_confirm_create_rule.tsx => components/confirm_create_rule/confirm_create_rule.tsx} (100%) create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/index.ts create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/components/index.ts rename src/platform/packages/shared/response-ops/rule_form/src/{ => components}/request_code_block/index.ts (100%) rename src/platform/packages/shared/response-ops/rule_form/src/{ => components}/request_code_block/request_code_block.tsx (89%) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_confirm_create_rule.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx similarity index 100% rename from src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_confirm_create_rule.test.tsx rename to src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_confirm_create_rule.tsx b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx similarity index 100% rename from src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_confirm_create_rule.tsx rename to src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/index.ts new file mode 100644 index 0000000000000..123d453215d13 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/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 './rule_page_confirm_create_rule'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts new file mode 100644 index 0000000000000..c6ca884507daa --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 './request_code_block'; +export * from './confirm_create_rule'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/request_code_block/index.ts similarity index 100% rename from src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts rename to src/platform/packages/shared/response-ops/rule_form/src/components/request_code_block/index.ts diff --git a/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx b/src/platform/packages/shared/response-ops/rule_form/src/components/request_code_block/request_code_block.tsx similarity index 89% rename from src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx rename to src/platform/packages/shared/response-ops/rule_form/src/components/request_code_block/request_code_block.tsx index ff229564cc281..ea9430fab2873 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/request_code_block/request_code_block.tsx @@ -16,11 +16,11 @@ import { UpdateRuleBody, transformCreateRuleBody, transformUpdateRuleBody, -} from '../common/apis'; -import { BASE_ALERTING_API_PATH } from '../constants'; -import { useRuleFormState } from '../hooks'; -import { SHOW_REQUEST_MODAL_ERROR } from '../translations'; -import { RuleFormData } from '../types'; +} from '../../common/apis'; +import { BASE_ALERTING_API_PATH } from '../../constants'; +import { useRuleFormState } from '../../hooks'; +import { SHOW_REQUEST_MODAL_ERROR } from '../../translations'; +import { RuleFormData } from '../../types'; const stringifyBodyRequest = ({ formData, diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx index fa6c14f996316..b5cf0682f42df 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx @@ -26,7 +26,7 @@ import { RULE_FLYOUT_FOOTER_BACK_TEXT, RULE_FLYOUT_HEADER_BACK_TEXT, } from '../translations'; -import { RequestCodeBlock } from '../request_code_block'; +import { RequestCodeBlock } from '../components'; interface RuleFlyoutShowRequestProps { isEdit: boolean; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx index 375d4c320c205..6228685421750 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx @@ -17,7 +17,7 @@ import { } from '../translations'; import { useRuleFormScreenContext, useRuleFormState } from '../hooks'; import { hasRuleErrors } from '../validation'; -import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule'; +import { RulePageConfirmCreateRule } from '../components'; export interface RulePageFooterProps { isEdit?: boolean; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx index b9adc3ca4ead1..4033e878756f3 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx @@ -18,7 +18,7 @@ import { EuiTextColor, } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { RequestCodeBlock } from '../request_code_block'; +import { RequestCodeBlock } from '../components'; import { SHOW_REQUEST_MODAL_SUBTITLE, SHOW_REQUEST_MODAL_TITLE } from '../translations'; import { useRuleFormScreenContext } from '../hooks'; From dbc9712479ee7cfb6f5e3638ad783c5a466e38a2 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 22 Jan 2025 13:09:57 -0600 Subject: [PATCH 26/62] Move confirm close modal to own component --- .../confirm_create_rule.test.tsx | 49 ------------------- .../confirm_create_rule.tsx | 8 +-- .../components/confirm_create_rule/index.ts | 2 +- .../confirm_rule_close/confirm_rule_close.tsx | 43 ++++++++++++++++ .../components/confirm_rule_close/index.ts | 10 ++++ .../rule_form/src/components/index.ts | 1 + .../rule_form/src/rule_page/rule_page.tsx | 24 ++------- .../src/rule_page/rule_page_footer.tsx | 7 +-- 8 files changed, 64 insertions(+), 80 deletions(-) delete mode 100644 src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/components/confirm_rule_close/confirm_rule_close.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/components/confirm_rule_close/index.ts diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx deleted file mode 100644 index 6c4a428989e9f..0000000000000 --- a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.test.tsx +++ /dev/null @@ -1,49 +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", 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 { fireEvent, render, screen } from '@testing-library/react'; -import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule'; -import { - CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT, - CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT, - CONFIRM_RULE_SAVE_MESSAGE_TEXT, -} from '../translations'; - -const onConfirmMock = jest.fn(); -const onCancelMock = jest.fn(); - -describe('rulePageConfirmCreateRule', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - test('renders correctly', () => { - render(); - - expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument(); - expect(screen.getByText(CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT)).toBeInTheDocument(); - expect(screen.getByText(CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT)).toBeInTheDocument(); - expect(screen.getByText(CONFIRM_RULE_SAVE_MESSAGE_TEXT)).toBeInTheDocument(); - }); - - test('can confirm rule creation', () => { - render(); - - fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); - expect(onConfirmMock).toHaveBeenCalled(); - }); - - test('can cancel rule creation', () => { - render(); - - fireEvent.click(screen.getByTestId('confirmModalCancelButton')); - expect(onCancelMock).toHaveBeenCalled(); - }); -}); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx index 497936fd1094f..f7cb270c3ac3e 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_create_rule/confirm_create_rule.tsx @@ -14,19 +14,19 @@ import { CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT, CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT, CONFIRM_RULE_SAVE_MESSAGE_TEXT, -} from '../translations'; +} from '../../translations'; -export interface RulePageConfirmCreateRuleProps { +export interface ConfirmCreateRuleProps { onCancel: () => void; onConfirm: () => void; } -export const RulePageConfirmCreateRule = (props: RulePageConfirmCreateRuleProps) => { +export const ConfirmCreateRule = (props: ConfirmCreateRuleProps) => { const { onCancel, onConfirm } = props; return ( void; + onConfirm: () => void; +} + +export const ConfirmRuleClose = (props: ConfirmRuleCloseRuleProps) => { + const { onCancel, onConfirm } = props; + + return ( + + +

{RULE_FORM_CANCEL_MODAL_DESCRIPTION}

+
+
+ ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_rule_close/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_rule_close/index.ts new file mode 100644 index 0000000000000..a313092bed805 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/confirm_rule_close/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 './confirm_rule_close'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts index c6ca884507daa..e7b78c3b19bce 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts @@ -9,3 +9,4 @@ export * from './request_code_block'; export * from './confirm_create_rule'; +export * from './confirm_cancel_create_rule'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx index e37c8610683be..b3321b5d223c0 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx @@ -10,7 +10,6 @@ import { EuiButtonEmpty, EuiCallOut, - EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiPageTemplate, @@ -20,19 +19,13 @@ import { import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; import React, { useCallback, useMemo, useState } from 'react'; import { useRuleFormScreenContext, useRuleFormState, useRuleFormSteps } from '../hooks'; -import { - DISABLED_ACTIONS_WARNING_TITLE, - RULE_FORM_CANCEL_MODAL_CANCEL, - RULE_FORM_CANCEL_MODAL_CONFIRM, - RULE_FORM_CANCEL_MODAL_DESCRIPTION, - RULE_FORM_CANCEL_MODAL_TITLE, - RULE_FORM_RETURN_TITLE, -} from '../translations'; +import { DISABLED_ACTIONS_WARNING_TITLE, RULE_FORM_RETURN_TITLE } from '../translations'; import type { RuleFormData } from '../types'; import { RulePageFooter } from './rule_page_footer'; import { RulePageNameInput } from './rule_page_name_input'; import { RuleActionsConnectorsModal } from '../rule_actions/rule_actions_connectors_modal'; import { RulePageShowRequestModal } from './rule_page_show_request_modal'; +import { ConfirmRuleClose } from '../components'; export interface RulePageProps { isEdit?: boolean; @@ -141,18 +134,7 @@ export const RulePage = (props: RulePageProps) => { {isCancelModalOpen && ( - setIsCancelModalOpen(false)} - onConfirm={onCancel} - data-test-subj="confirmRuleCloseModal" - buttonColor="danger" - defaultFocusedButton="confirm" - title={RULE_FORM_CANCEL_MODAL_TITLE} - confirmButtonText={RULE_FORM_CANCEL_MODAL_CONFIRM} - cancelButtonText={RULE_FORM_CANCEL_MODAL_CANCEL} - > -

{RULE_FORM_CANCEL_MODAL_DESCRIPTION}

-
+ setIsCancelModalOpen(false)} onConfirm={onCancel} /> )} {isConnectorsScreenVisible && } {isShowRequestScreenVisible && } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx index 6228685421750..d5c8d6c32ade3 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx @@ -17,7 +17,7 @@ import { } from '../translations'; import { useRuleFormScreenContext, useRuleFormState } from '../hooks'; import { hasRuleErrors } from '../validation'; -import { RulePageConfirmCreateRule } from '../components'; +import { ConfirmCreateRule } from '../components'; export interface RulePageFooterProps { isEdit?: boolean; @@ -131,10 +131,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
{showCreateConfirmation && ( - + )} ); From fd7abec6077a835b8a4ca55312068ef822bf47ca Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 22 Jan 2025 13:41:31 -0600 Subject: [PATCH 27/62] Replace flyout callouts and screens with confirm modals --- .../rule_form/src/components/index.ts | 2 +- .../src/rule_flyout/rule_flyout.test.tsx | 32 +++++++++ .../rule_form/src/rule_flyout/rule_flyout.tsx | 39 ++++++----- .../src/rule_flyout/rule_flyout_body.tsx | 63 ++++++++--------- .../rule_flyout_confirm_cancel.tsx | 67 ------------------- .../rule_form_screen_context.tsx | 5 -- .../src/rule_page/rule_page_footer.test.tsx | 4 +- .../rule_form/src/translations.ts | 15 ----- .../discover/search_source_alert.ts | 2 +- .../alert_create_flyout.ts | 16 ++--- .../page_objects/triggers_actions_ui_page.ts | 4 +- 11 files changed, 98 insertions(+), 151 deletions(-) delete mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx diff --git a/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts index e7b78c3b19bce..f1f737cf52e97 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/components/index.ts @@ -9,4 +9,4 @@ export * from './request_code_block'; export * from './confirm_create_rule'; -export * from './confirm_cancel_create_rule'; +export * from './confirm_rule_close'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx index 8525ba7b5a057..2dee8c39d0910 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx @@ -128,6 +128,9 @@ describe('ruleFlyout', () => { fireEvent.click(screen.getByTestId('ruleFlyoutFooterSaveButton')); + expect(await screen.findByTestId('confirmCreateRuleModal')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('confirmModalConfirmButton')); + expect(onSave).toHaveBeenCalledWith({ ...formDataMock, consumer: 'logs', @@ -140,4 +143,33 @@ describe('ruleFlyout', () => { fireEvent.click(screen.getByTestId('ruleFlyoutFooterCancelButton')); expect(onCancel).toHaveBeenCalled(); }); + + test('should display discard changes modal only if changes are made in the form', () => { + useRuleFormState.mockReturnValue({ + plugins: { + application: { + navigateToUrl, + capabilities: { + actions: { + show: true, + save: true, + execute: true, + }, + }, + }, + }, + baseErrors: {}, + paramsErrors: {}, + touched: true, + formData: formDataMock, + connectors: [], + connectorTypes: [], + aadTemplateFields: [], + }); + + render(); + + fireEvent.click(screen.getByTestId('ruleFlyoutFooterCancelButton')); + expect(screen.getByTestId('confirmRuleCloseModal')).toBeInTheDocument(); + }); }); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index 365b4eac31a76..b69ba60c35a49 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -15,7 +15,7 @@ import { RuleFlyoutBody } from './rule_flyout_body'; import { RuleFlyoutShowRequest } from './rule_flyout_show_request'; import { useRuleFormScreenContext, useRuleFormState } from '../hooks'; import { RuleFlyoutSelectConnector } from './rule_flyout_select_connector'; -import { RuleFlyoutConfirmCancel } from './rule_flyout_confirm_cancel'; +import { ConfirmRuleClose } from '../components'; interface RuleFlyoutProps { isEdit?: boolean; @@ -29,18 +29,20 @@ export const RuleFlyout = ({ onSave, isEdit = false, isSaving = false, - onCancel = () => {}, + // Input is named onCancel for consistency with RulePage but rename it to onClose for more clarity on its + // function within the flyout. This avoids the compulsion to name a function something like onCancelCancel when + // we're displaying the confirmation modal for closing the flyout. + onCancel: onClose = () => {}, onChangeMetaData = () => {}, }: RuleFlyoutProps) => { const [initialStep, setInitialStep] = useState(undefined); + const [isConfirmCloseModalVisible, setIsConfirmCloseModalVisible] = useState(false); const { isConnectorsScreenVisible, isShowRequestScreenVisible, - isConfirmCancelScreenVisible, setIsShowRequestScreenVisible, setIsConnectorsScreenVisible, - setIsConfirmCancelScreenVisible, } = useRuleFormScreenContext(); const onCloseConnectorsScreen = useCallback(() => { setInitialStep(RuleFormStepId.ACTIONS); @@ -56,46 +58,44 @@ export const RuleFlyout = ({ setIsShowRequestScreenVisible(false); }, [setIsShowRequestScreenVisible]); - const onCancelBack = useCallback(() => { - setIsConfirmCancelScreenVisible(false); - }, [setIsConfirmCancelScreenVisible]); + const onCancelClose = useCallback(() => { + setIsConfirmCloseModalVisible(false); + }, []); const hideCloseButton = useMemo( - () => isShowRequestScreenVisible || isConnectorsScreenVisible || isConfirmCancelScreenVisible, - [isConnectorsScreenVisible, isShowRequestScreenVisible, isConfirmCancelScreenVisible] + () => isShowRequestScreenVisible || isConnectorsScreenVisible, + [isConnectorsScreenVisible, isShowRequestScreenVisible] ); const { touched } = useRuleFormState(); - const onCancelInternal = useCallback(() => { + const onClickCloseOrCancelButton = useCallback(() => { if (touched) { - setIsConfirmCancelScreenVisible(true); + setIsConfirmCloseModalVisible(true); } else { - onCancel(); + onClose(); } - }, [touched, setIsConfirmCancelScreenVisible, onCancel]); + }, [touched, setIsConfirmCloseModalVisible, onClose]); return ( - {isConfirmCancelScreenVisible ? ( - - ) : isShowRequestScreenVisible ? ( + {isShowRequestScreenVisible ? ( ) : isConnectorsScreenVisible ? ( ) : ( )} + {isConfirmCloseModalVisible && ( + + )} ); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx index bcb4a1fc3ec01..d22f390e8b663 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx @@ -14,25 +14,23 @@ import { EuiSpacer, EuiStepsHorizontal, EuiTitle, - useEuiPaddingSize, } from '@elastic/eui'; import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; import { isEmpty } from 'lodash'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { RuleFormStepId } from '../constants'; import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks'; import { DISABLED_ACTIONS_WARNING_TITLE, RULE_FLYOUT_HEADER_CREATE_TITLE, RULE_FLYOUT_HEADER_EDIT_TITLE, - RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TITLE, - RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TEXT, } from '../translations'; import type { RuleFormData, RuleFormState } from '../types'; import { hasRuleErrors } from '../validation'; import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; +import { ConfirmCreateRule } from '../components'; interface RuleFlyoutBodyProps { isEdit?: boolean; @@ -53,6 +51,8 @@ export const RuleFlyoutBody = ({ onShowRequest, onChangeMetaData = () => {}, }: RuleFlyoutBodyProps) => { + const [showCreateConfirmation, setShowCreateConfirmation] = useState(false); + const { formData, multiConsumerSelection, @@ -65,8 +65,6 @@ export const RuleFlyoutBody = ({ metadata = {}, } = useRuleFormState(); - const footerCalloutPaddingSize = useEuiPaddingSize('l'); - useEffect(() => { if (!isEmpty(metadata)) { onChangeMetaData(metadata); @@ -100,14 +98,6 @@ export const RuleFlyoutBody = ({ } = useRuleFormHorizontalSteps(initialStep); const { actions } = formData; - - const onSaveInternal = useCallback(() => { - onSave({ - ...formData, - ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), - }); - }, [onSave, formData, multiConsumerSelection]); - const hasActionsDisabled = useMemo(() => { const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); return actions.some((action) => { @@ -123,10 +113,29 @@ export const RuleFlyoutBody = ({ }); }, [actions, connectors, connectorTypes]); - const showNoActionsCallout = useMemo( - () => !hasActionsDisabled && actions.length === 0 && (isEdit || !hasNextStep), - [actions, hasActionsDisabled, hasNextStep, isEdit] - ); + const onSaveInternal = useCallback(() => { + onSave({ + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }); + }, [onSave, formData, multiConsumerSelection]); + + const onClickSave = useCallback(() => { + if (!hasActionsDisabled && actions.length === 0) { + setShowCreateConfirmation(true); + } else { + onSaveInternal(); + } + }, [actions.length, hasActionsDisabled, onSaveInternal]); + + const onCreateConfirmClick = useCallback(() => { + setShowCreateConfirmation(false); + onSaveInternal(); + }, [onSaveInternal]); + + const onCreateCancelClick = useCallback(() => { + setShowCreateConfirmation(false); + }, []); return ( <> @@ -154,21 +163,10 @@ export const RuleFlyoutBody = ({ )} {currentStepComponent} - {showNoActionsCallout && ( - -

{RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TEXT}

-
- )} {isEdit ? ( )} + {showCreateConfirmation && ( + + )} ); }; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx deleted file mode 100644 index c8d75c7e65e92..0000000000000 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_confirm_cancel.tsx +++ /dev/null @@ -1,67 +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", 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 { - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiText, - EuiTitle, - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import React from 'react'; -import { - RULE_FORM_CANCEL_MODAL_DESCRIPTION, - RULE_FORM_CANCEL_MODAL_TITLE, - RULE_FORM_CANCEL_MODAL_CANCEL, - RULE_FORM_CANCEL_MODAL_CONFIRM, -} from '../translations'; - -interface RuleFlyoutShowRequestProps { - onBack: () => void; - onConfirm: () => void; -} -export const RuleFlyoutConfirmCancel = ({ onBack, onConfirm }: RuleFlyoutShowRequestProps) => { - return ( - <> - - -

{RULE_FORM_CANCEL_MODAL_TITLE}

-
-
- -

- {RULE_FORM_CANCEL_MODAL_DESCRIPTION} -

-
- - - - - {RULE_FORM_CANCEL_MODAL_CANCEL} - - - - - {RULE_FORM_CANCEL_MODAL_CONFIRM} - - - - - - ); -}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx index a5b29ad3ab88b..15c346266c922 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx @@ -17,10 +17,8 @@ import React, { createContext, useState } from 'react'; const initialRuleFormScreenContextState = { isConnectorsScreenVisible: false, isShowRequestScreenVisible: false, - isConfirmCancelScreenVisible: false, setIsConnectorsScreenVisible: (show: boolean) => {}, setIsShowRequestScreenVisible: (show: boolean) => {}, - setIsConfirmCancelScreenVisible: (show: boolean) => {}, }; export const RuleFormScreenContext = createContext(initialRuleFormScreenContextState); @@ -28,16 +26,13 @@ export const RuleFormScreenContext = createContext(initialRuleFormScreenContextS export const RuleFormScreenContextProvider: React.FC = ({ children }) => { const [isConnectorsScreenVisible, setIsConnectorsScreenVisible] = useState(false); const [isShowRequestScreenVisible, setIsShowRequestScreenVisible] = useState(false); - const [isConfirmCancelScreenVisible, setIsConfirmCancelScreenVisible] = useState(false); return ( {children} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx index adf54ed9fb55f..9bbfbe311bfec 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx @@ -90,7 +90,7 @@ describe('rulePageFooter', () => { render(); fireEvent.click(screen.getByTestId('rulePageFooterSaveButton')); - expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument(); + expect(screen.getByTestId('confirmCreateRuleModal')).toBeInTheDocument(); }); test('should not show creat rule confirmation if user cannot read actions', () => { @@ -113,7 +113,7 @@ describe('rulePageFooter', () => { render(); fireEvent.click(screen.getByTestId('rulePageFooterSaveButton')); - expect(screen.queryByTestId('rulePageConfirmCreateRule')).not.toBeInTheDocument(); + expect(screen.queryByTestId('confirmCreateRuleModal')).not.toBeInTheDocument(); expect(onSave).toHaveBeenCalled(); }); 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 ef0479338eb69..f39443c9c29ad 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 @@ -561,21 +561,6 @@ export const CONFIRM_RULE_SAVE_MESSAGE_TEXT = i18n.translate( } ); -export const RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TITLE = i18n.translate( - 'responseOpsRuleForm.ruleForm.ruleFlyout.noActionsCalloutTitle', - { - defaultMessage: 'No actions configured', - } -); - -export const RULE_FORM_FLYOUT_NO_ACTIONS_CALLOUT_TEXT = i18n.translate( - 'responseOpsRuleForm.ruleForm.ruleFlyout.noActionsCalloutText', - { - defaultMessage: - 'This rule will be saved with no actions configured. You can add an action later at any time.', - } -); - export const RULE_FORM_PAGE_RULE_DEFINITION_TITLE = i18n.translate( 'responseOpsRuleForm.ruleForm.ruleDefinitionTitle', { diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts index 68e65d396e6cf..de079aa82e19b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts @@ -661,7 +661,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('rulePageFooterSaveButton'); await retry.waitFor('confirmation modal', async () => { - return await testSubjects.exists('rulePageConfirmCreateRule'); + return await testSubjects.exists('confirmCreateRuleModal'); }); await testSubjects.click('confirmModalConfirmButton'); 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 1c563ed9382f6..9d6c4149fd0d8 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 @@ -490,15 +490,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await defineAlwaysFiringAlert(alertName); await testSubjects.click('rulePageFooterSaveButton'); - await testSubjects.existOrFail('rulePageConfirmCreateRule'); - await testSubjects.click('rulePageConfirmCreateRule > confirmModalCancelButton'); + await testSubjects.existOrFail('confirmCreateRuleModal'); + await testSubjects.click('confirmCreateRuleModal > confirmModalCancelButton'); await testSubjects.missingOrFail('confirmRuleSaveModal'); await find.existsByCssSelector('[data-test-subj="rulePageFooterSaveButton"]:not(disabled)'); await testSubjects.click('rulePageFooterSaveButton'); - await testSubjects.existOrFail('rulePageConfirmCreateRule'); - await testSubjects.click('rulePageConfirmCreateRule > confirmModalConfirmButton'); - await testSubjects.missingOrFail('rulePageConfirmCreateRule'); + await testSubjects.existOrFail('confirmCreateRuleModal'); + await testSubjects.click('confirmCreateRuleModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmCreateRuleModal'); const toastTitle = await toasts.getTitleAndDismiss(); expect(toastTitle).to.eql(`Created rule "${alertName}"`); @@ -609,9 +609,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await defineAlwaysFiringAlert(ruleName); await testSubjects.click('rulePageFooterSaveButton'); - await testSubjects.existOrFail('rulePageConfirmCreateRule'); - await testSubjects.click('rulePageConfirmCreateRule > confirmModalConfirmButton'); - await testSubjects.missingOrFail('rulePageConfirmCreateRule'); + await testSubjects.existOrFail('confirmCreateRuleModal'); + await testSubjects.click('confirmCreateRuleModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmCreateRuleModal'); const toastTitle = await toasts.getTitleAndDismiss(); expect(toastTitle).to.eql(`Created rule "${ruleName}"`); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index e56b9f71f169d..ad26e76dff994 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -190,9 +190,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }, async saveAlert() { await testSubjects.click('rulePageFooterSaveButton'); - const isConfirmationModalVisible = await testSubjects.isDisplayed( - 'rulePageConfirmCreateRule' - ); + const isConfirmationModalVisible = await testSubjects.isDisplayed('confirmCreateRuleModal'); expect(isConfirmationModalVisible).to.eql(true, 'Expect confirmation modal to be visible'); await testSubjects.click('confirmModalConfirmButton'); }, From b0f977b5451b5d5fab38766f86c46808860d10b2 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 11:06:04 -0600 Subject: [PATCH 28/62] Remove unused import --- .../shared/response-ops/rule_form/src/rule_page/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/index.ts index 5f38a1560e39f..b7069b619a76f 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/index.ts @@ -10,4 +10,3 @@ export * from './rule_page'; export * from './rule_page_name_input'; export * from './rule_page_footer'; -export * from './rule_page_confirm_create_rule'; From 13429c1eff8015bd2300cb0244c3b1308eed3950 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 13:17:35 -0600 Subject: [PATCH 29/62] Fix ES query expression error display --- .../es_query/expression/expression.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx b/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx index 2f0c46a5e34c5..a39cbd423e7af 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx +++ b/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx @@ -60,10 +60,19 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< } ); - const errorParam = ALL_EXPRESSION_ERROR_KEYS.find((errorKey) => { - // @ts-expect-error upgrade typescript v5.1.6 - return errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined; - }); + const errorParam = + ALL_EXPRESSION_ERROR_KEYS.find((errorKey) => { + return ( + // @ts-expect-error upgrade typescript v5.1.6 + errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined + ); + }) ?? + // For search source alerts, if the only error is timeField, show this error even if the param is undefined + // timeField is inherently a part of the selectable data view, so if the user selects a data view with no + // timeField, this data view is incompatible with the rule. + (isSearchSource && !!errors.timeField?.length && !errors.searchConfiguration?.length) + ? 'timeField' + : undefined; const expressionError = !!errorParam && ( <> From f557eb280f208a131be7ab843bbda5e89046b3a8 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 13:24:52 -0600 Subject: [PATCH 30/62] Fix errorParam ternary syntax --- .../public/rule_types/es_query/expression/expression.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx b/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx index a39cbd423e7af..648494ae5b536 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx +++ b/x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/expression.tsx @@ -66,13 +66,13 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent< // @ts-expect-error upgrade typescript v5.1.6 errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined ); - }) ?? + }) || // For search source alerts, if the only error is timeField, show this error even if the param is undefined // timeField is inherently a part of the selectable data view, so if the user selects a data view with no // timeField, this data view is incompatible with the rule. - (isSearchSource && !!errors.timeField?.length && !errors.searchConfiguration?.length) + (isSearchSource && !!errors.timeField?.length && !errors.searchConfiguration?.length ? 'timeField' - : undefined; + : undefined); const expressionError = !!errorParam && ( <> From 286721fbc08a124fb303fa6c6b6c32a11919be24 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 14:38:20 -0600 Subject: [PATCH 31/62] Fix APM chypress test --- .../plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts index 34d5265e2828d..6540876ba6f7e 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts @@ -80,6 +80,9 @@ describe('Alerts', () => { cy.getByTestSubj('apmIsAboveFieldFieldNumber').clear(); cy.contains('is above 0 errors'); + // Navigate to Rule Details step + cy.getByTestSubj('ruleFormStep-details').click(); + // Save, with no actions cy.contains('Save').click(); cy.get(confirmModalButtonSelector).click(); From 1fd34d09098f4cdade98ca44494741bd8be74b37 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 14:40:10 -0600 Subject: [PATCH 32/62] Fix APM cypress test --- .../plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts | 3 ++- .../plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts index 6540876ba6f7e..c5e0e6a8b703a 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts @@ -73,7 +73,7 @@ describe('Alerts', () => { // has loaded. cy.contains('for the last'); cy.contains('Actions'); - cy.contains('Save').should('not.be.disabled'); + cy.contains('Next').should('not.be.disabled'); // Update "Is above" to "0" cy.contains('is above').click(); @@ -82,6 +82,7 @@ describe('Alerts', () => { // Navigate to Rule Details step cy.getByTestSubj('ruleFormStep-details').click(); + cy.contains('Save').should('not.be.disabled'); // Save, with no actions cy.contains('Save').click(); diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts index 63267a7f07c43..ca8a7ba6dc482 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts @@ -75,6 +75,10 @@ describe('Rules', () => { // has loaded. cy.contains('for the last'); cy.contains('Actions'); + cy.contains('Next').should('not.be.disabled'); + + // Navigate to Rule Details step + cy.getByTestSubj('ruleFormStep-details').click(); cy.contains('Save').should('not.be.disabled'); // Save, with no actions From c437919e8ccbdafe5e304e53b61a2a0500d74ca1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 14:52:11 -0600 Subject: [PATCH 33/62] Reduce discover accessibility test flakiness --- .../apps/discover/group1/_discover_accessibility.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index 9749328be31bc..8c0cbf1bf9988 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -75,7 +75,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await focusAndPressButton('discoverAlertsButton'); expect(await hasFocus('discoverAlertsButton')).to.be(false); await focusAndPressButton('discoverCreateAlertButton'); - expect(await testSubjects.exists('addRuleFlyoutTitle')).to.be(true); + // Increase timeout. The rule flyout has to load asynchronously when the user clicks the create rule button, + // so a higher timeout here reduces flakiness in the test + expect(await testSubjects.exists('addRuleFlyoutTitle', { timeout: 10000 })).to.be(true); await retry.try(async () => { await browser.pressKeys(browser.keys.ESCAPE); // A bug exists with the create rule flyout where sometimes the confirm modal From dbbe03ab4f817339983e48b7983bef76a4b49a80 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 15:40:37 -0600 Subject: [PATCH 34/62] Fix data touched tracking --- .../rule_form/src/create_rule_form.tsx | 1 - .../rule_form/src/edit_rule_form.tsx | 1 - .../rule_form/src/rule_flyout/rule_flyout.tsx | 4 +- .../rule_form_state_context.tsx | 8 +++- .../rule_form_state_provider.tsx | 48 ++++++++++--------- .../rule_form_state_reducer.ts | 15 +++--- .../rule_form/src/rule_page/rule_page.tsx | 11 ++++- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx index 5dfbe470a7d5f..08ce6e29f3a4a 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/create_rule_form.tsx @@ -172,7 +172,6 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { return (
{ return (
{ if (touched) { @@ -87,6 +87,8 @@ export const RuleFlyout = ({ maxWidth={500} className="ruleFormFlyout__container" hideCloseButton={hideCloseButton} + onClick={onInteraction} + onKeyDown={onInteraction} > {isShowRequestScreenVisible ? ( diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_context.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_context.tsx index 8d2551817cf61..2e40c1bdeb1d4 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_context.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_context.tsx @@ -11,7 +11,13 @@ import { createContext } from 'react'; import type { RuleFormState } from '../types'; import type { RuleFormStateReducerAction } from './rule_form_state_reducer'; -export const RuleFormStateContext = createContext({} as RuleFormState); +type RuleFormStateWithInteractHandler = RuleFormState & { + onInteraction: () => void; +}; + +export const RuleFormStateContext = createContext( + {} as RuleFormStateWithInteractHandler +); export const RuleFormReducerContext = createContext>( () => {} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx index 606ffe32f58e7..22f061be86323 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_provider.tsx @@ -7,29 +7,33 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useReducer, useRef, useEffect } from 'react'; -import { isEqual } from 'lodash'; +import React, { useReducer, useState, useCallback } from 'react'; import { RuleFormState } from '../types'; import { RuleFormStateContext, RuleFormReducerContext } from './rule_form_state_context'; -import { ruleFormStateReducer } from './rule_form_state_reducer'; +import { RuleFormStateReducerAction, ruleFormStateReducer } from './rule_form_state_reducer'; import { validateRuleBase, validateRuleParams } from '../validation'; export interface RuleFormStateProviderProps { initialRuleFormState: RuleFormState; - isFlyout?: boolean; } export const RuleFormStateProvider: React.FC< React.PropsWithChildren > = (props) => { - const { children, initialRuleFormState, isFlyout } = props; + // Tracking whether the user has changed the form is unreliable if we base it only on the difference + // between initial data and current data, as many rule types will use reducer actions to set their initial data. + // We need to track whether the user has actually physically interacted with the form before the ruleFormStateReducer + // can accurately determine the `touched` state + const [hasUserInteracted, setHasUserInteracted] = useState(false); + + const { children, initialRuleFormState } = props; const { formData, selectedRuleTypeModel: ruleTypeModel, minimumScheduleInterval, } = initialRuleFormState; - const [ruleFormState, dispatch] = useReducer(ruleFormStateReducer, { + const [ruleFormState, baseDispatch] = useReducer(ruleFormStateReducer, { ...initialRuleFormState, baseErrors: validateRuleBase({ formData, @@ -41,24 +45,24 @@ export const RuleFormStateProvider: React.FC< }), }); - // Rule form state `touched` tends to get reset improperly in the flyout when the screen is re-rendered, - // so track it based on the first returned value of the form data - const originalFormData = useRef(); - useEffect(() => { - if (!isFlyout) return; - if (!originalFormData.current && ruleFormState.formData && ruleFormState.touched === false) { - originalFormData.current = ruleFormState.formData; - } - }, [ruleFormState.formData, ruleFormState.touched, isFlyout]); + // Prime the dispatch function to set `touched` to true on the next action, but not yet + const onInteraction = useCallback(() => { + if (!hasUserInteracted) setHasUserInteracted(true); + }, [hasUserInteracted]); + const dispatch: React.Dispatch = useCallback( + (...args) => { + // If the user has interacted with the form and the `touched` state is false, first update it to be true + // before executing the next action + if (hasUserInteracted && !ruleFormState.touched) { + baseDispatch({ type: 'setTouched' }); + } + baseDispatch(...args); + }, + [baseDispatch, hasUserInteracted, ruleFormState.touched] + ); - const touched = isFlyout - ? ruleFormState.touched - : Boolean( - ruleFormState.touched || - (originalFormData.current && !isEqual(originalFormData.current, ruleFormState.formData)) - ); return ( - + {children} ); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_reducer.ts b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_reducer.ts index c1f4f6f25779c..78018215c9649 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_reducer.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_state/rule_form_state_reducer.ts @@ -8,7 +8,7 @@ */ import { RuleActionParams } from '@kbn/alerting-types'; -import { isEmpty, omit, isEqual } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { RuleFormActionsErrors, RuleFormParamsErrors, RuleUiAction } from '../common'; import { RuleFormData, RuleFormState } from '../types'; import { validateRuleBase, validateRuleParams } from '../validation'; @@ -109,7 +109,8 @@ export type RuleFormStateReducerAction = } | { type: 'runValidation'; - }; + } + | { type: 'setTouched' }; const getUpdateWithValidation = (ruleFormState: RuleFormState) => @@ -119,7 +120,6 @@ const getUpdateWithValidation = selectedRuleTypeModel, multiConsumerSelection, selectedRuleType, - formData: originalFormData, } = ruleFormState; const formData = updater(); @@ -150,14 +150,11 @@ const getUpdateWithValidation = } } - const touched = !isEqual(originalFormData, formData); - return { ...ruleFormState, formData, baseErrors, paramsErrors, - touched, }; }; @@ -356,6 +353,12 @@ export const ruleFormStateReducer = ( case 'runValidation': { return updateWithValidation(() => formData); } + case 'setTouched': { + return { + ...ruleFormState, + touched: true, + }; + } default: { return ruleFormState; } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx index b3321b5d223c0..bf7acadc96ab0 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx @@ -38,7 +38,7 @@ export const RulePage = (props: RulePageProps) => { const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props; const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); - const { formData, multiConsumerSelection, connectorTypes, connectors, touched } = + const { formData, multiConsumerSelection, connectorTypes, connectors, touched, onInteraction } = useRuleFormState(); const { steps } = useRuleFormSteps(); @@ -83,7 +83,14 @@ export const RulePage = (props: RulePageProps) => { return ( <> - + Date: Thu, 23 Jan 2025 15:52:04 -0600 Subject: [PATCH 35/62] Fix custom threshold preview test --- .../pages/alerts/custom_threshold_preview_chart.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts index 188e37d237d90..95cb5d994e80b 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold_preview_chart.ts @@ -51,8 +51,6 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); it('does render the correct error message', async () => { - await testSubjects.setValue('ruleDetailsNameInput', 'test custom threshold rule'); - await testSubjects.click('customEquation'); const customEquationField = await find.byCssSelector( '[data-test-subj="thresholdRuleCustomEquationEditorFieldText"]' From f21566ba66a169034fac2067afb12fef042d02cd Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 23 Jan 2025 15:55:54 -0600 Subject: [PATCH 36/62] Fix monitor alerts journey --- .../uptime/journeys/monitor_details/monitor_alerts.journey.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts index 415f69b508797..7022d07bca5f3 100644 --- a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts +++ b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts @@ -71,11 +71,11 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = step('close anomaly detection flyout', async () => { await page.click(byTestId('ruleFlyoutFooterCancelButton')); if ( - await page.waitForSelector(byTestId('ruleFlyoutConfirmCancelConfirmButton'), { + await page.waitForSelector(byTestId('confirmRuleCloseModal'), { timeout: 1000, }) ) { - await page.click(byTestId('ruleFlyoutConfirmCancelConfirmButton')); + await page.click(byTestId('confirmModalConfirmButton')); } }); From fb70192c5c5b820a9f98e78ceded22d259697266 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 24 Jan 2025 12:54:16 -0600 Subject: [PATCH 37/62] Merge isServerless implementation from main --- .../shared/response-ops/rule_form/src/rule_form.tsx | 11 ++++++----- .../shared/response-ops/rule_form/src/types.ts | 11 ++++------- .../public/components/create_alert.tsx | 2 +- .../public/alerting/transform_alerting_flyout.tsx | 6 +++--- .../shared/ml/public/alerting/ml_alerting_flyout.tsx | 4 ++-- .../sections/rule_form/rule_form_route.tsx | 4 ++-- .../public/common/validate_rule_form_plugins.ts | 2 +- .../shared/triggers_actions_ui/public/mocks.ts | 4 ++-- .../shared/triggers_actions_ui/public/plugin.ts | 12 ++++++------ .../alerting/ui_components/alerting_flyout/index.tsx | 2 +- .../custom_threshold/components/alert_flyout.tsx | 2 +- .../alerting/inventory/components/alert_flyout.tsx | 2 +- .../log_threshold/components/alert_flyout.tsx | 2 +- .../metric_threshold/components/alert_flyout.tsx | 2 +- .../alert_details/components/header_actions.tsx | 2 +- .../public/pages/rule_details/rule_details.tsx | 2 +- .../public/components/alerts_popover.tsx | 2 +- .../pages/slo_details/components/header_control.tsx | 2 +- .../slos/components/common/burn_rate_rule_flyout.tsx | 2 +- .../components/common/edit_burn_rate_rule_flyout.tsx | 2 +- .../compact_view/slo_list_compact_view.tsx | 2 +- .../components/alerts/hooks/use_synthetics_rules.ts | 10 +++++----- .../common/alerts/uptime_edit_alert_flyout.tsx | 6 +++--- .../overview/alerts/uptime_alerts_flyout_wrapper.tsx | 4 ++-- 24 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 5753e5ffea32e..f645095bf1d4d 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -19,12 +19,12 @@ import { RULE_FORM_ROUTE_PARAMS_ERROR_TEXT, RULE_FORM_ROUTE_PARAMS_ERROR_TITLE, } from './translations'; -import { RuleFormData, RuleFormServices, RuleTypeMetaData } from './types'; +import { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types'; const queryClient = new QueryClient(); export interface RuleFormProps { - services: RuleFormServices; + plugins: RuleFormPlugins; id?: string; ruleTypeId?: string; isFlyout?: boolean; @@ -42,13 +42,14 @@ export interface RuleFormProps; initialMetadata?: MetaData; + isServerless?: boolean; } export const RuleForm = ( props: RuleFormProps ) => { const { - services: _services, + plugins: _plugins, onCancel, onSubmit, onChangeMetaData, @@ -66,6 +67,7 @@ export const RuleForm = ( showMustacheAutocompleteSwitch, initialValues, initialMetadata, + isServerless, } = props; const { @@ -83,8 +85,7 @@ export const RuleForm = ( docLinks, ruleTypeRegistry, actionTypeRegistry, - isServerless, - } = _services; + } = _plugins; const ruleFormComponent = useMemo(() => { const plugins = { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/types.ts b/src/platform/packages/shared/response-ops/rule_form/src/types.ts index 74c24576c4263..ef96492f852a2 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/types.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/types.ts @@ -9,6 +9,7 @@ import { ActionType } from '@kbn/actions-types'; import { ActionVariable, RulesSettingsFlapping } from '@kbn/alerting-types'; +import type { ActionConnector, ActionTypeRegistryContract } from '@kbn/alerts-ui-shared'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { ApplicationStart } from '@kbn/core-application-browser'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; @@ -16,22 +17,21 @@ import type { HttpStart } from '@kbn/core-http-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; import type { NotificationsStart } from '@kbn/core-notifications-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; -import type { UserProfileService } from '@kbn/core-user-profile-browser'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { ActionConnector, ActionTypeRegistryContract } from '@kbn/alerts-ui-shared'; import { MinimumScheduleInterval, Rule, RuleFormActionsErrors, RuleFormBaseErrors, RuleFormParamsErrors, + RuleTypeMetaData, RuleTypeModel, RuleTypeParams, - RuleTypeMetaData, RuleTypeRegistryContract, RuleTypeWithDescription, RuleUiAction, @@ -53,7 +53,7 @@ export interface RuleFormData { flapping?: Rule['flapping']; } -export interface RuleFormServices { +export interface RuleFormPlugins { http: HttpStart; i18n: I18nStart; theme: ThemeServiceStart; @@ -68,11 +68,8 @@ export interface RuleFormServices { docLinks: DocLinksStart; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; - isServerless?: boolean; } -export type RuleFormPlugins = Omit; - export interface RuleFormState< Params extends RuleTypeParams = RuleTypeParams, MetaData = RuleTypeMetaData diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx index 1b14cfe08bd63..7c0e665c20b2e 100644 --- a/x-pack/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -38,7 +38,7 @@ export const CreateAlert = ({ {ruleFlyoutVisible ? ( = ({ onCloseFlyout, onSave, }) => { - const { triggersActionsUi, ...services } = useAppDependencies(); + const { triggersActionsUi, ...plugins } = useAppDependencies(); const AlertFlyout = useMemo(() => { if (!triggersActionsUi) return; const commonProps = { - services, + plugins, onCancel: () => { onCloseFlyout(); }, @@ -67,7 +67,7 @@ export const TransformAlertFlyout: FC = ({ }, }); // deps on id to avoid re-rendering on auto-refresh - }, [triggersActionsUi, services, initialAlert, ruleParams, onCloseFlyout, onSave]); + }, [triggersActionsUi, plugins, initialAlert, ruleParams, onCloseFlyout, onSave]); return <>{AlertFlyout}; }; diff --git a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx index 7852532fb36a9..dd2aad2a5e9dc 100644 --- a/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/alerting/ml_alerting_flyout.tsx @@ -38,14 +38,14 @@ export const MlAnomalyAlertFlyout: FC = ({ onSave, }) => { const { - services: { triggersActionsUi, ...services }, + services: { triggersActionsUi, ...plugins }, } = useMlKibana(); const AlertFlyout = useMemo(() => { if (!triggersActionsUi) return; const commonProps = { - services, + plugins, onCancel: () => { onCloseFlyout(); }, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx index 6925bfaca67d2..9be2cde33d058 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx @@ -62,7 +62,7 @@ export const RuleFormRoute = () => { return ( { docLinks, ruleTypeRegistry, actionTypeRegistry, - isServerless, ...startServices, }} + isServerless={isServerless} id={id} ruleTypeId={ruleTypeId} onCancel={() => { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts index 121822f44d451..cd43ca4290f44 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts @@ -23,7 +23,7 @@ const requiredPluginNames = [ ]; type RequiredRuleFormPlugins = Omit< - RuleFormProps['services'], + RuleFormProps['plugins'], 'actionTypeRegistry' | 'ruleTypeRegistry' >; export const validateRuleFormPlugins = (input: unknown): RequiredRuleFormPlugins => { diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts index 66cd4ecb4894b..bf0f3e0e9d0a7 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/mocks.ts @@ -89,8 +89,8 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleFormFlyout: (props) => { return getRuleFormFlyoutLazy({ ...props, - services: { - ...props.services, + plugins: { + ...props.plugins, actionTypeRegistry, ruleTypeRegistry, }, diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts index 0029397f97353..9e53f72a80e2b 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts @@ -121,10 +121,10 @@ export interface TriggersAndActionsUIPublicPluginStart { props: Omit ) => ReactElement; getRuleFormFlyout: ( - props: Omit, 'services'> & { - // Use Partial for compatibility with useKibana hooks that return - // possibly undefined services. Use validateRuleFormPlugins to ensure that the services are not undefined. - services: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; + props: Omit, 'plugins'> & { + // Use Partial for compatibility with useKibana hooks that return + // possibly undefined plugins. Use validateRuleFormPlugins to ensure that the plugins are not undefined. + plugins: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'>; } ) => ReactElement>; getAlertsTable: (props: AlertsTableProps) => ReactElement; @@ -488,8 +488,8 @@ export class Plugin getRuleFormFlyout: (props) => { return getRuleFormFlyoutLazy({ ...props, - services: { - ...validateRuleFormPlugins(props.services), + plugins: { + ...validateRuleFormPlugins(props.plugins), actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, }, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx index 8809a2e3980dc..7e4068500a83e 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx @@ -51,7 +51,7 @@ export function AlertingFlyout(props: Props) { () => ruleType && services.triggersActionsUi.getRuleFormFlyout({ - services, + plugins: services, consumer: APM_SERVER_FEATURE_ID, onCancel: onCloseAddFlyout, onSubmit: onCloseAddFlyout, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx index ff7ca6224e83f..6aa6f136f5721 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/custom_threshold/components/alert_flyout.tsx @@ -25,7 +25,7 @@ export function AlertFlyout({ onClose }: Props) { } return triggersActionsUI.getRuleFormFlyout({ - services, + plugins: services, consumer: 'infrastructure', onCancel: onClose, onSubmit: onClose, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index b5d917beb855a..7d6a2c1db167c 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -33,7 +33,7 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - services, + plugins: services, consumer: 'infrastructure', onCancel: onCloseFlyout, onSubmit: onCloseFlyout, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index 7e4d01e3298c6..cddf96001d8f8 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -24,7 +24,7 @@ export const AlertFlyout = (props: Props) => { () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - services, + plugins: services, consumer: 'logs', onSubmit: onCloseFlyout, onCancel: onCloseFlyout, diff --git a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 151a9eda0de2b..07007dc040f63 100644 --- a/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -29,7 +29,7 @@ export const AlertFlyout = (props: Props) => { () => triggersActionsUI && triggersActionsUI.getRuleFormFlyout({ - services, + plugins: services, consumer: 'infrastructure', onCancel: onCloseFlyout, onSubmit: onCloseFlyout, diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx index 8449e5645a58e..4b7bddfca798c 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -345,7 +345,7 @@ export function HeaderActions({ {rule && ruleConditionsFlyoutOpen ? ( { setRuleConditionsFlyoutOpen(false); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx index 96343bd69bec9..74a8d149b0e3f 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -269,7 +269,7 @@ export function RuleDetailsPage() { {isEditRuleFlyoutVisible && ( { diff --git a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx index 3330e3cfc907b..a9b223c9097ac 100644 --- a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx +++ b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/components/alerts_popover.tsx @@ -68,7 +68,7 @@ export const AlertsPopover = () => { ).toDataviewSpec(); return triggersActionsUi.getRuleFormFlyout({ - services: { application, http, ...services }, + plugins: { application, http, ...services }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, initialValues: { diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx index dc95828f1df79..4b6c6d53ca84c 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_details/components/header_control.tsx @@ -402,7 +402,7 @@ export function HeaderControl({ slo }: Props) { {isRuleFlyoutVisible ? ( {sloToAddRule ? ( { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, isOpen, hasMonitors, defaultRulesEnabled]); - const { triggersActionsUi, ...services } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; const onClose = useMemo(() => () => dispatch(setAlertFlyoutVisible(null)), [dispatch]); @@ -79,7 +79,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { return null; } return triggersActionsUi.getRuleFormFlyout({ - services, + plugins, onCancel: onClose, onSubmit: onClose, id: initialRule.id, @@ -90,7 +90,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { defaultRules?.statusRule, isNewRule, triggersActionsUi, - services, + plugins, onClose, ]); @@ -99,7 +99,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { return null; } return triggersActionsUi.getRuleFormFlyout({ - services, + plugins, consumer: 'uptime', ruleTypeId: alertFlyoutVisible, onCancel: onClose, @@ -115,7 +115,7 @@ export const useSyntheticsRules = (isOpen: boolean) => { }), }, }); - }, [isNewRule, alertFlyoutVisible, triggersActionsUi, services, onClose]); + }, [isNewRule, alertFlyoutVisible, triggersActionsUi, plugins, onClose]); return useMemo( () => ({ loading, EditAlertFlyout, NewRuleFlyout }), diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx index 2a5b446e9161b..833752afd95c1 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -28,7 +28,7 @@ export const UptimeEditAlertFlyoutComponent = ({ initialAlert, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi, ...services } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; const onClose = useCallback(() => { setAlertFlyoutVisibility(false); @@ -40,9 +40,9 @@ export const UptimeEditAlertFlyoutComponent = ({ id: initialAlert.id, onCancel: onClose, onSubmit: onClose, - services, + plugins, }), - [initialAlert, triggersActionsUi, onClose, services] + [initialAlert, triggersActionsUi, onClose, plugins] ); return <>{alertFlyoutVisible && EditAlertFlyout}; }; diff --git a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 1769213fc1b86..77d9b0856e26c 100644 --- a/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/solutions/observability/plugins/uptime/public/legacy_uptime/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -24,7 +24,7 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ alertTypeId, setAlertFlyoutVisibility, }: Props) => { - const { triggersActionsUi, ...services } = useKibana().services; + const { triggersActionsUi, ...plugins } = useKibana().services; const onCloseAlertFlyout = useCallback( () => setAlertFlyoutVisibility(false), [setAlertFlyoutVisibility] @@ -32,7 +32,7 @@ export const UptimeAlertsFlyoutWrapperComponent = ({ const AddAlertFlyout = useMemo( () => triggersActionsUi.getRuleFormFlyout({ - services, + plugins, consumer: 'uptime', onCancel: onCloseAlertFlyout, onSubmit: onCloseAlertFlyout, From 4fd12284ba3baea73f4373ebf804e03e6dfdbb84 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 24 Jan 2025 12:56:16 -0600 Subject: [PATCH 38/62] Fix APM cypress --- .../plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts index c5e0e6a8b703a..96ac162d67272 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts @@ -82,10 +82,10 @@ describe('Alerts', () => { // Navigate to Rule Details step cy.getByTestSubj('ruleFormStep-details').click(); - cy.contains('Save').should('not.be.disabled'); + cy.contains('Create rule').should('not.be.disabled'); // Save, with no actions - cy.contains('Save').click(); + cy.contains('Create rule').click(); cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); From f2e01b932de4cf8ed81ad41cffc44f14fc7c9ad0 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 24 Jan 2025 13:29:32 -0600 Subject: [PATCH 39/62] Fix some tests, improve loading prompty --- .../top_nav/app_menu_actions/get_alerts.tsx | 2 +- .../accessors/get_app_menu.ts | 2 +- .../group1/_discover_accessibility.ts | 3 +- .../public/common/get_rule_form_flyout.tsx | 37 ++++++++++++++++--- .../discover/search_source_alert.ts | 4 +- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx index 14d166c5b5589..68222ff40f4ca 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/app_menu_actions/get_alerts.tsx @@ -80,7 +80,7 @@ const CreateAlertFlyout: React.FC<{ ); return triggersActionsUi?.getRuleFormFlyout({ - services, + plugins: services, initialMetadata: discoverMetadata, consumer: 'alerts', onCancel: onFinishAction, diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts index 0aea7f863e8c1..6f2d18f8aead5 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/accessors/get_app_menu.ts @@ -92,7 +92,7 @@ const registerCustomThresholdRuleAction = ( const { filters, query } = data.query.getState(); return triggersActionsUi.getRuleFormFlyout({ - services: { data, ...services }, + plugins: { data, ...services }, consumer: 'logs', ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, initialValues: { diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index 8c0cbf1bf9988..20371740935a1 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -75,9 +75,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await focusAndPressButton('discoverAlertsButton'); expect(await hasFocus('discoverAlertsButton')).to.be(false); await focusAndPressButton('discoverCreateAlertButton'); + expect(await testSubjects.exists('ruleFormFlyoutLoading')).to.be(true); // Increase timeout. The rule flyout has to load asynchronously when the user clicks the create rule button, // so a higher timeout here reduces flakiness in the test - expect(await testSubjects.exists('addRuleFlyoutTitle', { timeout: 10000 })).to.be(true); + expect(await testSubjects.exists('addRuleFlyoutTitle', { timeout: 20000 })).to.be(true); await retry.try(async () => { await browser.pressKeys(browser.keys.ESCAPE); // A bug exists with the create rule flyout where sometimes the confirm modal diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index 5b71010e9f41b..4ffac46ece3df 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -5,24 +5,49 @@ * 2.0. */ -import { EuiPortal, EuiOverlayMask, EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiPortal, + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiOverlayMask, + EuiFlexGroup, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import type { RuleFormProps } from '@kbn/response-ops-rule-form'; import React, { Suspense, lazy } from 'react'; import type { RuleTypeMetaData } from '../types'; +const RuleForm: React.LazyExoticComponent>> = lazy(() => + import('@kbn/response-ops-rule-form').then((module) => ({ default: module.RuleForm })) +); + export const getRuleFormFlyoutLazy = ( props: RuleFormProps ) => { - const RuleForm: React.LazyExoticComponent>> = lazy(() => - import('@kbn/response-ops-rule-form').then((module) => ({ default: module.RuleForm })) - ); - + const isEdit = !!props.id; return ( - + + } + title={ +

+ {isEdit + ? i18n.translate('xpack.triggersActionsUi.ruleFormFlyout.loadingEditText', { + defaultMessage: 'Loading edit rule form', + }) + : i18n.translate('xpack.triggersActionsUi.ruleFormFlyout.loadingCreateText', { + defaultMessage: 'Loading create rule form', + })} +

+ } + /> +
} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index 9fb148a359c4f..27851484324c1 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -606,9 +606,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await openDiscoverAlertFlyout(); await defineSearchSourceAlert('test-adhoc-alert'); await testSubjects.click('ruleFormStep-details'); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFlyoutFooterSaveButton'); await retry.try(async () => { - await testSubjects.missingOrFail('saveRuleButton'); + await testSubjects.missingOrFail('ruleFlyoutFooterSaveButton'); }); await PageObjects.header.waitUntilLoadingHasFinished(); From e6befaff7814c63ac44b6dc95410b9bfce9d4f8b Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 24 Jan 2025 13:39:45 -0600 Subject: [PATCH 40/62] Fix initial metadata override --- .../shared/response-ops/rule_form/src/edit_rule_form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx index 06b7406ed9388..2d8d3a392f519 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/edit_rule_form.tsx @@ -50,7 +50,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { onSubmit, isFlyout, onChangeMetaData, - initialMetadata = {}, + initialMetadata, } = props; const { http, notifications, docLinks, ruleTypeRegistry, application, ...deps } = plugins; const { toasts } = notifications; From 07ee45aa846598810624907420a4f9316fd6e1f4 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 24 Jan 2025 15:04:03 -0600 Subject: [PATCH 41/62] Fix typecheck --- .../triggers_actions_ui_example/public/application.tsx | 4 ++-- .../private/monitoring/public/alerts/configuration.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index 5c22d7ea5c1e2..638233dd47a88 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -188,7 +188,7 @@ const TriggersActionsUiExampleApp = ({ render={() => ( ( = (props: Props) => { () => showFlyout && Legacy.shims.triggersActionsUi.getRuleFormFlyout({ - services, + plugins: services, id: alert.id, onSubmit: onClose, onCancel: onClose, From 370a2b67f7a18a6271b3310b1098f181291df913 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 10:58:53 -0600 Subject: [PATCH 42/62] Fix i18n --- .../public/common/get_rule_form_flyout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx index 4ffac46ece3df..e8cf6b8ca9a60 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_rule_form_flyout.tsx @@ -38,10 +38,10 @@ export const getRuleFormFlyoutLazy = {isEdit - ? i18n.translate('xpack.triggersActionsUi.ruleFormFlyout.loadingEditText', { + ? i18n.translate('xpack.triggersActionsUI.ruleFormFlyout.loadingEditText', { defaultMessage: 'Loading edit rule form', }) - : i18n.translate('xpack.triggersActionsUi.ruleFormFlyout.loadingCreateText', { + : i18n.translate('xpack.triggersActionsUI.ruleFormFlyout.loadingCreateText', { defaultMessage: 'Loading create rule form', })} From 52c6414b51aef88f766285de37e23d21080d987b Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 12:43:21 -0600 Subject: [PATCH 43/62] Fix uptime rule form flyout and observability rule params --- .../public/common/validate_rule_form_plugins.ts | 2 +- .../plugins/observability/public/pages/rules/rule.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts index cd43ca4290f44..f8872aaf8a2f9 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts @@ -8,6 +8,7 @@ import { RuleFormProps } from '@kbn/response-ops-rule-form'; const requiredPluginNames = [ + // dataViews is intentionally omitted from this list because it is not required for all rule types 'http', 'i18n', 'theme', @@ -17,7 +18,6 @@ const requiredPluginNames = [ 'charts', 'settings', 'data', - 'dataViews', 'unifiedSearch', 'docLinks', ]; diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rule.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rule.tsx index d33c2a6cbea29..401e2e04ebbac 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/rules/rule.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/rules/rule.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { RuleForm } from '@kbn/response-ops-rule-form'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; @@ -39,6 +39,11 @@ export function RulePage() { const location = useLocation<{ returnApp?: string; returnPath?: string }>(); const { returnApp, returnPath } = location.state || {}; + const { id, ruleTypeId } = useParams<{ + id?: string; + ruleTypeId?: string; + }>(); + useBreadcrumbs( [ { @@ -81,6 +86,8 @@ export function RulePage() { actionTypeRegistry, ...startServices, }} + id={id} + ruleTypeId={ruleTypeId} validConsumers={observabilityRuleCreationValidConsumers} multiConsumerSelection={AlertConsumers.LOGS} isServerless={!!serverless} From aa0287f509bb51a1bb42643b168723caad0e3cda Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 13:04:40 -0600 Subject: [PATCH 44/62] Fix uptime tests --- .../src/rule_flyout/rule_flyout_body.tsx | 2 +- .../ftr_e2e/cypress/e2e/alerts/error_count.cy.ts | 2 +- x-pack/test/functional/services/uptime/alerts.ts | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx index d22f390e8b663..5e9206da162d8 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx @@ -141,7 +141,7 @@ export const RuleFlyoutBody = ({ <> -

+

{isEdit ? RULE_FLYOUT_HEADER_EDIT_TITLE : RULE_FLYOUT_HEADER_CREATE_TITLE}

diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts index 96ac162d67272..3b1cf62e02d00 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts @@ -59,7 +59,7 @@ describe('Alerts', () => { describe('when rendered from Service view in APM app', () => { const ruleName = 'Error count threshold'; - const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]'; + const confirmModalButtonSelector = '[data-test-subj=confirmModalConfirmButton]'; it('alerts table is rendered correctly', () => { cy.loginAsEditorUser(); diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index 792dffd6a2929..35fed9796d250 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -21,12 +21,13 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { await testSubjects.click('xpack.synthetics.toggleTlsAlertFlyout'); } // ensure the flyout has opened - await testSubjects.exists('ruleNameInput'); + await testSubjects.exists('ruleFlyoutTitle'); }, async openMonitorStatusAlertType(alertType: string) { await testSubjects.click(`xpack.synthetics.alerts.${alertType}-SelectOption`); }, async setAlertTags(tags: string[]) { + await testSubjects.click('ruleFormStep-details'); for (let i = 0; i < tags.length; i += 1) { await testSubjects.click('comboBoxSearchInput'); await testSubjects.setValue('comboBoxInput', tags[i]); @@ -34,10 +35,12 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { } }, async setAlertName(name: string) { - await testSubjects.setValue('ruleNameInput', name); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', name); }, async setAlertInterval(value: string) { - await testSubjects.setValue('intervalInput', value); + await testSubjects.click('ruleFormStep-definition'); + await testSubjects.setValue('ruleScheduleNumberInput', value); }, async setAlertThrottleInterval(value: string) { await testSubjects.click('notifyWhenSelect'); @@ -109,11 +112,12 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { * While this field is set in previous step, it is possible that component rerendering could be * clearing out the value after it's filled in. To prevent this particular issue with flakiness, * we should attempt to set the name again before saving the alert */ - await testSubjects.setValue('ruleNameInput', name); - await testSubjects.click('saveRuleButton'); + await testSubjects.click('ruleFormStep-details'); + await testSubjects.setValue('ruleDetailsNameInput', name); + await testSubjects.click('ruleFlyoutFooterSaveButton'); }, async clickSaveAlertsConfirmButton() { - await testSubjects.click('confirmRuleSaveModal > confirmModalConfirmButton', 20000); + await testSubjects.click('confirmModalConfirmButton', 20000); }, }; } From 9ef99b5f49facfe8c433aa6ae7564c49f08bc913 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 13:08:55 -0600 Subject: [PATCH 45/62] Fix serverless search source test --- .../common/discover_ml_uptime/discover/search_source_alert.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index 27851484324c1..406630b382370 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -682,8 +682,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('rule name value is correct', async () => { - await testSubjects.click('ruleFormStep-details'); - await testSubjects.setValue('ruleDetailsNameInput', newAlert); const ruleName = await testSubjects.getAttribute('ruleDetailsNameInput', 'value'); From bb09a7d33e095bcea28c2d8d5f578d2193cc9fd4 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 13:10:26 -0600 Subject: [PATCH 46/62] Fix APM cypress --- .../plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts index ca8a7ba6dc482..a339a97349c3d 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts @@ -79,10 +79,10 @@ describe('Rules', () => { // Navigate to Rule Details step cy.getByTestSubj('ruleFormStep-details').click(); - cy.contains('Save').should('not.be.disabled'); + cy.contains('Create rule').should('not.be.disabled'); // Save, with no actions - cy.contains('Save').click(); + cy.contains('Create rule').click(); cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); From d182820d1b75618aec36280a218d276c34dd354d Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 13:23:51 -0600 Subject: [PATCH 47/62] Add debug logs to accessibility testing --- .../apps/discover/group1/_discover_accessibility.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index 20371740935a1..db1f91f737cdc 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -27,9 +27,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }; const hasFocus = async (testSubject: string) => { - const targetElement = await testSubjects.find(testSubject); const activeElement = await find.activeElement(); - return (await targetElement._webElement.getId()) === (await activeElement._webElement.getId()); + const activeElementTestID = await activeElement._webElement.getAttribute('data-test-subj'); + if (activeElementTestID !== testSubject) { + log.debug(`hasFocus: Active element test subj ${activeElementTestID} is not ${testSubject}`); + } else { + log.debug(`hasFocus: Active element test subj is ${activeElementTestID}`); + } + return activeElementTestID === testSubject; }; describe('discover accessibility', () => { From 118a1544b3f10fa6647b7f7cd228e5d188f6cece Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 15:53:07 -0600 Subject: [PATCH 48/62] Fix synthetics journey --- .../journeys/alert_rules/custom_status_alert.journey.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts index 3d45e0698f616..c8e08e2a7f3d2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts @@ -50,8 +50,9 @@ journey(`CustomStatusAlert`, async ({ page, params }) => { await page.getByTestId('manageStatusRuleName').click(); await page.getByTestId('createNewStatusRule').click(); - await page.getByTestId('ruleNameInput').fill('Synthetics status rule'); - await page.getByTestId('saveRuleButton').click(); + await page.getByTestId('ruleFormStep-details').click(); + await page.getByTestId('ruleDetailsNameInput').fill('Synthetics status rule'); + await page.getByTestId('ruleFlyoutFooterSaveButton').click(); await page.getByTestId('confirmModalConfirmButton').click(); await page.waitForSelector(`text='Created rule "Synthetics status rule"'`); From b8a2e33a945498bfd628b42ce294f527b7572a81 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 16:01:03 -0600 Subject: [PATCH 49/62] Fix APM cypress --- .../apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts | 7 ++++--- .../apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts index 3b1cf62e02d00..fae8ec9f897eb 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/alerts/error_count.cy.ts @@ -59,7 +59,8 @@ describe('Alerts', () => { describe('when rendered from Service view in APM app', () => { const ruleName = 'Error count threshold'; - const confirmModalButtonSelector = '[data-test-subj=confirmModalConfirmButton]'; + const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]'; + const saveButtonSelector = 'button[data-test-subj=ruleFlyoutFooterSaveButton]'; it('alerts table is rendered correctly', () => { cy.loginAsEditorUser(); @@ -82,10 +83,10 @@ describe('Alerts', () => { // Navigate to Rule Details step cy.getByTestSubj('ruleFormStep-details').click(); - cy.contains('Create rule').should('not.be.disabled'); + cy.get(saveButtonSelector).should('not.be.disabled'); // Save, with no actions - cy.contains('Create rule').click(); + cy.get(saveButtonSelector).click(); cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); diff --git a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts index a339a97349c3d..0337014f767c6 100644 --- a/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts +++ b/x-pack/solutions/observability/plugins/apm/ftr_e2e/cypress/e2e/rules/error_count.cy.ts @@ -60,6 +60,7 @@ describe('Rules', () => { const ruleName = 'Error count threshold'; const comboBoxInputSelector = '[data-popover-open] [data-test-subj=comboBoxSearchInput]'; const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]'; + const saveButtonSelector = 'button[data-test-subj=ruleFlyoutFooterSaveButton]'; describe('when created from APM', () => { describe('when created from Service Inventory', () => { @@ -79,10 +80,10 @@ describe('Rules', () => { // Navigate to Rule Details step cy.getByTestSubj('ruleFormStep-details').click(); - cy.contains('Create rule').should('not.be.disabled'); + cy.get(saveButtonSelector).should('not.be.disabled'); // Save, with no actions - cy.contains('Create rule').click(); + cy.get(saveButtonSelector).click(); cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); From 09d456d66b8b698e91f4b6764378be16d000788a Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 27 Jan 2025 16:47:41 -0600 Subject: [PATCH 50/62] Remove code path for experimental use of v1 flyout --- .../public/application/rules_app.tsx | 27 +++++++------------ .../rules_list/components/rules_list.tsx | 3 ++- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx index 2477d9d95c4b4..2e88a55e438aa 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx @@ -54,7 +54,6 @@ import { KibanaContextProvider, useKibana } from '../common/lib/kibana'; import { ConnectorProvider } from './context/connector_context'; import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID } from '../common/constants'; import { queryClient } from './query_client'; -import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features'; const TriggersActionsUIHome = lazy(() => import('./home')); const RuleDetailsRoute = lazy( @@ -125,25 +124,19 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) = application: { navigateToApp }, } = useKibana().services; - const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); - return ( - {!isUsingRuleCreateFlyout && ( - - )} - {!isUsingRuleCreateFlyout && ( - - )} + + (null); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); - const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); + // TODO: Remove this when removing the v1 flyout code + const isUsingRuleCreateFlyout = false; // getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); const [percentileOptions, setPercentileOptions] = useState(initialPercentileOptions); From c89a7076876ef7f486c5ede0ffed2a6e9444d969 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 28 Jan 2025 11:46:11 -0600 Subject: [PATCH 51/62] Update synthetics journey --- .../journeys/alert_rules/default_status_alert.journey.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts index 7b7f52b1dce7c..21a8ccf29518d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts @@ -63,9 +63,9 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { await page.getByTestId('editDefaultStatusRule').click(); await page.waitForSelector('text=Monitor status rule'); - await page.getByTestId('intervalInputUnit').selectOption('second'); - await page.getByTestId('intervalInput').fill('20'); - await page.click(byTestId('ruleFlyoutFooterCancelButton')); + await page.getByTestId('ruleScheduleUnitInput').selectOption('second'); + await page.getByTestId('ruleScheduleNumberInput').fill('20'); + await page.click(byTestId('ruleFlyoutFooterSaveButton')); await page.waitForSelector("text=Updated 'Synthetics status internal rule'"); }); From 565c974bf4cfeea12155f493bb9ccea1e8219e34 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 28 Jan 2025 11:47:59 -0600 Subject: [PATCH 52/62] Update uptime synthetic test and skip discover accessibility test --- .../apps/discover/group1/_discover_accessibility.ts | 2 +- .../journeys/monitor_details/monitor_alerts.journey.ts | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index db1f91f737cdc..d36a4e9e036e4 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should return focus to the alerts button when dismissing the alerts popover', () => expectButtonToLoseAndRegainFocusWhenOverlayIsOpenedAndClosed('discoverAlertsButton')); - it('should return focus to the alerts button when dismissing the create rule flyout', async () => { + it.skip('should return focus to the alerts button when dismissing the create rule flyout', async () => { await focusAndPressButton('discoverAlertsButton'); expect(await hasFocus('discoverAlertsButton')).to.be(false); await focusAndPressButton('discoverCreateAlertButton'); diff --git a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts index 7022d07bca5f3..6e19f8b1c954a 100644 --- a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts +++ b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/journeys/monitor_details/monitor_alerts.journey.ts @@ -70,13 +70,6 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = step('close anomaly detection flyout', async () => { await page.click(byTestId('ruleFlyoutFooterCancelButton')); - if ( - await page.waitForSelector(byTestId('confirmRuleCloseModal'), { - timeout: 1000, - }) - ) { - await page.click(byTestId('confirmModalConfirmButton')); - } }); step('open anomaly detection alert', async () => { @@ -90,7 +83,7 @@ journey('MonitorAlerts', async ({ page, params }: { page: Page; params: any }) = }); step('save anomaly detection alert', async () => { - await page.click(byTestId('saveRuleButton')); + await page.click(byTestId('ruleFlyoutFooterSaveButton')); await page.click(byTestId('confirmModalConfirmButton')); await page.waitForSelector(`text=Created rule "${alertId}"`); }); From 3f16b58d50803b0a80c68d831e1b85d277e4a086 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 28 Jan 2025 14:50:32 -0600 Subject: [PATCH 53/62] Unskip discover accessibility test --- test/functional/apps/discover/group1/_discover_accessibility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index d36a4e9e036e4..db1f91f737cdc 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should return focus to the alerts button when dismissing the alerts popover', () => expectButtonToLoseAndRegainFocusWhenOverlayIsOpenedAndClosed('discoverAlertsButton')); - it.skip('should return focus to the alerts button when dismissing the create rule flyout', async () => { + it('should return focus to the alerts button when dismissing the create rule flyout', async () => { await focusAndPressButton('discoverAlertsButton'); expect(await hasFocus('discoverAlertsButton')).to.be(false); await focusAndPressButton('discoverCreateAlertButton'); From 27cb8b339267ce8a4acd152af43d6f2b555b1526 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 28 Jan 2025 15:17:46 -0600 Subject: [PATCH 54/62] Remove flyout loading check from discover test --- test/functional/apps/discover/group1/_discover_accessibility.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/apps/discover/group1/_discover_accessibility.ts b/test/functional/apps/discover/group1/_discover_accessibility.ts index db1f91f737cdc..f45fdd98c89f8 100644 --- a/test/functional/apps/discover/group1/_discover_accessibility.ts +++ b/test/functional/apps/discover/group1/_discover_accessibility.ts @@ -80,7 +80,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await focusAndPressButton('discoverAlertsButton'); expect(await hasFocus('discoverAlertsButton')).to.be(false); await focusAndPressButton('discoverCreateAlertButton'); - expect(await testSubjects.exists('ruleFormFlyoutLoading')).to.be(true); // Increase timeout. The rule flyout has to load asynchronously when the user clicks the create rule button, // so a higher timeout here reduces flakiness in the test expect(await testSubjects.exists('addRuleFlyoutTitle', { timeout: 20000 })).to.be(true); From 688c1454deea2989b4d97329e793b7aaf97d5648 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 28 Jan 2025 15:39:21 -0600 Subject: [PATCH 55/62] Update synthetics tests --- .../journeys/alert_rules/default_status_alert.journey.ts | 1 + .../plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts index 21a8ccf29518d..068ccd21d4057 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts @@ -66,6 +66,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { await page.getByTestId('ruleScheduleUnitInput').selectOption('second'); await page.getByTestId('ruleScheduleNumberInput').fill('20'); await page.click(byTestId('ruleFlyoutFooterSaveButton')); + await page.click(byTestId('confirmModalConfirmButton')); await page.waitForSelector("text=Updated 'Synthetics status internal rule'"); }); diff --git a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx index db5e13e975267..d0795efe42e82 100644 --- a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx +++ b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx @@ -112,7 +112,8 @@ export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; ki }, async updateAlert({ id, threshold }: AlertType) { - await this.fillByTestSubj('ruleNameInput', id); + await page.click(byTestId('ruleFormStep-details')); + await this.fillByTestSubj('ruleDetailsNameInput', id); await this.selectAlertThreshold(threshold); }, From c3eff4fb842b71aa06d7822417769db2b0d217ca Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 29 Jan 2025 11:44:53 -0600 Subject: [PATCH 56/62] Omit dataViews from RequiredRuleFormPlugins typedef --- .../public/common/validate_rule_form_plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts index f8872aaf8a2f9..9af01ca368700 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/validate_rule_form_plugins.ts @@ -24,7 +24,7 @@ const requiredPluginNames = [ type RequiredRuleFormPlugins = Omit< RuleFormProps['plugins'], - 'actionTypeRegistry' | 'ruleTypeRegistry' + 'actionTypeRegistry' | 'ruleTypeRegistry' | 'dataViews' >; export const validateRuleFormPlugins = (input: unknown): RequiredRuleFormPlugins => { if (typeof input !== 'object' || input === null) { From 3ef3181b57935d8bf1064582d63271834efd1ef0 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 29 Jan 2025 12:37:15 -0600 Subject: [PATCH 57/62] Add encryption key to discover accessibility test --- test/functional/apps/discover/group1/config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/functional/apps/discover/group1/config.ts b/test/functional/apps/discover/group1/config.ts index 941eec8ca621c..c95b0e5d11e25 100644 --- a/test/functional/apps/discover/group1/config.ts +++ b/test/functional/apps/discover/group1/config.ts @@ -14,6 +14,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // required for alerts plugin to work + ], + }, testFiles: [require.resolve('.')], }; } From 756cfa8c6cb415f8a9f8fd194e53e5f386cbb3d0 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 29 Jan 2025 12:40:44 -0600 Subject: [PATCH 58/62] Fix uptime journey --- .../plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx index d0795efe42e82..80564d1bfa4a9 100644 --- a/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx +++ b/x-pack/solutions/observability/plugins/uptime/e2e/uptime/page_objects/monitor_details.tsx @@ -112,12 +112,13 @@ export function monitorDetailsPageProvider({ page, kibanaUrl }: { page: Page; ki }, async updateAlert({ id, threshold }: AlertType) { + await this.selectAlertThreshold(threshold); await page.click(byTestId('ruleFormStep-details')); await this.fillByTestSubj('ruleDetailsNameInput', id); - await this.selectAlertThreshold(threshold); }, async selectAlertThreshold(threshold: string) { + await page.click(byTestId('ruleFormStep-definition')); await page.click(byTestId('uptimeAnomalySeverity')); await page.click(byTestId('anomalySeveritySelect')); await page.click(`text=${threshold}`); From aa0abd018e3a36db8bb651f290afa82bb4676e15 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 30 Jan 2025 11:00:26 -0600 Subject: [PATCH 59/62] Fix typecheck --- .../shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts | 2 +- .../packages/shared/response-ops/rule_form/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts index 7453b0a384fb2..a26c3cbf7dead 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/rule_types.ts @@ -105,7 +105,7 @@ export interface RuleTypeParamsExpressionProps< metadata?: MetaData; charts: ChartsPluginSetup; data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; + dataViews?: DataViewsPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/src/platform/packages/shared/response-ops/rule_form/src/types.ts b/src/platform/packages/shared/response-ops/rule_form/src/types.ts index ef96492f852a2..a367ce163ef41 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/types.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/types.ts @@ -63,11 +63,11 @@ export interface RuleFormPlugins { charts: ChartsPluginSetup; settings: SettingsStart; data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; docLinks: DocLinksStart; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; + dataViews?: DataViewsPublicPluginStart; } export interface RuleFormState< From 990a73c0f6cf0d2c5141aac0061d151059346d73 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 30 Jan 2025 15:56:15 -0600 Subject: [PATCH 60/62] Attempt synthetics test fix --- .../journeys/alert_rules/default_status_alert.journey.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts index 068ccd21d4057..375a7257642c6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts @@ -66,8 +66,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { await page.getByTestId('ruleScheduleUnitInput').selectOption('second'); await page.getByTestId('ruleScheduleNumberInput').fill('20'); await page.click(byTestId('ruleFlyoutFooterSaveButton')); - await page.click(byTestId('confirmModalConfirmButton')); - await page.waitForSelector("text=Updated 'Synthetics status internal rule'"); + await page.waitForSelector('text=Updated "Synthetics status internal rule"'); }); step('Monitor is as up in overview page', async () => { From fc81cb4e8ed6a6655886383a5d46c9c39acfae6c Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 31 Jan 2025 14:30:51 -0600 Subject: [PATCH 61/62] Remove V1 Rule Form flyout --- .../.storybook/decorator.tsx | 1 - .../common/experimental_features.ts | 1 - .../public/application/home.tsx | 1 - .../sections/rule_details/components/rule.tsx | 1 - .../components/rule_definition.tsx | 36 +- .../rule_details/components/rule_details.tsx | 51 +- .../sections/rule_form/confirm_rule_close.tsx | 53 - .../sections/rule_form/confirm_rule_save.tsx | 52 - .../rule_form/get_initial_interval.test.ts | 23 - .../rule_form/get_initial_interval.ts | 19 - .../rule_form/has_rule_changed.test.ts | 165 --- .../sections/rule_form/has_rule_changed.ts | 42 - .../application/sections/rule_form/index.tsx | 19 - .../sections/rule_form/rule_add.test.tsx | 623 -------- .../sections/rule_form/rule_add.tsx | 419 ------ .../sections/rule_form/rule_add_footer.tsx | 100 -- .../rule_form/rule_conditions.test.tsx | 263 ---- .../sections/rule_form/rule_conditions.tsx | 130 -- .../rule_form/rule_conditions_group.test.tsx | 100 -- .../rule_form/rule_conditions_group.tsx | 66 - .../sections/rule_form/rule_edit.test.tsx | 270 ---- .../sections/rule_form/rule_edit.tsx | 413 ------ .../sections/rule_form/rule_errors.test.tsx | 347 ----- .../sections/rule_form/rule_errors.ts | 185 --- .../sections/rule_form/rule_form.scss | 4 - .../sections/rule_form/rule_form.test.tsx | 1257 ----------------- .../sections/rule_form/rule_form.tsx | 1153 --------------- .../rule_form_advanced_options.test.tsx | 232 --- .../rule_form/rule_form_advanced_options.tsx | 166 --- .../rule_form_consumer_selection.test.tsx | 222 --- .../rule_form_consumer_selection.tsx | 197 --- .../sections/rule_form/rule_form_route.tsx | 102 -- .../rule_form/rule_notify_when.test.tsx | 144 -- .../sections/rule_form/rule_notify_when.tsx | 236 ---- .../sections/rule_form/rule_reducer.test.ts | 277 ---- .../sections/rule_form/rule_reducer.ts | 319 ----- .../rule_form/show_request_modal.test.tsx | 242 ---- .../sections/rule_form/show_request_modal.tsx | 85 -- .../sections/rule_form/solution_filter.tsx | 84 -- .../rules_list/components/rules_list.tsx | 91 +- .../common/get_experimental_features.test.tsx | 5 - .../triggers_actions_ui/public/types.ts | 1 - .../test_serverless/functional/config.base.ts | 5 - 43 files changed, 26 insertions(+), 8176 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.test.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/index.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.test.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.tsx delete mode 100644 x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/.storybook/decorator.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/.storybook/decorator.tsx index fcaf0ce7597ce..188d36e9ee05c 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/.storybook/decorator.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/.storybook/decorator.tsx @@ -71,7 +71,6 @@ export const StorybookContextDecorator: FC diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx index 371fe483a4695..f36ea93a692a0 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_definition.tsx @@ -23,14 +23,12 @@ import { useLoadRuleTypesQuery } from '../../../hooks/use_load_rule_types_query' import { RuleDefinitionProps } from '../../../../types'; import { RuleType } from '../../../..'; import { useKibana } from '../../../../common/lib/kibana'; -import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { hasAllPrivilege, hasExecuteActionsCapability, hasShowActionsCapability, } from '../../../lib/capabilities'; import { RuleActions } from './rule_actions'; -import { RuleEdit } from '../../rule_form'; export const RuleDefinition: React.FunctionComponent = ({ rule, @@ -39,15 +37,11 @@ export const RuleDefinition: React.FunctionComponent = ({ onEditRule, hideEditButton = false, filteredRuleTypes = [], - useNewRuleForm = false, }) => { const { application: { capabilities, navigateToApp }, } = useKibana().services; - const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); - - const [editFlyoutVisible, setEditFlyoutVisible] = useState(false); const [ruleType, setRuleType] = useState(); const hasConditions = !!(rule?.params.criteria as any[])?.length; @@ -110,17 +104,13 @@ export const RuleDefinition: React.FunctionComponent = ({ }, [rule, ruleTypeRegistry]); const onEditRuleClick = () => { - if (!isUsingRuleCreateFlyout && useNewRuleForm) { - navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, - state: { - returnApp: 'management', - returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, - }, - }); - } else { - setEditFlyoutVisible(true); - } + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, + }, + }); }; const ruleDefinitionList = [ @@ -239,18 +229,6 @@ export const RuleDefinition: React.FunctionComponent = ({ - {editFlyoutVisible && ( - { - setEditFlyoutVisible(false); - return onEditRule(); - }} - initialRule={rule} - onClose={() => setEditFlyoutVisible(false)} - ruleTypeRegistry={ruleTypeRegistry} - actionTypeRegistry={actionTypeRegistry} - /> - )} ); }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index 767b7cf70b9bb..bd887e758c55e 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState, useEffect, useReducer, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { EuiPageHeader, @@ -52,14 +52,12 @@ import { } from '../../common/components/with_bulk_rule_api_operations'; import { RuleRouteWithApi } from './rule_route'; import { ViewInApp } from './view_in_app'; -import { RuleEdit } from '../../rule_form'; import { routeToRules } from '../../../constants'; import { rulesErrorReasonTranslationsMapping, rulesWarningReasonTranslationsMapping, } from '../../rules_list/translations'; import { useKibana } from '../../../../common/lib/kibana'; -import { getRuleReducer } from '../../rule_form/rule_reducer'; import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api'; import { runRule } from '../../../lib/run_rule'; import { @@ -71,7 +69,6 @@ import { import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; import { RefreshToken } from './types'; import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal'; -import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; export type RuleDetailsProps = { rule: Rule; @@ -79,7 +76,6 @@ export type RuleDetailsProps = { actionTypes: ActionType[]; requestRefresh: () => Promise; refreshToken?: RefreshToken; - useNewRuleForm?: boolean; } & Pick< BulkOperationsComponentOpts, 'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule' @@ -102,7 +98,6 @@ export const RuleDetails: React.FunctionComponent = ({ const { application: { capabilities, navigateToApp }, ruleTypeRegistry, - actionTypeRegistry, setBreadcrumbs, chrome, http, @@ -112,14 +107,6 @@ export const RuleDetails: React.FunctionComponent = ({ notifications: { toasts }, } = useKibana().services; - const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); - - const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]); - const [{}, dispatch] = useReducer(ruleReducer, { rule }); - const setInitialRule = (value: Rule) => { - dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); - }; - const [rulesToDelete, setRulesToDelete] = useState([]); const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState(false); @@ -178,7 +165,6 @@ export const RuleDetails: React.FunctionComponent = ({ ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext : false); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const onRunRule = async (id: string) => { await runRule(http, toasts, id); }; @@ -240,10 +226,6 @@ export const RuleDetails: React.FunctionComponent = ({ hasEditButton, ]); - const setRule = async () => { - history.push(getRuleDetailsRoute(rule.id)); - }; - const goToRulesList = () => { history.push(routeToRules); }; @@ -265,17 +247,13 @@ export const RuleDetails: React.FunctionComponent = ({ }; const onEditRuleClick = () => { - if (!isUsingRuleCreateFlyout) { - navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, - state: { - returnApp: 'management', - returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, - }, - }); - } else { - setEditFlyoutVisibility(true); - } + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`, + }, + }); }; const editButton = hasEditButton ? ( @@ -292,19 +270,6 @@ export const RuleDetails: React.FunctionComponent = ({ defaultMessage="Edit" /> - {editFlyoutVisible && ( - { - setInitialRule(rule); - setEditFlyoutVisibility(false); - }} - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - ruleType={ruleType} - onSave={setRule} - /> - )} ) : null; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx deleted file mode 100644 index f553755c430e8..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_close.tsx +++ /dev/null @@ -1,53 +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 React from 'react'; -import { EuiConfirmModal } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; - -interface Props { - onConfirm: () => void; - onCancel: () => void; -} - -export const ConfirmRuleClose: React.FC = ({ onConfirm, onCancel }) => { - return ( - -

- -

-
- ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx deleted file mode 100644 index a3359374f2d10..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/confirm_rule_save.tsx +++ /dev/null @@ -1,52 +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 React from 'react'; -import { EuiConfirmModal } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; - -interface Props { - onConfirm: () => void; - onCancel: () => void; -} - -export const ConfirmRuleSave: React.FC = ({ onConfirm, onCancel }) => { - return ( - -

- -

-
- ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.test.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.test.ts deleted file mode 100644 index 83c7fd93d13d2..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.test.ts +++ /dev/null @@ -1,23 +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 { getInitialInterval } from './get_initial_interval'; -import { DEFAULT_RULE_INTERVAL } from '../../constants'; - -describe('getInitialInterval', () => { - test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is undefined', () => { - expect(getInitialInterval()).toEqual(DEFAULT_RULE_INTERVAL); - }); - - test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is smaller than or equal to default', () => { - expect(getInitialInterval('1m')).toEqual(DEFAULT_RULE_INTERVAL); - }); - - test('should return minimumScheduleInterval if minimumScheduleInterval is greater than default', () => { - expect(getInitialInterval('5m')).toEqual('5m'); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.ts deleted file mode 100644 index 96c76cd688e48..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/get_initial_interval.ts +++ /dev/null @@ -1,19 +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 { parseDuration } from '@kbn/alerting-plugin/common'; -import { DEFAULT_RULE_INTERVAL } from '../../constants'; - -export function getInitialInterval(minimumScheduleInterval?: string) { - if (minimumScheduleInterval) { - // return minimum schedule interval if it is larger than the default - if (parseDuration(minimumScheduleInterval) > parseDuration(DEFAULT_RULE_INTERVAL)) { - return minimumScheduleInterval; - } - } - return DEFAULT_RULE_INTERVAL; -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts deleted file mode 100644 index 17f54d297b9be..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.test.ts +++ /dev/null @@ -1,165 +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 { InitialRule } from './rule_reducer'; -import { hasRuleChanged } from './has_rule_changed'; - -function createRule(overrides = {}): InitialRule { - return { - params: {}, - consumer: 'test', - ruleTypeId: 'test', - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - notifyWhen: 'onActionGroupChange', - ...overrides, - }; -} - -test('should return false for same rule', () => { - const a = createRule(); - expect(hasRuleChanged(a, a, true)).toEqual(false); -}); - -test('should return true for different rule', () => { - const a = createRule(); - const b = createRule({ ruleTypeId: 'differentTest' }); - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare name field', () => { - // name field doesn't exist initially - const a = createRule(); - // set name to actual value - const b = createRule({ name: 'myRule' }); - // set name to different value - const c = createRule({ name: 'anotherRule' }); - // set name to various empty/null/undefined states - const d = createRule({ name: '' }); - const e = createRule({ name: undefined }); - const f = createRule({ name: null }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); - expect(hasRuleChanged(a, c, true)).toEqual(true); - expect(hasRuleChanged(a, d, true)).toEqual(false); - expect(hasRuleChanged(a, e, true)).toEqual(false); - expect(hasRuleChanged(a, f, true)).toEqual(false); - - expect(hasRuleChanged(b, c, true)).toEqual(true); - expect(hasRuleChanged(b, d, true)).toEqual(true); - expect(hasRuleChanged(b, e, true)).toEqual(true); - expect(hasRuleChanged(b, f, true)).toEqual(true); - - expect(hasRuleChanged(c, d, true)).toEqual(true); - expect(hasRuleChanged(c, e, true)).toEqual(true); - expect(hasRuleChanged(c, f, true)).toEqual(true); - - expect(hasRuleChanged(d, e, true)).toEqual(false); - expect(hasRuleChanged(d, f, true)).toEqual(false); -}); - -test('should correctly compare ruleTypeId field', () => { - const a = createRule(); - - // set ruleTypeId to different value - const b = createRule({ ruleTypeId: 'myRuleId' }); - // set ruleTypeId to various empty/null/undefined states - const c = createRule({ ruleTypeId: '' }); - const d = createRule({ ruleTypeId: undefined }); - const e = createRule({ ruleTypeId: null }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); - expect(hasRuleChanged(a, c, true)).toEqual(true); - expect(hasRuleChanged(a, d, true)).toEqual(true); - expect(hasRuleChanged(a, e, true)).toEqual(true); - - expect(hasRuleChanged(b, c, true)).toEqual(true); - expect(hasRuleChanged(b, d, true)).toEqual(true); - expect(hasRuleChanged(b, e, true)).toEqual(true); - - expect(hasRuleChanged(c, d, true)).toEqual(false); - expect(hasRuleChanged(c, e, true)).toEqual(false); - expect(hasRuleChanged(d, e, true)).toEqual(false); -}); - -test('should correctly compare throttle field', () => { - // throttle field doesn't exist initially - const a = createRule(); - // set throttle to actual value - const b = createRule({ throttle: '1m' }); - // set throttle to different value - const c = createRule({ throttle: '1h' }); - // set throttle to various empty/null/undefined states - const d = createRule({ throttle: '' }); - const e = createRule({ throttle: undefined }); - const f = createRule({ throttle: null }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); - expect(hasRuleChanged(a, c, true)).toEqual(true); - expect(hasRuleChanged(a, d, true)).toEqual(false); - expect(hasRuleChanged(a, e, true)).toEqual(false); - expect(hasRuleChanged(a, f, true)).toEqual(false); - - expect(hasRuleChanged(b, c, true)).toEqual(true); - expect(hasRuleChanged(b, d, true)).toEqual(true); - expect(hasRuleChanged(b, e, true)).toEqual(true); - expect(hasRuleChanged(b, f, true)).toEqual(true); - - expect(hasRuleChanged(c, d, true)).toEqual(true); - expect(hasRuleChanged(c, e, true)).toEqual(true); - expect(hasRuleChanged(c, f, true)).toEqual(true); - - expect(hasRuleChanged(d, e, true)).toEqual(false); - expect(hasRuleChanged(d, f, true)).toEqual(false); -}); - -test('should correctly compare tags field', () => { - const a = createRule(); - const b = createRule({ tags: ['first'] }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare schedule field', () => { - const a = createRule(); - const b = createRule({ schedule: { interval: '3h' } }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare actions field', () => { - const a = createRule(); - const b = createRule({ - actions: [{ actionTypeId: 'action', group: 'group', id: 'actionId', params: {} }], - }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); - -test('should skip comparing params field if compareParams=false', () => { - const a = createRule(); - const b = createRule({ params: { newParam: 'value' } }); - - expect(hasRuleChanged(a, b, false)).toEqual(false); -}); - -test('should correctly compare params field if compareParams=true', () => { - const a = createRule(); - const b = createRule({ params: { newParam: 'value' } }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); - -test('should correctly compare notifyWhen field', () => { - const a = createRule(); - const b = createRule({ notifyWhen: 'onActiveAlert' }); - - expect(hasRuleChanged(a, b, true)).toEqual(true); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts deleted file mode 100644 index 4b3920e9a241f..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/has_rule_changed.ts +++ /dev/null @@ -1,42 +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 deepEqual from 'fast-deep-equal'; -import { pick } from 'lodash'; -import { RuleTypeParams } from '../../../types'; -import { InitialRule } from './rule_reducer'; - -const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions']; - -function getNonNullCompareFields(rule: InitialRule) { - const { name, ruleTypeId, throttle, notifyWhen } = rule; - return { - ...(!!(name && name.length > 0) ? { name } : {}), - ...(!!(ruleTypeId && ruleTypeId.length > 0) ? { ruleTypeId } : {}), - ...(!!(throttle && throttle.length > 0) ? { throttle } : {}), - ...(!!(notifyWhen && notifyWhen.length > 0) ? { notifyWhen } : {}), - }; -} - -export function hasRuleChanged(a: InitialRule, b: InitialRule, compareParams: boolean) { - // Deep compare these fields - let objectsAreEqual = deepEqual(pick(a, DEEP_COMPARE_FIELDS), pick(b, DEEP_COMPARE_FIELDS)); - if (compareParams) { - objectsAreEqual = objectsAreEqual && deepEqual(a.params, b.params); - } - - const nonNullCompareFieldsAreEqual = deepEqual( - getNonNullCompareFields(a), - getNonNullCompareFields(b) - ); - - return !objectsAreEqual || !nonNullCompareFieldsAreEqual; -} - -export function haveRuleParamsChanged(a: RuleTypeParams, b: RuleTypeParams) { - return !deepEqual(a, b); -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/index.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/index.tsx deleted file mode 100644 index 1a99e346ed808..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/index.tsx +++ /dev/null @@ -1,19 +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 { lazy } from 'react'; -import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; -import type { RuleAddComponent } from './rule_add'; -import type { RuleEditComponent } from './rule_edit'; - -export const RuleAdd = suspendedComponentWithProps( - lazy(() => import('./rule_add')) -) as RuleAddComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component - -export const RuleEdit = suspendedComponentWithProps( - lazy(() => import('./rule_edit')) -) as RuleEditComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx deleted file mode 100644 index a8e8e9df322d5..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ /dev/null @@ -1,623 +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 { v4 as uuidv4 } from 'uuid'; -import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { render, screen, within } from '@testing-library/react'; - -import { EuiFormLabel } from '@elastic/eui'; -import { coreMock } from '@kbn/core/public/mocks'; -import RuleAdd from './rule_add'; -import { createRule } from '@kbn/response-ops-rule-form/src/common/apis/create_rule'; - -import { fetchAlertingFrameworkHealth as fetchAlertingFrameworkHealth } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { AlertConsumers, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; -import { - Rule, - RuleAddProps, - RuleFlyoutCloseReason, - GenericValidationResult, - ValidationResult, - RuleCreationValidConsumer, - RuleType, - RuleTypeModel, -} from '../../../types'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common'; -import { useKibana } from '../../../common/lib/kibana'; - -import { fetchUiConfig } from '@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config'; -import { fetchUiHealthStatus } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status'; -import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { waitFor } from '@testing-library/react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import userEvent from '@testing-library/user-event'; -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../lib/rule_api/rule_types', () => ({ - loadRuleTypes: jest.fn(), -})); -jest.mock('@kbn/response-ops-rule-form/src/common/apis/create_rule', () => ({ - createRule: jest.fn(), -})); -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health', () => ({ - fetchAlertingFrameworkHealth: jest.fn(() => ({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, - })), -})); - -jest.mock('@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config', () => ({ - fetchUiConfig: jest.fn(), -})); - -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => ({ - fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })), -})); - -jest.mock('../../lib/action_connector_api', () => ({ - loadActionTypes: jest.fn(), - loadAllActions: jest.fn(), -})); - -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ - fetchFlappingSettings: jest.fn().mockResolvedValue({ - lookBackWindow: 20, - statusChangeThreshold: 20, - }), -})); - -const actionTypeRegistry = actionTypeRegistryMock.create(); -const ruleTypeRegistry = ruleTypeRegistryMock.create(); - -export const TestExpression: FunctionComponent = () => { - return ( - - - - ); -}; - -describe('rule_add', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - jest.resetAllMocks(); - }); - - async function setup({ - initialValues, - onClose = jest.fn(), - defaultScheduleInterval, - ruleTypeId, - actionsShow = false, - validConsumers, - ruleTypesOverwrite, - ruleTypeModelOverwrite, - }: { - initialValues?: Partial; - onClose?: RuleAddProps['onClose']; - defaultScheduleInterval?: string; - ruleTypeId?: string; - actionsShow?: boolean; - validConsumers?: RuleCreationValidConsumer[]; - ruleTypesOverwrite?: RuleType[]; - ruleTypeModelOverwrite?: RuleTypeModel; - }) { - const useKibanaMock = useKibana as jest.Mocked; - const mocks = coreMock.createSetup(); - const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types'); - - const ruleTypes = ruleTypesOverwrite || [ - { - id: 'my-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - defaultScheduleInterval, - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - }, - ]; - loadRuleTypes.mockResolvedValue(ruleTypes); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - rulesSettings: { - writeFlappingSettingsUI: true, - }, - rules: { - show: true, - save: true, - delete: true, - }, - actions: { - show: actionsShow, - }, - }; - - mocks.http.get.mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, - }); - - const ruleType = ruleTypeModelOverwrite || { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }; - - const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'my-action-type', - iconClass: 'test', - selectMessage: 'test', - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); - actionTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.list.mockReturnValue([ruleType]); - ruleTypeRegistry.get.mockReturnValue(ruleType); - ruleTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.list.mockReturnValue([actionTypeModel]); - actionTypeRegistry.has.mockReturnValue(true); - - return { - consumer: ALERTING_FEATURE_ID, - onClose, - initialValues, - onSave: () => { - return new Promise(() => {}); - }, - actionTypeRegistry, - ruleTypeRegistry, - metadata: { test: 'some value', fields: ['test'] }, - ruleTypeId, - validConsumers, - }; - } - - it('renders rule add flyout', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - - const onClose = jest.fn(); - const props = await setup({ - initialValues: {}, - onClose, - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('addRuleFlyoutTitle')).toBeInTheDocument(); - - expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument(); - expect(await screen.findByTestId('showRequestButton')).toBeInTheDocument(); - - await userEvent.click(await screen.findByTestId('cancelSaveRuleButton')); - expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.CANCELED, { - fields: ['test'], - test: 'some value', - }); - }); - - it('renders selection of rule types to pick in the modal', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const onClose = jest.fn(); - const props = await setup({ - initialValues: {}, - onClose, - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('my-rule-type-SelectOption')).toBeInTheDocument(); - - expect(await screen.findByText('Test')).toBeInTheDocument(); - expect(await screen.findByText('test')).toBeInTheDocument(); - }); - - it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const onClose = jest.fn(); - - const props = await setup({ - initialValues: {}, - onClose, - ruleTypeId: 'my-rule-type', - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('ruleNameInput')).toBeInTheDocument(); - - await userEvent.type(await screen.findByTestId('ruleNameInput'), 'my[Space]rule[Space]type'); - - expect(await screen.findByTestId('ruleNameInput')).toHaveValue('my rule type'); - expect(await screen.findByTestId('comboBoxSearchInput')).toHaveValue(''); - expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m'); - - await userEvent.click(await screen.findByTestId('cancelSaveRuleButton')); - - expect(onClose).not.toHaveBeenCalled(); - expect(await screen.findByTestId('confirmRuleCloseModal')).toBeInTheDocument(); - }); - - it('renders rule add flyout with initial values', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const onClose = jest.fn(); - const props = await setup({ - initialValues: { - name: 'Simple status rule', - tags: ['uptime', 'logs'], - schedule: { - interval: '1h', - }, - }, - onClose, - ruleTypeId: 'my-rule-type', - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('ruleNameInput')).toHaveValue('Simple status rule'); - - expect( - await within(await screen.findByTestId('tagsComboBox')).findByText('uptime') - ).toBeInTheDocument(); - expect( - await within(await screen.findByTestId('tagsComboBox')).findByText('logs') - ).toBeInTheDocument(); - - expect(await screen.findByTestId('intervalInput')).toHaveValue(1); - expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('h'); - }); - - it('renders rule add flyout with DEFAULT_RULE_INTERVAL if no initialValues specified and no minimumScheduleInterval', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({}); - const props = await setup({ ruleTypeId: 'my-rule-type' }); - - render( - - - - - - ); - - expect(await screen.findByTestId('intervalInput')).toHaveValue(1); - - expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m'); - }); - - it('renders rule add flyout with minimumScheduleInterval if minimumScheduleInterval is greater than DEFAULT_RULE_INTERVAL', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '5m', enforce: false }, - }); - const props = await setup({ ruleTypeId: 'my-rule-type' }); - - render( - - - - - - ); - - expect(await screen.findByTestId('intervalInput')).toHaveValue(5); - - expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m'); - }); - - it('emit an onClose event when the rule is saved', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const onClose = jest.fn(); - const rule = mockRule(); - - (createRule as jest.MockedFunction).mockResolvedValue(rule); - - const props = await setup({ - initialValues: { - name: 'Simple status rule', - ruleTypeId: 'my-rule-type', - tags: ['uptime', 'logs'], - schedule: { - interval: '1h', - }, - }, - onClose, - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument(); - - await userEvent.click(await screen.findByTestId('saveRuleButton')); - - await waitFor(() => { - return expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED, { - test: 'some value', - fields: ['test'], - }); - }); - }); - - it('should set consumer automatically if only 1 authorized consumer exists', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const onClose = jest.fn(); - const props = await setup({ - initialValues: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - tags: ['uptime', 'logs'], - schedule: { - interval: '1h', - }, - }, - onClose, - ruleTypesOverwrite: [ - { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - }, - ], - ruleTypeModelOverwrite: { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS], - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument(); - - await waitFor(async () => { - await userEvent.click(await screen.findByTestId('saveRuleButton')); - return expect(createRule).toHaveBeenLastCalledWith( - expect.objectContaining({ - rule: expect.objectContaining({ - consumer: 'logs', - }), - }) - ); - }); - }); - - it('should enforce any default interval', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - const props = await setup({ - initialValues: { ruleTypeId: 'my-rule-type' }, - onClose: jest.fn(), - defaultScheduleInterval: '3h', - ruleTypeId: 'my-rule-type', - actionsShow: true, - }); - - render( - - - - - - ); - - expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('h'); - - expect(await screen.findByTestId('intervalInput')).toHaveValue(3); - }); - - it('should load connectors and connector types when there is a pre-selected rule type', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - - const props = await setup({ - initialValues: {}, - onClose: jest.fn(), - ruleTypeId: 'my-rule-type', - actionsShow: true, - }); - - render( - - - - - - ); - - await waitFor(() => { - expect(fetchUiHealthStatus).toHaveBeenCalledTimes(1); - expect(fetchAlertingFrameworkHealth).toHaveBeenCalledTimes(1); - expect(loadActionTypes).toHaveBeenCalledTimes(1); - expect(loadAllActions).toHaveBeenCalledTimes(1); - }); - }); - - it('should not load connectors and connector types when there is not an encryptionKey', async () => { - (fetchUiConfig as jest.Mock).mockResolvedValue({ - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - (fetchAlertingFrameworkHealth as jest.Mock).mockResolvedValue({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: false, - }); - - const props = await setup({ - initialValues: {}, - onClose: jest.fn(), - ruleTypeId: 'my-rule-type', - actionsShow: true, - }); - - render( - - - - - - ); - - await waitFor(() => { - expect(fetchUiHealthStatus).toHaveBeenCalledTimes(1); - expect(fetchAlertingFrameworkHealth).toHaveBeenCalledTimes(1); - expect(loadActionTypes).not.toHaveBeenCalled(); - expect(loadAllActions).not.toHaveBeenCalled(); - }); - - expect( - await screen.findByText('You must configure an encryption key to use Alerting.', { - collapseWhitespace: false, - }) - ).toBeInTheDocument(); - }); -}); - -function mockRule(overloads: Partial = {}): Rule { - return { - id: uuidv4(), - enabled: true, - name: `rule-${uuidv4()}`, - tags: [], - ruleTypeId: '.noop', - consumer: 'consumer', - schedule: { interval: '1m' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - revision: 0, - ...overloads, - }; -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx deleted file mode 100644 index 419f4c7696379..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ /dev/null @@ -1,419 +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 { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiPortal, EuiTitle } from '@elastic/eui'; -import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { - CreateRuleBody, - createRule, - fetchUiConfig as triggersActionsUiConfig, -} from '@kbn/response-ops-rule-form'; -import { isEmpty } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; -import { - IErrorObject, - Rule, - RuleAddProps, - RuleCreationValidConsumer, - RuleFlyoutCloseReason, - RuleTypeIndex, - RuleTypeMetaData, - RuleTypeParams, - RuleUpdates, - TriggersActionsUiConfig, -} from '../../../types'; -import { HealthCheck } from '../../components/health_check'; -import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content'; -import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; -import { HealthContextProvider } from '../../context/health_context'; -import { hasShowActionsCapability } from '../../lib/capabilities'; -import { loadRuleTypes } from '../../lib/rule_api/rule_types'; -import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; -import { ConfirmRuleClose } from './confirm_rule_close'; -import { ConfirmRuleSave } from './confirm_rule_save'; -import { getInitialInterval } from './get_initial_interval'; -import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed'; -import RuleAddFooter from './rule_add_footer'; -import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; -import { RuleForm } from './rule_form'; -import { InitialRule, getRuleReducer } from './rule_reducer'; -import { ShowRequestModal } from './show_request_modal'; - -const defaultCreateRuleErrorMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText', - { - defaultMessage: 'Cannot create rule.', - } -); - -export type RuleAddComponent = typeof RuleAdd; - -const RuleAdd = < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData ->({ - consumer, - ruleTypeRegistry, - actionTypeRegistry, - onClose, - canChangeTrigger, - ruleTypeId, - initialValues, - reloadRules, - onSave, - hideGrouping, - hideInterval, - metadata: initialMetadata, - filteredRuleTypes, - validConsumers, - useRuleProducer, - initialSelectedConsumer, - ...props -}: RuleAddProps) => { - const onSaveHandler = onSave ?? reloadRules; - const [metadata, setMetadata] = useState(initialMetadata); - const onChangeMetaData = useCallback((newMetadata: any) => setMetadata(newMetadata), []); - - const initialRule: InitialRule = useMemo(() => { - return { - params: {}, - consumer, - ruleTypeId, - schedule: { - interval: DEFAULT_RULE_INTERVAL, - }, - actions: [], - tags: [], - ...(initialValues ? initialValues : {}), - }; - }, [ruleTypeId, consumer, initialValues]); - const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]); - const [{ rule }, dispatch] = useReducer(ruleReducer, { - rule: initialRule, - }); - const [config, setConfig] = useState({ isUsingSecurity: false }); - const [initialRuleParams, setInitialRuleParams] = useState({}); - const [isSaving, setIsSaving] = useState(false); - const [isConfirmRuleSaveModalOpen, setIsConfirmRuleSaveModalOpen] = useState(false); - const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState(false); - const [isShowRequestModalOpen, setIsShowRequestModalOpen] = useState(false); - const [ruleTypeIndex, setRuleTypeIndex] = useState( - props.ruleTypeIndex - ); - const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); - const [isRuleValid, setIsRuleValid] = useState(false); - - const selectableConsumer = useMemo( - () => rule.ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule.ruleTypeId), - [rule] - ); - const [selectedConsumer, setSelectedConsumer] = useState< - RuleCreationValidConsumer | null | undefined - >(selectableConsumer ? initialSelectedConsumer : null); - - const setRule = (value: InitialRule) => { - dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); - }; - - const setRuleProperty = (key: Key, value: Rule[Key] | null) => { - dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); - }; - - const { - http, - notifications: { toasts }, - application: { capabilities }, - isServerless, - ...startServices - } = useKibana().services; - - const canShowActions = hasShowActionsCapability(capabilities); - - useEffect(() => { - (async () => { - setConfig(await triggersActionsUiConfig({ http })); - })(); - }, [http]); - - useEffect(() => { - if (ruleTypeId) { - setRuleProperty('ruleTypeId', ruleTypeId); - } - }, [ruleTypeId]); - - useEffect(() => { - if (!props.ruleTypeIndex) { - (async () => { - const ruleTypes = await loadRuleTypes({ http }); - const index: RuleTypeIndex = new Map(); - for (const ruleType of ruleTypes) { - index.set(ruleType.id, ruleType); - } - setRuleTypeIndex(index); - })(); - } - }, [props.ruleTypeIndex, http]); - - useEffect(() => { - if (isEmpty(rule.params) && !isEmpty(initialRuleParams)) { - // rule params are explicitly cleared when the rule type is cleared. - // clear the "initial" params in order to capture the - // default when a new rule type is selected - setInitialRuleParams({}); - } else if (isEmpty(initialRuleParams)) { - // captures the first change to the rule params, - // when consumers set a default value for the rule params - setInitialRuleParams(rule.params); - } - }, [rule.params, initialRuleParams]); - - const [ruleActionsErrors, setRuleActionsErrors] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - (async () => { - setIsLoading(true); - const res = await getRuleActionErrors(rule.actions, actionTypeRegistry); - setIsLoading(false); - setRuleActionsErrors([...res]); - })(); - }, [rule.actions, actionTypeRegistry]); - - useEffect(() => { - if (config.minimumScheduleInterval && !initialValues?.schedule?.interval) { - setRuleProperty('schedule', { - interval: getInitialInterval(config.minimumScheduleInterval.value), - }); - } - }, [config.minimumScheduleInterval, initialValues]); - - useEffect(() => { - if (rule.ruleTypeId && ruleTypeIndex) { - const type = ruleTypeIndex.get(rule.ruleTypeId); - if (type?.defaultScheduleInterval && !changedFromDefaultInterval) { - setRuleProperty('schedule', { interval: type.defaultScheduleInterval }); - } - } - }, [rule.ruleTypeId, ruleTypeIndex, rule.schedule.interval, changedFromDefaultInterval]); - - useEffect(() => { - if (rule.schedule.interval !== DEFAULT_RULE_INTERVAL && !changedFromDefaultInterval) { - setChangedFromDefaultInterval(true); - } - }, [rule.schedule.interval, changedFromDefaultInterval]); - - const checkForChangesAndCloseFlyout = () => { - if ( - hasRuleChanged(rule, initialRule, false) || - haveRuleParamsChanged(rule.params, initialRuleParams) - ) { - setIsConfirmRuleCloseModalOpen(true); - } else { - onClose(RuleFlyoutCloseReason.CANCELED, metadata); - } - }; - - const saveRuleAndCloseFlyout = async () => { - const savedRule = await onSaveRule(); - setIsSaving(false); - if (savedRule) { - onClose(RuleFlyoutCloseReason.SAVED, metadata); - if (onSaveHandler) { - onSaveHandler(metadata); - } - } - }; - - const ruleType = rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null; - const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = useMemo( - () => - getRuleErrors( - { - ...rule, - ...(selectableConsumer && selectedConsumer !== undefined - ? { consumer: selectedConsumer } - : {}), - } as Rule, - ruleType, - config, - actionTypeRegistry, - isServerless - ), - [rule, selectableConsumer, selectedConsumer, ruleType, config, actionTypeRegistry, isServerless] - ); - - // Confirm before saving if user is able to add actions but hasn't added any to this rule - const shouldConfirmSave = canShowActions && rule.actions?.length === 0; - - async function onSaveRule(): Promise { - try { - const { flapping, ...restRule } = rule; - const newRule = await createRule({ - http, - rule: { - ...restRule, - ...(selectableConsumer && selectedConsumer ? { consumer: selectedConsumer } : {}), - ...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}), - } as CreateRuleBody, - }); - toasts.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText', { - defaultMessage: 'Created rule "{ruleName}"', - values: { - ruleName: newRule.name, - }, - }) - ); - return newRule; - } catch (errorRes) { - const message = parseRuleCircuitBreakerErrorMessage( - errorRes.body?.message || defaultCreateRuleErrorMessage - ); - toasts.addDanger({ - title: message.summary, - ...(message.details && { - text: toMountPoint( - {message.details}, - startServices - ), - }), - }); - } - } - - useEffect(() => { - setIsRuleValid(isValidRule(rule, ruleErrors, ruleActionsErrors)); - }, [rule, ruleErrors, ruleActionsErrors]); - - return ( - - - - -

- -

-
-
- - - - - - { - setIsSaving(true); - if (isLoading || !isValidRule(rule, ruleErrors, ruleActionsErrors)) { - setRule( - getRuleWithInvalidatedFields( - rule as Rule, - ruleParamsErrors, - ruleBaseErrors, - ruleActionsErrors - ) - ); - setIsSaving(false); - return; - } - if (shouldConfirmSave) { - setIsConfirmRuleSaveModalOpen(true); - } else { - await saveRuleAndCloseFlyout(); - } - }} - onCancel={checkForChangesAndCloseFlyout} - onShowRequest={() => { - setIsShowRequestModalOpen(true); - }} - /> - - - {isConfirmRuleSaveModalOpen && ( - { - setIsConfirmRuleSaveModalOpen(false); - await saveRuleAndCloseFlyout(); - }} - onCancel={() => { - setIsSaving(false); - setIsConfirmRuleSaveModalOpen(false); - }} - /> - )} - {isConfirmRuleCloseModalOpen && ( - { - setIsConfirmRuleCloseModalOpen(false); - onClose(RuleFlyoutCloseReason.CANCELED, metadata); - }} - onCancel={() => { - setIsConfirmRuleCloseModalOpen(false); - }} - /> - )} - {isShowRequestModalOpen && ( - { - setIsShowRequestModalOpen(false); - }} - rule={ - { - ...rule, - ...(selectableConsumer && selectedConsumer ? { consumer: selectedConsumer } : {}), - } as RuleUpdates - } - /> - )} -
-
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleAdd as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx deleted file mode 100644 index 1e9c5eb1ca485..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_add_footer.tsx +++ /dev/null @@ -1,100 +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 React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, - EuiLoadingSpinner, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useHealthContext } from '../../context/health_context'; - -interface RuleAddFooterProps { - isSaving: boolean; - isFormLoading: boolean; - isRuleValid: boolean; - onSave: () => void; - onCancel: () => void; - onShowRequest: () => void; -} - -export const RuleAddFooter = ({ - isSaving, - onSave, - onCancel, - onShowRequest, - isFormLoading, - isRuleValid, -}: RuleAddFooterProps) => { - const { loadingHealthCheck } = useHealthContext(); - - return ( - - - - - {i18n.translate('xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - {isFormLoading ? ( - - - - - ) : ( - <> - )} - - - - - - - - - - - - - - - - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleAddFooter as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx deleted file mode 100644 index bde4596a518fb..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.test.tsx +++ /dev/null @@ -1,263 +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 * as React from 'react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { act } from 'react-dom/test-utils'; -import { ReactWrapper } from 'enzyme'; -import { RuleConditions, ActionGroupWithCondition } from './rule_conditions'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiTitle, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiButtonEmpty, -} from '@elastic/eui'; - -describe('rule_conditions', () => { - async function setup(element: React.ReactElement): Promise> { - const wrapper = mountWithIntl(element); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - return wrapper; - } - - it('renders with custom headline', async () => { - const wrapper = await setup( - - ); - - expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot( - `"xpack.triggersActionsUI.sections.ruleForm.conditions.title"` - ); - expect( - wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage') - ).toMatchInlineSnapshot(`"Conditions:"`); - - expect(wrapper.find('[data-test-subj="ruleConditionsHeadline"]').get(0)).toMatchInlineSnapshot(` - - Set different threshold with their own status - - `); - }); - - it('renders any action group with conditions on it', async () => { - const ConditionForm = ({ - actionGroup, - }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; - }) => { - return ( - - ID - {actionGroup?.id} - Name - {actionGroup?.name} - SomeProp - - {actionGroup?.conditions?.someProp} - - - ); - }; - - const wrapper = await setup( - - - - ); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) - .toMatchInlineSnapshot(` - - default - - `); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) - .toMatchInlineSnapshot(` - - Default - - `); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2)) - .toMatchInlineSnapshot(` - - my prop value - - `); - }); - - it('doesnt render action group without conditions', async () => { - const ConditionForm = ({ - actionGroup, - }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; - }) => { - return ( - - ID - {actionGroup?.id} - - ); - }; - - const wrapper = await setup( - - - - ); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0)) - .toMatchInlineSnapshot(` - - default - - `); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1)) - .toMatchInlineSnapshot(` - - shouldRender - - `); - - expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2); - }); - - it('render add buttons for action group without conditions', async () => { - const onInitializeConditionsFor = jest.fn(); - - const ConditionForm = ({ - actionGroup, - }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; - }) => { - return ( - - ID - {actionGroup?.id} - - ); - }; - - const wrapper = await setup( - - - - ); - - expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` - - Should Render A Link - - `); - wrapper.find(EuiButtonEmpty).simulate('click'); - - expect(onInitializeConditionsFor).toHaveBeenCalledWith({ - id: 'shouldRenderLink', - name: 'Should Render A Link', - }); - }); - - it('passes in any additional props the container passes in', async () => { - const callbackProp = jest.fn(); - - const ConditionForm = ({ - actionGroup, - someCallbackProp, - }: { - actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>; - someCallbackProp: ( - actionGroup: ActionGroupWithCondition<{ someProp: string }, string> - ) => void; - }) => { - if (!actionGroup) { - return
; - } - - // call callback when the actionGroup is available - someCallbackProp(actionGroup); - return ( - - ID - {actionGroup?.id} - Name - {actionGroup?.name} - SomeProp - - {actionGroup?.conditions?.someProp} - - - ); - }; - - await setup( - - - - ); - - expect(callbackProp).toHaveBeenCalledWith({ - id: 'default', - name: 'Default', - conditions: { someProp: 'my prop value' }, - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx deleted file mode 100644 index c9e95118698a5..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions.tsx +++ /dev/null @@ -1,130 +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 React, { PropsWithChildren } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFlexItem, EuiText, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui'; -import { partition } from 'lodash'; -import { ActionGroup, getBuiltinActionGroups } from '@kbn/alerting-plugin/common'; - -const BUILT_IN_ACTION_GROUPS: Set = new Set(getBuiltinActionGroups().map(({ id }) => id)); - -export type ActionGroupWithCondition< - T, - ActionGroupIds extends string -> = ActionGroup & - ( - | // allow isRequired=false with or without conditions - { - conditions?: T; - isRequired?: false; - } - // but if isRequired=true then conditions must be specified - | { - conditions: T; - isRequired: true; - } - ); - -export interface RuleConditionsProps { - headline?: string; - actionGroups: Array>; - onInitializeConditionsFor?: ( - actionGroup: ActionGroupWithCondition - ) => void; - onResetConditionsFor?: ( - actionGroup: ActionGroupWithCondition - ) => void; - includeBuiltInActionGroups?: boolean; -} - -export const RuleConditions = ({ - headline, - actionGroups, - onInitializeConditionsFor, - onResetConditionsFor, - includeBuiltInActionGroups = false, - children, -}: PropsWithChildren>) => { - const [withConditions, withoutConditions] = partition( - includeBuiltInActionGroups - ? actionGroups - : actionGroups.filter(({ id }) => !BUILT_IN_ACTION_GROUPS.has(id)), - (actionGroup) => Object.hasOwn(actionGroup, 'conditions') - ); - - return ( - - - - - -
- -
-
- {headline && ( - - - {headline} - - - )} -
-
-
- - - {withConditions.map((actionGroup) => ( - - {React.isValidElement(children) && - React.cloneElement( - React.Children.only(children), - onResetConditionsFor - ? { - // @ts-expect-error upgrade typescript v4.9.5 - actionGroup, - onResetConditionsFor, - } - : { actionGroup } - )} - - ))} - {onInitializeConditionsFor && withoutConditions.length > 0 && ( - - - - - - {withoutConditions.map((actionGroup) => ( - - onInitializeConditionsFor(actionGroup)} - > - {actionGroup.name} - - - ))} - - - )} - - -
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleConditions as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.test.tsx deleted file mode 100644 index 2c4dddbce5a5a..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.test.tsx +++ /dev/null @@ -1,100 +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 * as React from 'react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { act } from 'react-dom/test-utils'; -import { ReactWrapper } from 'enzyme'; -import { RuleConditionsGroup } from './rule_conditions_group'; -import { EuiFormRow, EuiButtonIcon } from '@elastic/eui'; - -describe('rule_conditions_group', () => { - async function setup(element: React.ReactElement): Promise> { - const wrapper = mountWithIntl(element); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - return wrapper; - } - - it('renders with actionGroup name as label', async () => { - const InnerComponent = () =>
{'inner component'}
; - const wrapper = await setup( - - - - ); - - expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(` - - - My Group - - - `); - expect(wrapper.find(InnerComponent).prop('actionGroup')).toMatchInlineSnapshot(` - Object { - "id": "myGroup", - "name": "My Group", - } - `); - }); - - it('renders a reset button when onResetConditionsFor is specified', async () => { - const onResetConditionsFor = jest.fn(); - const wrapper = await setup( - -
{'inner component'}
-
- ); - - expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`); - - wrapper.find(EuiButtonIcon).simulate('click'); - - expect(onResetConditionsFor).toHaveBeenCalledWith({ - id: 'myGroup', - name: 'My Group', - }); - }); - - it('shouldnt render a reset button when isRequired is true', async () => { - const onResetConditionsFor = jest.fn(); - const wrapper = await setup( - -
{'inner component'}
-
- ); - - expect(wrapper.find(EuiButtonIcon).length).toEqual(0); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx deleted file mode 100644 index 35f56410269a8..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_conditions_group.tsx +++ /dev/null @@ -1,66 +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 React, { PropsWithChildren } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; -import { RuleConditionsProps, ActionGroupWithCondition } from './rule_conditions'; - -export type RuleConditionsGroupProps = { - actionGroup?: ActionGroupWithCondition; -} & Pick, 'onResetConditionsFor'>; - -export const RuleConditionsGroup = ({ - actionGroup, - onResetConditionsFor, - children, - ...otherProps -}: PropsWithChildren>) => { - if (!actionGroup) { - return null; - } - - return ( - - {actionGroup.name} - - } - fullWidth - labelAppend={ - onResetConditionsFor && - !actionGroup.isRequired && ( - onResetConditionsFor(actionGroup)} - /> - ) - } - > - {React.isValidElement(children) ? ( - React.cloneElement(React.Children.only(children), { - // @ts-expect-error upgrade typescript v4.9.5 - actionGroup, - ...otherProps, - }) - ) : ( - <> - )} - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleConditionsGroup as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx deleted file mode 100644 index 925c2bb2edeb4..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx +++ /dev/null @@ -1,270 +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 * as React from 'react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { act } from 'react-dom/test-utils'; -import { coreMock } from '@kbn/core/public/mocks'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ValidationResult, Rule, GenericValidationResult } from '../../../types'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { ReactWrapper } from 'enzyme'; -import RuleEdit from './rule_edit'; -import { useKibana } from '../../../common/lib/kibana'; -import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -jest.mock('../../../common/lib/kibana'); -const actionTypeRegistry = actionTypeRegistryMock.create(); -const ruleTypeRegistry = ruleTypeRegistryMock.create(); -const useKibanaMock = useKibana as jest.Mocked; - -jest.mock('../../lib/rule_api/rule_types', () => ({ - loadRuleTypes: jest.fn(), -})); -jest.mock('@kbn/response-ops-rule-form/src/common/apis/update_rule', () => ({ - updateRule: jest.fn().mockRejectedValue({ body: { message: 'Fail message' } }), -})); -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health', () => ({ - fetchAlertingFrameworkHealth: jest.fn(() => ({ - isSufficientlySecure: true, - hasPermanentEncryptionKey: true, - })), -})); - -jest.mock('@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config', () => ({ - fetchUiConfig: jest.fn().mockResolvedValue({ - isUsingSecurity: true, - minimumScheduleInterval: { value: '1m', enforce: false }, - }), -})); - -jest.mock('./rule_errors', () => ({ - getRuleActionErrors: jest.fn().mockImplementation(() => { - return []; - }), - getRuleErrors: jest.fn().mockImplementation(() => ({ - ruleParamsErrors: {}, - ruleBaseErrors: {}, - ruleErrors: { - name: new Array(), - 'schedule.interval': new Array(), - ruleTypeId: new Array(), - actionConnectors: new Array(), - }, - })), - isValidRule: jest.fn(), -})); - -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => ({ - fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })), -})); - -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ - fetchFlappingSettings: jest.fn().mockResolvedValue({ - lookBackWindow: 20, - statusChangeThreshold: 20, - }), -})); - -describe('rule_edit', () => { - let wrapper: ReactWrapper; - let mockedCoreSetup: ReturnType; - - beforeEach(() => { - mockedCoreSetup = coreMock.createSetup(); - }); - - async function setup(initialRuleFields = {}) { - const [ - { - application: { capabilities }, - }, - ] = await mockedCoreSetup.getStartServices(); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - rulesSettings: { - writeFlappingSettingsUI: true, - }, - rules: { - show: true, - save: true, - delete: true, - execute: true, - }, - }; - - const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types'); - const ruleTypes = [ - { - id: 'my-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - }, - ]; - const ruleType = { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: false, - }; - - const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'my-action-type', - iconClass: 'test', - selectMessage: 'test', - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - loadRuleTypes.mockResolvedValue(ruleTypes); - const rule: Rule = { - id: 'ab5661e0-197e-45ee-b477-302d89193b5e', - params: { - aggType: 'average', - threshold: [1000, 5000], - index: 'kibana_sample_data_flights', - timeField: 'timestamp', - aggField: 'DistanceMiles', - window: '1s', - comparator: 'between', - }, - consumer: 'rules', - ruleTypeId: 'my-rule-type', - enabled: false, - schedule: { interval: '1m' }, - actions: [ - { - actionTypeId: 'my-action-type', - group: 'threshold met', - params: { message: 'Rule [{{ctx.metadata.name}}] has exceeded the threshold' }, - id: '917f5d41-fbc4-4056-a8ad-ac592f7dcee2', - }, - ], - tags: [], - name: 'test rule', - throttle: null, - notifyWhen: null, - apiKeyOwner: null, - createdBy: 'elastic', - updatedBy: 'elastic', - createdAt: new Date(), - muteAll: false, - mutedInstanceIds: [], - updatedAt: new Date(), - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - revision: 0, - ...initialRuleFields, - }; - actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); - actionTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.list.mockReturnValue([ruleType]); - ruleTypeRegistry.get.mockReturnValue(ruleType); - ruleTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.list.mockReturnValue([actionTypeModel]); - actionTypeRegistry.has.mockReturnValue(true); - - wrapper = mountWithIntl( - - {}} - initialRule={rule} - onSave={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - /> - - ); - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders rule edit flyout', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="editRuleFlyoutTitle"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveEditedRuleButton"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="showEditedRequestButton"]').exists()).toBeTruthy(); - }); - - it('displays a toast message on save for server errors', async () => { - const { isValidRule } = jest.requireMock('./rule_errors'); - (isValidRule as jest.Mock).mockImplementation(() => { - return true; - }); - await setup({ name: undefined }); - - await act(async () => { - wrapper.find('[data-test-subj="saveEditedRuleButton"]').last().simulate('click'); - }); - expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith({ - title: 'Fail message', - }); - }); - - it('should pass in the config into `getRuleErrors`', async () => { - const { getRuleErrors } = jest.requireMock('./rule_errors'); - await setup(); - const lastCall = getRuleErrors.mock.calls[getRuleErrors.mock.calls.length - 1]; - expect(lastCall[2]).toBeDefined(); - expect(lastCall[2]).toEqual({ - isUsingSecurity: true, - minimumScheduleInterval: { value: '1m', enforce: false }, - }); - }); - - it('should render an alert icon next to save button stating the potential change in permissions', async () => { - // Use fake timers so we don't have to wait for the EuiToolTip timeout - jest.useFakeTimers({ legacyFakeTimers: true }); - await setup(); - - expect(wrapper.find('[data-test-subj="changeInPrivilegesTip"]').exists()).toBeTruthy(); - await act(async () => { - wrapper.find('[data-test-subj="changeInPrivilegesTip"]').first().simulate('mouseover'); - }); - - // Run the timers so the EuiTooltip will be visible - jest.runOnlyPendingTimers(); - - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').last().text()).toBe( - 'Saving this rule will change its privileges and might change its behavior.' - ); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx deleted file mode 100644 index 9e6c535aec939..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ /dev/null @@ -1,413 +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 { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiIconTip, - EuiLoadingSpinner, - EuiPortal, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { RuleNotifyWhen, parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; -import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { fetchUiConfig as triggersActionsUiConfig, updateRule } from '@kbn/response-ops-rule-form'; -import { cloneDeep, omit } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; -import { - IErrorObject, - Rule, - RuleAction, - RuleEditProps, - RuleFlyoutCloseReason, - RuleNotifyWhenType, - RuleType, - RuleTypeMetaData, - RuleTypeParams, - RuleUiAction, - TriggersActionsUiConfig, -} from '../../../types'; -import { HealthCheck } from '../../components/health_check'; -import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content'; -import { HealthContextProvider } from '../../context/health_context'; -import { loadRuleTypes } from '../../lib/rule_api/rule_types'; -import { getRuleWithInvalidatedFields } from '../../lib/value_validators'; -import { ConfirmRuleClose } from './confirm_rule_close'; -import { hasRuleChanged } from './has_rule_changed'; -import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors'; -import { RuleForm } from './rule_form'; -import { getRuleReducer } from './rule_reducer'; -import { ShowRequestModal } from './show_request_modal'; - -const defaultUpdateRuleErrorMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText', - { - defaultMessage: 'Cannot update rule.', - } -); - -// Separate function for determining if an untyped action has a group property or not, which helps determine if -// it is a default action or a system action. Consolidated here to deal with type definition complexity -const actionHasDefinedGroup = (action: RuleUiAction): action is RuleAction => { - if (!('group' in action)) return false; - // If the group property is present, ensure that it isn't null or undefined - return Boolean(action.group); -}; - -const cloneAndMigrateRule = (initialRule: Rule) => { - const clonedRule = cloneDeep(omit(initialRule, 'notifyWhen', 'throttle')); - - const hasRuleLevelNotifyWhen = Boolean(initialRule.notifyWhen); - const hasRuleLevelThrottle = Boolean(initialRule.throttle); - - if (hasRuleLevelNotifyWhen || hasRuleLevelThrottle) { - const frequency = hasRuleLevelNotifyWhen - ? { - summary: false, - notifyWhen: initialRule.notifyWhen as RuleNotifyWhenType, - throttle: - initialRule.notifyWhen === RuleNotifyWhen.THROTTLE ? initialRule.throttle! : null, - } - : { summary: false, notifyWhen: RuleNotifyWhen.THROTTLE, throttle: initialRule.throttle! }; - - clonedRule.actions = clonedRule.actions.map((action: RuleUiAction) => { - if (actionHasDefinedGroup(action)) { - return { - ...action, - frequency, - }; - } - return action; - }); - } - return clonedRule; -}; - -export type RuleEditComponent = typeof RuleEdit; - -export const RuleEdit = < - Params extends RuleTypeParams = RuleTypeParams, - MetaData extends RuleTypeMetaData = RuleTypeMetaData ->({ - initialRule, - onClose, - reloadRules, - onSave, - hideInterval, - ruleTypeRegistry, - actionTypeRegistry, - metadata: initialMetadata, - ...props -}: RuleEditProps) => { - const onSaveHandler = onSave ?? reloadRules; - const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]); - const [{ rule }, dispatch] = useReducer(ruleReducer, { - rule: cloneAndMigrateRule(initialRule), - }); - const [isSaving, setIsSaving] = useState(false); - const [hasActionsDisabled, setHasActionsDisabled] = useState(false); - const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = - useState(false); - const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState(false); - const [isShowRequestModalOpen, setIsShowRequestModalOpen] = useState(false); - const [isRuleValid, setIsRuleValid] = useState(false); - const [ruleActionsErrors, setRuleActionsErrors] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [serverRuleType, setServerRuleType] = useState | undefined>( - props.ruleType - ); - const [config, setConfig] = useState({ isUsingSecurity: false }); - - const [metadata, setMetadata] = useState(initialMetadata); - const onChangeMetaData = useCallback((newMetadata: any) => setMetadata(newMetadata), []); - - const { - http, - notifications: { toasts }, - isServerless, - ...startServices - } = useKibana().services; - - const setRule = (value: Rule) => { - dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); - }; - - const ruleType = ruleTypeRegistry.get(rule.ruleTypeId); - - useEffect(() => { - (async () => { - setConfig(await triggersActionsUiConfig({ http })); - })(); - }, [http]); - - useEffect(() => { - (async () => { - setIsLoading(true); - const res = await getRuleActionErrors(rule.actions, actionTypeRegistry); - setRuleActionsErrors([...res]); - setIsLoading(false); - })(); - }, [rule.actions, actionTypeRegistry]); - - useEffect(() => { - if (!props.ruleType && !serverRuleType) { - (async () => { - const serverRuleTypes = await loadRuleTypes({ http }); - for (const _serverRuleType of serverRuleTypes) { - if (ruleType.id === _serverRuleType.id) { - setServerRuleType(_serverRuleType); - } - } - })(); - } - }, [props.ruleType, ruleType.id, serverRuleType, http]); - - const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = getRuleErrors( - rule as Rule, - ruleType, - config, - actionTypeRegistry, - isServerless - ); - - const checkForChangesAndCloseFlyout = () => { - if (hasRuleChanged(rule, initialRule, true)) { - setIsConfirmRuleCloseModalOpen(true); - } else { - onClose(RuleFlyoutCloseReason.CANCELED, metadata); - } - }; - - async function onSaveRule(): Promise { - setIsSaving(true); - try { - if ( - !isLoading && - isValidRule(rule, ruleErrors, ruleActionsErrors) && - !hasActionsWithBrokenConnector - ) { - const { flapping, ...restRule } = rule; - const newRule = await updateRule({ - http, - rule: { - ...restRule, - ...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}), - }, - id: rule.id, - }); - toasts.addSuccess( - i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText', { - defaultMessage: "Updated ''{ruleName}''", - values: { - ruleName: newRule.name, - }, - }) - ); - onClose(RuleFlyoutCloseReason.SAVED, metadata); - if (onSaveHandler) { - onSaveHandler(metadata); - } - } else { - setRule( - getRuleWithInvalidatedFields(rule, ruleParamsErrors, ruleBaseErrors, ruleActionsErrors) - ); - } - } catch (errorRes) { - const message = parseRuleCircuitBreakerErrorMessage( - errorRes.body?.message || defaultUpdateRuleErrorMessage - ); - toasts.addDanger({ - title: message.summary, - ...(message.details && { - text: toMountPoint( - {message.details}, - startServices - ), - }), - }); - } - setIsSaving(false); - } - - useEffect(() => { - setIsRuleValid(isValidRule(rule, ruleErrors, ruleActionsErrors)); - }, [rule, ruleErrors, ruleActionsErrors]); - - return ( - - - - -

- -

-
-
- - - - {hasActionsDisabled && ( - <> - - - - )} - - - - - - checkForChangesAndCloseFlyout()} - > - {i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - {isLoading ? ( - - - - - ) : ( - <> - )} - - - - { - setIsShowRequestModalOpen(true); - }} - > - - - - - await onSaveRule()} - > - - - - {config.isUsingSecurity && ( - - - - )} - - - - - - - {isConfirmRuleCloseModalOpen && ( - { - setIsConfirmRuleCloseModalOpen(false); - onClose(RuleFlyoutCloseReason.CANCELED, metadata); - }} - onCancel={() => { - setIsConfirmRuleCloseModalOpen(false); - }} - /> - )} - {isShowRequestModalOpen && ( - { - setIsShowRequestModalOpen(false); - }} - rule={rule} - ruleId={rule.id} - edit={true} - /> - )} -
-
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleEdit as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx deleted file mode 100644 index 4457c2daa1038..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.test.tsx +++ /dev/null @@ -1,347 +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 { v4 as uuidv4 } from 'uuid'; -import React, { Fragment } from 'react'; -import { - validateBaseProperties, - getRuleErrors, - getRuleActionErrors, - hasObjectErrors, - isValidRule, -} from './rule_errors'; -import { Rule, RuleTypeModel } from '../../../types'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ActionTypeModel } from '../../..'; - -const actionTypeRegistry = actionTypeRegistryMock.create(); -const config = { isUsingSecurity: true, minimumScheduleInterval: { value: '1m', enforce: false } }; -describe('rule_errors', () => { - describe('validateBaseProperties()', () => { - it('should validate the name', () => { - const rule = mockRule(); - rule.name = ''; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: ['Name is required.'], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - - it('should validate the interval', () => { - const rule = mockRule(); - rule.schedule.interval = ''; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': ['Check interval is required.'], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - - it('should validate the minimumScheduleInterval if enforce = false', () => { - const rule = mockRule(); - rule.schedule.interval = '2s'; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - - it('should validate the minimumScheduleInterval if enforce = true', () => { - const rule = mockRule(); - rule.schedule.interval = '2s'; - const result = validateBaseProperties( - rule, - { - isUsingSecurity: true, - minimumScheduleInterval: { value: '1m', enforce: true }, - }, - actionTypeRegistry - ); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': ['Interval must be at least 1 minute.'], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - - it('should validate the ruleTypeId', () => { - const rule = mockRule(); - rule.ruleTypeId = ''; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: ['Rule type is required.'], - actionConnectors: [], - consumer: [], - }); - }); - - it('should get an error when consumer is null', () => { - const rule = mockRule(); - rule.consumer = null as unknown as string; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: ['Scope is required.'], - }); - }); - - it('should not get an error when consumer is undefined', () => { - const rule = mockRule(); - rule.consumer = undefined as unknown as string; - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - - it('should validate the connectors', () => { - const rule = mockRule(); - rule.actions = [ - { - id: '1234', - actionTypeId: 'myActionType', - group: '', - params: { - name: 'yes', - }, - }, - ]; - const actionType = { - id: 'test', - name: 'Test', - isSystemActionType: false, - } as unknown as ActionTypeModel; - actionTypeRegistry.get.mockReturnValue(actionType); - const result = validateBaseProperties(rule, config, actionTypeRegistry); - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: ['Action for myActionType connector is required.'], - consumer: [], - }); - }); - - it('should not throw an error for system actions', () => { - const rule = mockRule(); - - rule.actions = [ - { - id: '1234', - actionTypeId: '.test-system-action', - params: {}, - }, - ]; - - const actionType = { - id: '.test-system-action', - name: 'Test', - isSystemActionType: true, - } as unknown as ActionTypeModel; - - actionTypeRegistry.get.mockReturnValue(actionType); - const result = validateBaseProperties(rule, config, actionTypeRegistry); - - expect(result.errors).toStrictEqual({ - name: [], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }); - }); - }); - - describe('getRuleErrors()', () => { - it('should return all errors', () => { - const result = getRuleErrors( - mockRule({ - name: '', - }), - mockRuleTypeModel({ - validate: () => ({ - errors: { - field: ['This is wrong'], - }, - }), - }), - config, - actionTypeRegistry - ); - expect(result).toStrictEqual({ - ruleParamsErrors: { field: ['This is wrong'] }, - ruleBaseErrors: { - name: ['Name is required.'], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }, - ruleErrors: { - name: ['Name is required.'], - field: ['This is wrong'], - 'schedule.interval': [], - ruleTypeId: [], - actionConnectors: [], - consumer: [], - }, - }); - }); - }); - - describe('getRuleActionErrors()', () => { - it('should return an array of errors', async () => { - actionTypeRegistry.get.mockImplementation((actionTypeId: string) => ({ - ...actionTypeRegistryMock.createMockActionTypeModel(), - validateParams: jest.fn().mockImplementation(() => ({ - errors: { - [actionTypeId]: ['Yes, this failed'], - }, - })), - })); - const result = await getRuleActionErrors( - [ - { - id: '1234', - actionTypeId: 'myActionType', - group: '', - params: { - name: 'yes', - }, - }, - { - id: '5678', - actionTypeId: 'myActionType2', - group: '', - params: { - name: 'yes', - }, - }, - ], - - actionTypeRegistry - ); - expect(result).toStrictEqual([ - { - myActionType: ['Yes, this failed'], - }, - { - myActionType2: ['Yes, this failed'], - }, - ]); - }); - }); - - describe('hasObjectErrors()', () => { - it('should return true for any errors', () => { - expect( - hasObjectErrors({ - foo: ['1'], - }) - ).toBe(true); - expect( - hasObjectErrors({ - foo: { - foo: ['1'], - }, - }) - ).toBe(true); - }); - it('should return false for no errors', () => { - expect(hasObjectErrors({})).toBe(false); - }); - }); - - describe('isValidRule()', () => { - it('should return true for a valid rule', () => { - const result = isValidRule(mockRule(), {}, []); - expect(result).toBe(true); - }); - it('should return false for an invalid rule', () => { - expect( - isValidRule( - mockRule(), - { - name: ['This is wrong'], - }, - [] - ) - ).toBe(false); - expect( - isValidRule(mockRule(), {}, [ - { - name: ['This is wrong'], - }, - ]) - ).toBe(false); - }); - }); -}); - -function mockRuleTypeModel(overloads: Partial = {}): RuleTypeModel { - return { - id: 'ruleTypeModel', - description: 'some rule', - iconClass: 'something', - documentationUrl: null, - validate: () => ({ errors: {} }), - ruleParamsExpression: () => , - requiresAppContext: false, - ...overloads, - }; -} - -function mockRule(overloads: Partial = {}): Rule { - return { - id: uuidv4(), - enabled: true, - name: `rule-${uuidv4()}`, - tags: [], - ruleTypeId: '.noop', - consumer: 'consumer', - schedule: { interval: '1m' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - revision: 0, - ...overloads, - }; -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts deleted file mode 100644 index c86a216053d3d..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_errors.ts +++ /dev/null @@ -1,185 +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 { isObject } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { RuleNotifyWhen, SanitizedRuleAction } from '@kbn/alerting-plugin/common'; -import { formatDuration, parseDuration } from '@kbn/alerting-plugin/common/parse_duration'; -import { - RuleTypeModel, - Rule, - IErrorObject, - ValidationResult, - ActionTypeRegistryContract, - TriggersActionsUiConfig, - RuleUiAction, -} from '../../../types'; -import { InitialRule } from './rule_reducer'; - -export function validateBaseProperties( - ruleObject: InitialRule, - config: TriggersActionsUiConfig, - actionTypeRegistry: ActionTypeRegistryContract -): ValidationResult { - const validationResult = { errors: {} }; - - const errors = { - name: new Array(), - 'schedule.interval': new Array(), - consumer: new Array(), - ruleTypeId: new Array(), - actionConnectors: new Array(), - }; - - validationResult.errors = errors; - - if (!ruleObject.name) { - errors.name.push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText', { - defaultMessage: 'Name is required.', - }) - ); - } - - if (ruleObject.consumer === null) { - errors.consumer.push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText', { - defaultMessage: 'Scope is required.', - }) - ); - } - - if (ruleObject.schedule.interval.length < 2) { - errors['schedule.interval'].push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText', { - defaultMessage: 'Check interval is required.', - }) - ); - } else if (config.minimumScheduleInterval && config.minimumScheduleInterval.enforce) { - const duration = parseDuration(ruleObject.schedule.interval); - const minimumDuration = parseDuration(config.minimumScheduleInterval.value); - if (duration < minimumDuration) { - errors['schedule.interval'].push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText', { - defaultMessage: 'Interval must be at least {minimum}.', - values: { - minimum: formatDuration(config.minimumScheduleInterval.value, true), - }, - }) - ); - } - } - - const invalidThrottleActions = ruleObject.actions.filter((a) => { - if (actionTypeRegistry.get(a.actionTypeId).isSystemActionType) return false; - - const defaultAction = a as SanitizedRuleAction; - if (!defaultAction.frequency?.throttle) return false; - - const throttleDuration = parseDuration(defaultAction.frequency.throttle); - const intervalDuration = - ruleObject.schedule.interval && ruleObject.schedule.interval.length > 1 - ? parseDuration(ruleObject.schedule.interval) - : 0; - - return ( - defaultAction.frequency?.notifyWhen === RuleNotifyWhen.THROTTLE && - throttleDuration < intervalDuration - ); - }); - - if (invalidThrottleActions.length) { - errors['schedule.interval'].push( - i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.error.actionThrottleBelowSchedule', - { - defaultMessage: - "Custom action intervals cannot be shorter than the rule's check interval", - } - ) - ); - } - - if (!ruleObject.ruleTypeId) { - errors.ruleTypeId.push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText', { - defaultMessage: 'Rule type is required.', - }) - ); - } - - const emptyConnectorActions = ruleObject.actions.find( - (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 - ); - - if (emptyConnectorActions !== undefined) { - errors.actionConnectors.push( - i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector', { - defaultMessage: 'Action for {actionTypeId} connector is required.', - values: { actionTypeId: emptyConnectorActions.actionTypeId }, - }) - ); - } - - return validationResult; -} - -export function getRuleErrors( - rule: Rule, - ruleTypeModel: RuleTypeModel | null, - config: TriggersActionsUiConfig, - actionTypeRegistry: ActionTypeRegistryContract, - isServerless?: boolean -) { - const ruleParamsErrors: IErrorObject = ruleTypeModel - ? ruleTypeModel.validate(rule.params, isServerless).errors - : {}; - - const ruleBaseErrors = validateBaseProperties(rule, config, actionTypeRegistry) - .errors as IErrorObject; - - const ruleErrors = { - ...ruleParamsErrors, - ...ruleBaseErrors, - } as IErrorObject; - - return { - ruleParamsErrors, - ruleBaseErrors, - ruleErrors, - }; -} - -export async function getRuleActionErrors( - actions: RuleUiAction[], - actionTypeRegistry: ActionTypeRegistryContract -): Promise { - return await Promise.all( - actions.map( - async (ruleAction: RuleUiAction) => - ( - await actionTypeRegistry.get(ruleAction.actionTypeId)?.validateParams(ruleAction.params) - ).errors - ) - ); -} - -export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => - !!Object.values(errors).find((errorList) => { - if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); - return errorList.length >= 1; - }); - -export function isValidRule( - ruleObject: InitialRule | Rule, - validationResult: IErrorObject, - actionsErrors: IErrorObject[] -): ruleObject is Rule { - return ( - !hasObjectErrors(validationResult) && - actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) - ); -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss deleted file mode 100644 index d80a80faac0a5..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.scss +++ /dev/null @@ -1,4 +0,0 @@ -.triggersActionsUI__ruleTypeNodeHeading { - margin-left: $euiSizeS; - margin-right: $euiSizeS; -} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx deleted file mode 100644 index a89f7fe76339b..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ /dev/null @@ -1,1257 +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 React, { FunctionComponent } from 'react'; -import { EuiFormLabel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { ActionForm } from '../action_connector_form'; -import { AlertConsumers, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; -import { RuleFormConsumerSelection } from './rule_form_consumer_selection'; -import { - ValidationResult, - Rule, - RuleType, - RuleTypeModel, - GenericValidationResult, - RuleCreationValidConsumer, -} from '../../../types'; -import { RuleForm } from './rule_form'; -import { coreMock } from '@kbn/core/public/mocks'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ALERTING_FEATURE_ID, RecoveredActionGroup } from '@kbn/alerting-plugin/common'; -import { useKibana } from '../../../common/lib/kibana'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - cacheTime: 0, - }, - }, -}); - -const toMapById = [ - (acc: Map, val: { id: unknown }) => acc.set(val.id, val), - new Map(), -] as const; - -const actionTypeRegistry = actionTypeRegistryMock.create(); -const ruleTypeRegistry = ruleTypeRegistryMock.create(); - -const mockSetConsumer = jest.fn(); - -export const TestExpression: FunctionComponent = () => { - return ( - - - - ); -}; - -jest.mock('../../hooks/use_load_rule_types_query', () => ({ - useLoadRuleTypesQuery: jest.fn(), -})); -jest.mock('../../../common/lib/kibana'); -jest.mock('../../lib/capabilities', () => ({ - hasAllPrivilege: jest.fn(() => true), - hasSaveRulesCapability: jest.fn(() => true), - hasShowActionsCapability: jest.fn(() => true), - hasExecuteActionsCapability: jest.fn(() => true), -})); -jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({ - fetchFlappingSettings: jest.fn().mockResolvedValue({ - lookBackWindow: 20, - statusChangeThreshold: 20, - }), -})); - -describe('rule_form', () => { - const ruleType = { - id: 'my-rule-type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: false, - }; - - const actionType = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'my-action-type', - iconClass: 'test', - selectMessage: 'test', - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - - const ruleTypeNonEditable = { - id: 'non-edit-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: true, - }; - - const disabledByLicenseRuleType = { - id: 'disabled-by-license', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: false, - }; - - const useKibanaMock = useKibana as jest.Mocked; - - describe('rule recovery message', () => { - let wrapper: ReactWrapper; - const defaultRecoveryMessage = 'Sample default recovery message'; - - async function setup(enforceMinimum = false, schedule = '1m') { - const mocks = coreMock.createSetup(); - const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query'); - const myRuleModel = { - id: 'my-rule-type', - description: 'Sample rule type model', - iconClass: 'sampleIconClass', - defaultActionMessage: 'Sample default action message', - defaultRecoveryMessage, - requiresAppContext: false, - }; - const myRule = { - id: 'my-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - { - id: 'recovered', - name: 'Recovered', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - params: [], - state: [], - }, - enabledInLicense: true, - }; - const disabledByLicenseRule = { - id: 'disabled-by-license', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'gold', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - params: [], - state: [], - }, - enabledInLicense: false, - }; - useLoadRuleTypesQuery.mockReturnValue({ - ruleTypesState: { - data: new Map([ - [myRule.id, myRule], - [disabledByLicenseRule.id, disabledByLicenseRule], - ]), - }, - }); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - rulesSettings: { - writeFlappingSettingsUI: true, - }, - rules: { - show: true, - save: true, - delete: true, - }, - }; - ruleTypeRegistry.list.mockReturnValue([ - ruleType, - ruleTypeNonEditable, - disabledByLicenseRuleType, - ]); - ruleTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.get.mockReturnValue(myRuleModel as RuleTypeModel); - actionTypeRegistry.list.mockReturnValue([actionType]); - actionTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.get.mockReturnValue(actionType); - const initialRule = { - name: 'test', - params: {}, - consumer: ALERTING_FEATURE_ID, - schedule: { - interval: schedule, - }, - actions: [], - tags: [], - muteAll: false, - enabled: false, - mutedInstanceIds: [], - ruleTypeId: 'my-rule-type', - } as unknown as Rule; - - wrapper = mountWithIntl( - - {}} - errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }} - operation="create" - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - onChangeMetaData={jest.fn()} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders defaultRecoveryMessage for recovery action when specified', async () => { - await setup(); - const actionForm = wrapper.find(ActionForm); - expect(actionForm.first().prop('actionGroups')?.[1]).toEqual( - expect.objectContaining({ defaultActionMessage: defaultRecoveryMessage }) - ); - }); - }); - - describe('rule_form create rule', () => { - let wrapper: ReactWrapper; - - async function setup(options?: { - showRulesList?: boolean; - enforceMinimum?: boolean; - schedule?: string; - featureId?: string; - initialRuleOverwrite?: Partial; - validConsumers?: RuleCreationValidConsumer[]; - ruleTypesOverwrite?: RuleType[]; - ruleTypeModelOverwrite?: RuleTypeModel; - useRuleProducer?: boolean; - selectedConsumer?: RuleCreationValidConsumer | null; - }) { - const { - showRulesList = false, - enforceMinimum = false, - schedule = '1m', - featureId = 'alerting', - initialRuleOverwrite, - validConsumers, - ruleTypesOverwrite, - ruleTypeModelOverwrite, - useRuleProducer = false, - selectedConsumer, - } = options || {}; - - const mocks = coreMock.createSetup(); - const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query'); - const ruleTypes: RuleType[] = ruleTypesOverwrite || [ - { - id: 'my-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - params: [], - state: [], - }, - enabledInLicense: true, - category: 'my-category', - }, - { - id: 'disabled-by-license', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'gold', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - actionVariables: { - params: [], - state: [], - }, - enabledInLicense: false, - category: 'my-category', - }, - ]; - useLoadRuleTypesQuery.mockReturnValue({ - ruleTypesState: { - data: ruleTypes.reduce(...toMapById), - }, - }); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - rulesSettings: { - writeFlappingSettingsUI: true, - }, - rules: { - show: true, - save: true, - delete: true, - }, - }; - ruleTypeRegistry.list.mockReturnValue([ - ruleTypeModelOverwrite || ruleType, - ruleTypeNonEditable, - disabledByLicenseRuleType, - ]); - ruleTypeRegistry.get.mockReturnValue(ruleTypeModelOverwrite || ruleType); - ruleTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.list.mockReturnValue([actionType]); - actionTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.get.mockReturnValue(actionType); - const initialRule = { - name: 'test', - params: {}, - consumer: ALERTING_FEATURE_ID, - schedule: { - interval: schedule, - }, - actions: [], - tags: [], - muteAll: false, - enabled: false, - mutedInstanceIds: [], - ...(!showRulesList ? { ruleTypeId: ruleType.id } : {}), - alertDelay: { - active: 1, - }, - } as unknown as Rule; - - wrapper = mountWithIntl( - - {}} - errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }} - operation="create" - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - connectorFeatureId={featureId} - onChangeMetaData={jest.fn()} - validConsumers={validConsumers} - setConsumer={mockSetConsumer} - useRuleProducer={useRuleProducer} - selectedConsumer={selectedConsumer} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders rule name', async () => { - await setup(); - const ruleNameField = wrapper.find('[data-test-subj="ruleNameInput"]'); - expect(ruleNameField.exists()).toBeTruthy(); - expect(ruleNameField.first().prop('value')).toBe('test'); - }); - - it('renders registered selected rule type', async () => { - await setup({ showRulesList: true }); - const ruleTypeSelectOptions = wrapper.find('[data-test-subj="my-rule-type-SelectOption"]'); - expect(ruleTypeSelectOptions.exists()).toBeTruthy(); - }); - - it('renders minimum schedule interval helper text when enforce = true', async () => { - await setup({ enforceMinimum: true }); - expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( - `Interval must be at least 1 minute.` - ); - }); - - it('renders minimum schedule interval helper suggestion when enforce = false and schedule is less than configuration', async () => { - await setup({ schedule: '10s' }); - expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( - `Intervals less than 1 minute are not recommended due to performance considerations.` - ); - }); - - it('does not render minimum schedule interval helper when enforce = false and schedule is greater than configuration', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="intervalFormRow"]').first().prop('helpText')).toEqual( - `` - ); - }); - - it('handles schedule interval inputs correctly', async () => { - const getIntervalInput = () => { - return wrapper.find('[data-test-subj="intervalInput"] input').first(); - }; - - await setup(); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: '2' } }); - expect(getIntervalInput().props().value).toEqual(2); - - getIntervalInput().simulate('change', { target: { value: '20' } }); - expect(getIntervalInput().props().value).toEqual(20); - - getIntervalInput().simulate('change', { target: { value: '999' } }); - expect(getIntervalInput().props().value).toEqual(999); - - // Invalid values: - await setup(); - getIntervalInput().simulate('change', { target: { value: '0' } }); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: 'INVALID' } }); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: '-123' } }); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: '1.0123' } }); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: '0.0123' } }); - expect(getIntervalInput().props().value).toEqual(1); - - getIntervalInput().simulate('change', { target: { value: '+123' } }); - expect(getIntervalInput().props().value).toEqual(1); - }); - - it('does not render registered rule type which non editable', async () => { - await setup(); - const ruleTypeSelectOptions = wrapper.find( - '[data-test-subj="non-edit-rule-type-SelectOption"]' - ); - expect(ruleTypeSelectOptions.exists()).toBeFalsy(); - }); - - it('renders registered action types', async () => { - await setup(); - const ruleTypeSelectOptions = wrapper.find( - '[data-test-subj=".server-log-alerting-ActionTypeSelectOption"]' - ); - expect(ruleTypeSelectOptions.exists()).toBeFalsy(); - }); - - it('renders uses feature id to load action types', async () => { - await setup({ schedule: '1m', featureId: 'anotherFeature' }); - const ruleTypeSelectOptions = wrapper.find( - '[data-test-subj=".server-log-anotherFeature-ActionTypeSelectOption"]' - ); - expect(ruleTypeSelectOptions.exists()).toBeFalsy(); - }); - - it('renders rule type description', async () => { - await setup({ showRulesList: true }); - wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); - const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); - expect(ruleDescription.exists()).toBeTruthy(); - expect(ruleDescription.first().text()).toContain('Rule when testing'); - }); - - it('renders rule type documentation link', async () => { - await setup({ showRulesList: true }); - wrapper.find('button[data-test-subj="my-rule-type-SelectOption"]').first().simulate('click'); - const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); - expect(ruleDocumentationLink.exists()).toBeTruthy(); - expect(ruleDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); - }); - - it('renders rule types disabled by license', async () => { - await setup({ showRulesList: true }); - const actionOption = wrapper.find(`[data-test-subj="disabled-by-license-SelectOption"]`); - expect(actionOption.exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="disabled-by-license-disabledTooltip"]').exists() - ).toBeTruthy(); - }); - - it('should select the only one available consumer', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - name: 'Threshold Rule 1', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - alerts: { read: true, all: true }, - apm: { read: true, all: true }, - discover: { read: true, all: true }, - infrastructure: { read: true, all: true }, - // Setting logs all to false, this shouldn't show up - logs: { read: true, all: false }, - ml: { read: true, all: true }, - monitoring: { read: true, all: true }, - siem: { read: true, all: true }, - slo: { read: true, all: false }, - stackAlerts: { read: true, all: true }, - uptime: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - }, - ], - ruleTypeModelOverwrite: { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS], - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - expect(mockSetConsumer).toHaveBeenLastCalledWith('infrastructure'); - }); - - it('should render multiple consumers in the dropdown and select the first one in the list if no default is specified', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - }, - ], - ruleTypeModelOverwrite: { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeTruthy(); - expect(wrapper.find(RuleFormConsumerSelection).props().consumers).toEqual([ - 'infrastructure', - 'logs', - ]); - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(mockSetConsumer).toHaveBeenLastCalledWith('infrastructure'); - }); - - it('should not display the consumer select for invalid rule types', async () => { - await setup(); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy(); - }); - - it('Do not show alert query in action when we do not have hasFieldsForAAD or hasAlertsMappings or belong to security', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: 'my-rule-type', - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: 'my-rule-type', - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - hasFieldsForAAD: false, - hasAlertsMappings: false, - }, - ], - ruleTypeModelOverwrite: { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(false); - }); - - it('Show alert query in action when rule type hasFieldsForAAD', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: 'my-rule-type', - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: 'my-rule-type', - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - hasFieldsForAAD: true, - hasAlertsMappings: false, - }, - ], - ruleTypeModelOverwrite: { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true); - }); - - it('Show alert query in action when rule type hasAlertsMappings', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: 'my-rule-type', - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: 'my-rule-type', - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - hasFieldsForAAD: false, - hasAlertsMappings: true, - }, - ], - ruleTypeModelOverwrite: { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true); - }); - - it('Show alert query in action when rule type is from security solution', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'siem', - ruleTypeId: 'my-rule-type', - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: 'my-rule-type', - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: 'siem', - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - hasFieldsForAAD: false, - hasAlertsMappings: false, - }, - ], - ruleTypeModelOverwrite: { - id: 'my-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true); - }); - - it('show alert query in action when multi consumer rule type does not have a consumer selected', async () => { - await setup({ - initialRuleOverwrite: { - name: 'Simple rule', - consumer: 'alerts', - ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - schedule: { - interval: '1h', - }, - }, - ruleTypesOverwrite: [ - { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - name: 'Threshold Rule', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - enabledInLicense: true, - category: 'my-category', - defaultActionGroupId: 'threshold.fired', - minimumLicenseRequired: 'basic', - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - infrastructure: { read: true, all: true }, - logs: { read: true, all: true }, - }, - actionVariables: { - context: [], - state: [], - params: [], - }, - hasFieldsForAAD: true, - hasAlertsMappings: true, - }, - ], - ruleTypeModelOverwrite: { - id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: TestExpression, - requiresAppContext: false, - }, - selectedConsumer: 'logs', - }); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true); - }); - - it('renders rule alert delay', async () => { - const getAlertDelayInput = () => { - return wrapper.find('[data-test-subj="alertDelayInput"] input').first(); - }; - - await setup(); - // expect the accordion to be closed by default - expect(wrapper.find('.euiAccordion-isOpen').exists()).toBeFalsy(); - - expect(getAlertDelayInput().props().value).toEqual(1); - - getAlertDelayInput().simulate('change', { target: { value: '2' } }); - expect(getAlertDelayInput().props().value).toEqual(2); - - getAlertDelayInput().simulate('change', { target: { value: '20' } }); - expect(getAlertDelayInput().props().value).toEqual(20); - - getAlertDelayInput().simulate('change', { target: { value: '999' } }); - expect(getAlertDelayInput().props().value).toEqual(999); - }); - }); - - describe('rule_form create rule non ruleing consumer and producer', () => { - let wrapper: ReactWrapper; - - async function setup() { - const { useLoadRuleTypesQuery } = jest.requireMock('../../hooks/use_load_rule_types_query'); - useLoadRuleTypesQuery.mockReturnValue({ - ruleTypesState: { - data: [ - { - id: 'other-consumer-producer-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: ALERTING_FEATURE_ID, - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - }, - { - id: 'same-consumer-producer-rule-type', - name: 'Test', - actionGroups: [ - { - id: 'testActionGroup', - name: 'Test Action Group', - }, - ], - defaultActionGroupId: 'testActionGroup', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - producer: 'test', - authorizedConsumers: { - [ALERTING_FEATURE_ID]: { read: true, all: true }, - test: { read: true, all: true }, - }, - }, - ].reduce(...toMapById), - }, - }); - const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - rulesSettings: { - writeFlappingSettingsUI: true, - }, - rules: { - show: true, - save: true, - delete: true, - }, - }; - ruleTypeRegistry.list.mockReturnValue([ - { - id: 'same-consumer-producer-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: true, - }, - { - id: 'other-consumer-producer-rule-type', - iconClass: 'test', - description: 'test', - documentationUrl: null, - validate: (): ValidationResult => { - return { errors: {} }; - }, - ruleParamsExpression: () => <>, - requiresAppContext: false, - }, - ]); - ruleTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.get.mockReturnValue(actionType); - - const initialRule = { - name: 'non ruleing consumer test', - params: {}, - consumer: 'test', - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - muteAll: false, - enabled: false, - mutedInstanceIds: [], - } as unknown as Rule; - - wrapper = mountWithIntl( - - {}} - errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }} - operation="create" - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - onChangeMetaData={jest.fn()} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(useLoadRuleTypesQuery).toHaveBeenCalled(); - } - - it('renders rule type options which producer correspond to the rule consumer', async () => { - await setup(); - const ruleTypeSelectOptions = wrapper.find( - '[data-test-subj="same-consumer-producer-rule-type-SelectOption"]' - ); - expect(ruleTypeSelectOptions.exists()).toBeTruthy(); - }); - }); - - describe('rule_form edit rule', () => { - let wrapper: ReactWrapper; - - async function setup() { - ruleTypeRegistry.list.mockReturnValue([ruleType]); - ruleTypeRegistry.get.mockReturnValue(ruleType); - ruleTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.list.mockReturnValue([actionType]); - actionTypeRegistry.has.mockReturnValue(true); - actionTypeRegistry.get.mockReturnValue(actionType); - - const initialRule = { - name: 'test', - ruleTypeId: ruleType.id, - params: {}, - consumer: ALERTING_FEATURE_ID, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - muteAll: false, - enabled: false, - mutedInstanceIds: [], - } as unknown as Rule; - - wrapper = mountWithIntl( - - {}} - errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }} - operation="create" - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - onChangeMetaData={jest.fn()} - /> - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders rule name', async () => { - await setup(); - const ruleNameField = wrapper.find('[data-test-subj="ruleNameInput"]'); - expect(ruleNameField.exists()).toBeTruthy(); - expect(ruleNameField.first().prop('value')).toBe('test'); - }); - - it('renders registered selected rule type', async () => { - await setup(); - const ruleTypeSelectOptions = wrapper.find('[data-test-subj="selectedRuleTypeTitle"]'); - expect(ruleTypeSelectOptions.exists()).toBeTruthy(); - }); - - it('renders rule type description', async () => { - await setup(); - const ruleDescription = wrapper.find('[data-test-subj="ruleDescription"]'); - expect(ruleDescription.exists()).toBeTruthy(); - expect(ruleDescription.first().text()).toContain('Rule when testing'); - }); - - it('renders rule type documentation link', async () => { - await setup(); - const ruleDocumentationLink = wrapper.find('[data-test-subj="ruleDocumentationLink"]'); - expect(ruleDocumentationLink.exists()).toBeTruthy(); - expect(ruleDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx deleted file mode 100644 index 4e6c5279e0716..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ /dev/null @@ -1,1153 +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 React, { - Fragment, - useState, - useEffect, - useCallback, - Suspense, - useMemo, - useRef, -} from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTextColor, - EuiTitle, - EuiForm, - EuiSpacer, - EuiFieldText, - EuiFieldSearch, - EuiFlexGrid, - EuiFormRow, - EuiComboBox, - EuiFieldNumber, - EuiSelect, - EuiIconTip, - EuiButtonIcon, - EuiHorizontalRule, - EuiEmptyPrompt, - EuiListGroupItem, - EuiListGroup, - EuiLink, - EuiText, - EuiNotificationBadge, - EuiErrorBoundary, - EuiToolTip, - EuiCallOut, - EuiAccordion, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; -import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; -import { capitalize } from 'lodash'; -import { KibanaFeature } from '@kbn/features-plugin/public'; -import { - formatDuration, - getDurationNumberInItsUnit, - getDurationUnitValue, - parseDuration, -} from '@kbn/alerting-plugin/common/parse_duration'; -import { - RuleActionParam, - ALERTING_FEATURE_ID, - RecoveredActionGroup, - isActionGroupDisabledForActionTypeId, - RuleActionAlertsFilterProperty, - RuleActionKey, -} 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 type { Flapping } from '@kbn/alerting-types'; -import { RuleReducerAction, InitialRule } from './rule_reducer'; -import { - RuleTypeModel, - Rule, - IErrorObject, - RuleType, - RuleTypeRegistryContract, - ActionTypeRegistryContract, - TriggersActionsUiConfig, - RuleCreationValidConsumer, - RuleUiAction, -} from '../../../types'; -import { getTimeOptions } from '../../../common/lib/get_time_options'; -import { ActionForm } from '../action_connector_form'; -import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; -import { SolutionFilter } from './solution_filter'; -import './rule_form.scss'; -import { useKibana } from '../../../common/lib/kibana'; -import { recoveredActionGroupMessage, summaryMessage } from '../../constants'; -import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_enabled'; -import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; -import { - ruleTypeCompare, - ruleTypeGroupCompare, - ruleTypeUngroupedCompare, -} from '../../lib/rule_type_compare'; -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'; -import { getInitialInterval } from './get_initial_interval'; -import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query'; -import { RuleFormAdvancedOptions } from './rule_form_advanced_options'; - -const ENTER_KEY = 13; - -const INTEGER_REGEX = /^[1-9][0-9]*$/; - -const NOOP = () => {}; - -function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { - return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; -} - -const authorizedToDisplayRuleType = ({ - rule, - ruleType, - validConsumers, -}: { - rule: InitialRule; - ruleType: RuleType; - validConsumers?: RuleCreationValidConsumer[]; -}) => { - if (!ruleType) { - return false; - } - // If we have a generic threshold/ES query rule... - if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id)) { - // And an array of valid consumers are passed in, we will show it - // if the rule type has at least one of the consumers as authorized - if (Array.isArray(validConsumers)) { - return validConsumers.some((consumer) => hasAllPrivilege(consumer, ruleType)); - } - // If no array was passed in, then we will show it if at least one of its - // authorized consumers allows it to be shown. - return Object.entries(ruleType.authorizedConsumers).some(([_, privilege]) => { - return privilege.all; - }); - } - // For non-generic threshold/ES query rules, we will still do the check - // against `alerts` since we are still setting rule consumers to `alerts` - return hasAllPrivilege(rule.consumer, ruleType); -}; - -export type RuleTypeItems = Array<{ ruleTypeModel: RuleTypeModel; ruleType: RuleType }>; - -interface RuleFormProps> { - rule: InitialRule; - config: TriggersActionsUiConfig; - dispatch: React.Dispatch; - errors: IErrorObject; - ruleTypeRegistry: RuleTypeRegistryContract; - actionTypeRegistry: ActionTypeRegistryContract; - operation: string; - canChangeTrigger?: boolean; // to hide Change trigger button - canShowConsumerSelection?: boolean; - setHasActionsDisabled?: (value: boolean) => void; - setHasActionsWithBrokenConnector?: (value: boolean) => void; - setConsumer?: (consumer: RuleCreationValidConsumer | null) => void; - metadata?: MetaData; - filteredRuleTypes?: string[]; - hideGrouping?: boolean; - hideInterval?: boolean; - connectorFeatureId?: string; - selectedConsumer?: RuleCreationValidConsumer | null; - validConsumers?: RuleCreationValidConsumer[]; - onChangeMetaData: (metadata: MetaData) => void; - useRuleProducer?: boolean; - initialSelectedConsumer?: RuleCreationValidConsumer | null; -} - -const EMPTY_ARRAY: string[] = []; - -export const RuleForm = ({ - rule, - config, - canChangeTrigger = true, - canShowConsumerSelection = false, - dispatch, - errors, - setHasActionsDisabled, - setHasActionsWithBrokenConnector, - setConsumer = NOOP, - selectedConsumer, - operation, - ruleTypeRegistry, - actionTypeRegistry, - metadata, - filteredRuleTypes: ruleTypeToFilter = EMPTY_ARRAY, - hideGrouping = false, - hideInterval, - connectorFeatureId = AlertingConnectorFeatureId, - validConsumers, - onChangeMetaData, - useRuleProducer, - initialSelectedConsumer, -}: RuleFormProps) => { - const { - notifications: { toasts }, - docLinks, - application: { capabilities }, - kibanaFeatures, - charts, - data, - unifiedSearch, - dataViews, - } = useKibana().services; - const canShowActions = hasShowActionsCapability(capabilities); - const { colorMode } = useEuiTheme(); - - const [ruleTypeModel, setRuleTypeModel] = useState(null); - const flyoutBodyOverflowRef = useRef(null); - - const defaultRuleInterval = getInitialInterval(config.minimumScheduleInterval?.value); - const defaultScheduleInterval = getDurationNumberInItsUnit(defaultRuleInterval); - const defaultScheduleIntervalUnit = getDurationUnitValue(defaultRuleInterval); - - const [ruleInterval, setRuleInterval] = useState( - rule.schedule.interval - ? getDurationNumberInItsUnit(rule.schedule.interval) - : defaultScheduleInterval - ); - const [ruleIntervalUnit, setRuleIntervalUnit] = useState( - rule.schedule.interval - ? getDurationUnitValue(rule.schedule.interval) - : defaultScheduleIntervalUnit - ); - const [alertDelay, setAlertDelay] = useState(rule.alertDelay?.active ?? 1); - const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); - - const [availableRuleTypes, setAvailableRuleTypes] = useState([]); - const [filteredRuleTypes, setFilteredRuleTypes] = useState([]); - const [searchText, setSearchText] = useState(); - const [inputText, setInputText] = useState(); - const [solutions, setSolutions] = useState | undefined>(undefined); - const [solutionsFilter, setSolutionFilter] = useState([]); - let hasDisabledByLicenseRuleTypes: boolean = false; - const { - ruleTypesState: { - data: ruleTypeIndex, - error: loadRuleTypesError, - isLoading: ruleTypesIsLoading, - }, - } = useLoadRuleTypesQuery({ filteredRuleTypes: ruleTypeToFilter }); - const ruleTypes = useMemo(() => [...ruleTypeIndex.values()], [ruleTypeIndex]); - - // load rule types - useEffect(() => { - if (rule.ruleTypeId && ruleTypeIndex?.has(rule.ruleTypeId)) { - setDefaultActionGroupId(ruleTypeIndex.get(rule.ruleTypeId)!.defaultActionGroupId); - } - - const getAvailableRuleTypes = (ruleTypesResult: RuleType[]) => - ruleTypeRegistry - .list() - .reduce((arr: RuleTypeItems, ruleTypeRegistryItem: RuleTypeModel) => { - const ruleType = ruleTypesResult.find((item) => ruleTypeRegistryItem.id === item.id); - if (ruleType) { - arr.push({ - ruleType, - ruleTypeModel: ruleTypeRegistryItem, - }); - } - return arr; - }, []) - .filter(({ ruleType }) => - authorizedToDisplayRuleType({ - rule, - ruleType, - validConsumers, - }) - ) - .filter((item) => - rule.consumer === ALERTING_FEATURE_ID - ? !item.ruleTypeModel.requiresAppContext - : item.ruleType!.producer === rule.consumer - ); - - const availableRuleTypesResult = getAvailableRuleTypes(ruleTypes); - setAvailableRuleTypes(availableRuleTypesResult); - - const solutionsResult = availableRuleTypesResult.reduce( - (result: Map, ruleTypeItem) => { - if (!result.has(ruleTypeItem.ruleType.producer)) { - result.set( - ruleTypeItem.ruleType.producer, - (kibanaFeatures - ? getProducerFeatureName(ruleTypeItem.ruleType.producer, kibanaFeatures) - : capitalize(ruleTypeItem.ruleType.producer)) ?? - capitalize(ruleTypeItem.ruleType.producer) - ); - } - return result; - }, - new Map() - ); - const solutionsEntries = [...solutionsResult.entries()]; - const isOnlyO11y = - availableRuleTypesResult.length === 1 && - availableRuleTypesResult.every((rt) => rt.ruleType.producer === AlertConsumers.OBSERVABILITY); - if (!isOnlyO11y) { - setSolutions(new Map(solutionsEntries.sort(([, a], [, b]) => a.localeCompare(b)))); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - ruleTypes, - ruleTypeIndex, - rule.ruleTypeId, - kibanaFeatures, - rule.consumer, - ruleTypeRegistry, - validConsumers, - ]); - - useEffect(() => { - if (loadRuleTypesError) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage', - { defaultMessage: 'Unable to load rule types' } - ), - }); - } - }, [loadRuleTypesError, toasts]); - - useEffect(() => { - setRuleTypeModel(rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null); - if (rule.ruleTypeId && ruleTypeIndex && ruleTypeIndex.has(rule.ruleTypeId)) { - setDefaultActionGroupId(ruleTypeIndex.get(rule.ruleTypeId)!.defaultActionGroupId); - } - }, [rule, rule.ruleTypeId, ruleTypeIndex, ruleTypeRegistry]); - - useEffect(() => { - if (rule.schedule.interval) { - const interval = getDurationNumberInItsUnit(rule.schedule.interval); - const intervalUnit = getDurationUnitValue(rule.schedule.interval); - setRuleInterval(interval); - setRuleIntervalUnit(intervalUnit); - } - }, [rule.schedule.interval, defaultScheduleInterval, defaultScheduleIntervalUnit]); - - useEffect(() => { - if (rule.alertDelay) { - setAlertDelay(rule.alertDelay.active); - } - }, [rule.alertDelay]); - - useEffect(() => { - if (!flyoutBodyOverflowRef.current) { - // We're using this as a reliable way to reset the scroll position - // of the flyout independently of the selected rule type - flyoutBodyOverflowRef.current = document.querySelector('.euiFlyoutBody__overflow'); - } - }, []); - - const resetContentScroll = useCallback(() => flyoutBodyOverflowRef?.current?.scroll?.(0, 0), []); - - useEffect(() => { - if (rule.ruleTypeId) { - resetContentScroll(); - } - }, [rule.ruleTypeId, resetContentScroll]); - - const setRuleProperty = useCallback( - (key: Key, value: Rule[Key] | null) => { - dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); - }, - [dispatch] - ); - - const setActions = useCallback( - (updatedActions: RuleUiAction[]) => setRuleProperty('actions', updatedActions), - [setRuleProperty] - ); - - const setRuleParams = (key: string, value: any) => { - dispatch({ command: { type: 'setRuleParams' }, payload: { key, value } }); - }; - - const setScheduleProperty = (key: string, value: any) => { - dispatch({ command: { type: 'setScheduleProperty' }, payload: { key, value } }); - }; - - const setActionProperty = ( - key: Key, - value: RuleActionParam | null, - index: number - ) => { - dispatch({ command: { type: 'setRuleActionProperty' }, payload: { key, value, index } }); - }; - - const setActionParamsProperty = useCallback( - (key: string, value: RuleActionParam, index: number) => { - dispatch({ command: { type: 'setRuleActionParams' }, payload: { key, value, index } }); - }, - [dispatch] - ); - - const setActionFrequencyProperty = useCallback( - (key: string, value: RuleActionParam, index: number) => { - dispatch({ command: { type: 'setRuleActionFrequency' }, payload: { key, value, index } }); - }, - [dispatch] - ); - - const setActionAlertsFilterProperty = useCallback( - (key: string, value: RuleActionAlertsFilterProperty, index: number) => { - dispatch({ command: { type: 'setRuleActionAlertsFilter' }, payload: { key, value, index } }); - }, - [dispatch] - ); - - const setAlertDelayProperty = (key: string, value: any) => { - dispatch({ command: { type: 'setAlertDelayProperty' }, payload: { key, value } }); - }; - - const onAlertDelayChange = (value: string) => { - const parsedValue = value === '' ? '' : parseInt(value, 10); - setAlertDelayProperty('active', parsedValue || 1); - setAlertDelay(parsedValue || undefined); - }; - - useEffect(() => { - const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null; - setFilteredRuleTypes( - availableRuleTypes - .filter((ruleTypeItem) => - solutionsFilter.length > 0 - ? solutionsFilter.find((item) => ruleTypeItem.ruleType!.producer === item) - : ruleTypeItem - ) - .filter((ruleTypeItem) => - searchValue - ? ruleTypeItem.ruleType.name.toString().toLocaleLowerCase().includes(searchValue) || - ruleTypeItem.ruleType!.producer.toLocaleLowerCase().includes(searchValue) || - ruleTypeItem.ruleTypeModel.description.toLocaleLowerCase().includes(searchValue) - : ruleTypeItem - ) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ruleTypeRegistry, availableRuleTypes, searchText, JSON.stringify(solutionsFilter)]); - - useEffect(() => { - if (ruleTypeModel) { - const ruleType = ruleTypes.find((rt) => rt.id === ruleTypeModel.id); - if (ruleType && useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id)) { - setConsumer(ruleType.producer as RuleCreationValidConsumer); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ruleTypeModel, ruleTypes]); - - const authorizedConsumers = useMemo(() => { - // If the app context provides a consumer, we assume that consumer is - // is what we set for all rules that is created in that context - if (rule.consumer !== ALERTING_FEATURE_ID) { - return []; - } - - const selectedRuleType = availableRuleTypes.find( - ({ ruleType: availableRuleType }) => availableRuleType.id === rule.ruleTypeId - ); - - if (!selectedRuleType?.ruleType?.authorizedConsumers) { - return []; - } - return Object.entries(selectedRuleType.ruleType.authorizedConsumers).reduce< - RuleCreationValidConsumer[] - >((result, [authorizedConsumer, privilege]) => { - if ( - privilege.all && - (validConsumers || VALID_CONSUMERS).includes( - authorizedConsumer as RuleCreationValidConsumer - ) - ) { - result.push(authorizedConsumer as RuleCreationValidConsumer); - } - return result; - }, []); - }, [availableRuleTypes, rule, validConsumers]); - - const shouldShowConsumerSelect = useMemo(() => { - if (!canShowConsumerSelection) { - return false; - } - if (!authorizedConsumers.length) { - return false; - } - return !!rule.ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule.ruleTypeId); - }, [authorizedConsumers, rule, canShowConsumerSelection]); - - const selectedRuleType = rule?.ruleTypeId ? ruleTypeIndex?.get(rule?.ruleTypeId) : undefined; - const recoveryActionGroup = selectedRuleType?.recoveryActionGroup?.id; - - const tagsOptions = rule.tags ? rule.tags.map((label: string) => ({ label })) : []; - - const isActionGroupDisabledForActionType = useCallback( - (ruleType: RuleType, actionGroupId: string, actionTypeId: string): boolean => { - return isActionGroupDisabledForActionTypeId( - actionGroupId === ruleType?.recoveryActionGroup?.id - ? RecoveredActionGroup.id - : actionGroupId, - actionTypeId - ); - }, - [] - ); - - const RuleParamsExpressionComponent = ruleTypeModel ? ruleTypeModel.ruleParamsExpression : null; - - const ruleTypesByProducer = filteredRuleTypes.reduce( - ( - result: Record< - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - >, - ruleTypeValue - ) => { - const producer = ruleTypeValue.ruleType.producer; - if (producer) { - const checkEnabledResult = checkRuleTypeEnabled(ruleTypeValue.ruleType); - if (!checkEnabledResult.isEnabled) { - hasDisabledByLicenseRuleTypes = true; - } - (result[producer] = result[producer] || []).push({ - name: ruleTypeValue.ruleType.name, - id: ruleTypeValue.ruleTypeModel.id, - checkEnabledResult, - ruleTypeItem: ruleTypeValue.ruleTypeModel, - }); - } - return result; - }, - {} - ); - - const sortedRuleTypeNodes = hideGrouping - ? Object.entries(ruleTypesByProducer).sort((a, b) => - ruleTypeUngroupedCompare(a, b, ruleTypeToFilter) - ) - : Object.entries(ruleTypesByProducer).sort((a, b) => ruleTypeGroupCompare(a, b, solutions)); - - const ruleTypeNodes = sortedRuleTypeNodes.map(([solution, items], groupIndex) => ( - - {!hideGrouping && ( - <> - - - - - {(kibanaFeatures - ? getProducerFeatureName(solution, kibanaFeatures) - : capitalize(solution)) ?? capitalize(solution)} - - - - - {items.length} - - - - - )} - - {items - .sort((a, b) => ruleTypeCompare(a, b)) - .map((item, index) => { - const ruleTypeListItemHtml = ( - - {item.name} - -

{item.ruleTypeItem.description}

-
-
- ); - return ( - - {ruleTypeListItemHtml} - - ) - } - isDisabled={!item.checkEnabledResult.isEnabled} - onClick={() => { - setRuleProperty('ruleTypeId', item.id); - setRuleTypeModel(item.ruleTypeItem); - setActions([]); - setRuleProperty('params', {}); - if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { - setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); - } - - if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { - setConsumer(solution as RuleCreationValidConsumer); - } - }} - /> - ); - })} -
- -
- )); - - const getHelpTextForInterval = () => { - if (!config || !config.minimumScheduleInterval) { - return ''; - } - - // No help text if there is an error - if (errors['schedule.interval'].length) { - return ''; - } - - if (config.minimumScheduleInterval.enforce) { - // Always show help text if minimum is enforced - return i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText', { - defaultMessage: 'Interval must be at least {minimum}.', - values: { - minimum: formatDuration(config.minimumScheduleInterval.value, true), - }, - }); - } else if ( - rule.schedule.interval && - parseDuration(rule.schedule.interval) < parseDuration(config.minimumScheduleInterval.value) - ) { - // Only show help text if current interval is less than suggested - return i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText', - { - defaultMessage: - 'Intervals less than {minimum} are not recommended due to performance considerations.', - values: { - minimum: formatDuration(config.minimumScheduleInterval.value, true), - }, - } - ); - } else { - return ''; - } - }; - - const hasFieldsForAAD = useMemo(() => { - const hasAlertHasData = selectedRuleType - ? selectedRuleType.hasFieldsForAAD || - selectedRuleType.producer === AlertConsumers.SIEM || - selectedRuleType.hasAlertsMappings - : false; - - return hasAlertHasData; - }, [selectedRuleType]); - - const ruleTypeDetails = ( - <> - - - - -
- {rule.ruleTypeId && ruleTypeIndex && ruleTypeIndex.has(rule.ruleTypeId) - ? ruleTypeIndex.get(rule.ruleTypeId)!.name - : ''} -
-
-
- {canChangeTrigger ? ( - - { - setRuleProperty('ruleTypeId', null); - setRuleTypeModel(null); - setRuleProperty('params', {}); - }} - /> - - ) : null} -
- {ruleTypeModel?.description && ( - - - - {ruleTypeModel.description}  - {ruleTypeModel?.documentationUrl && ( - - - - )} - - - - )} - - {RuleParamsExpressionComponent && - defaultActionGroupId && - rule.ruleTypeId && - selectedRuleType ? ( - - - - - } - > - - - - - - ) : null} - {hideInterval !== true && ( - <> - - - - {i18n.translate('xpack.triggersActionsUI.sections.ruleForm.ruleScheduleLabel', { - defaultMessage: 'Rule schedule', - })} - - - - - - } - data-test-subj="intervalFormRow" - display="rowCompressed" - helpText={getHelpTextForInterval()} - isInvalid={!!errors['schedule.interval'].length} - error={errors['schedule.interval'] as string[]} - > - - - { - const value = e.target.value; - if (value === '' || INTEGER_REGEX.test(value)) { - const parsedValue = value === '' ? '' : parseInt(value, 10); - setRuleInterval(parsedValue || undefined); - setScheduleProperty('interval', `${parsedValue}${ruleIntervalUnit}`); - } - }} - /> - - - { - setRuleIntervalUnit(e.target.value); - setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); - }} - data-test-subj="intervalInputUnit" - /> - - - - - - - )} - - - - - - } - > - - setRuleProperty('flapping', flapping as Flapping)} - enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED} - /> - - - {shouldShowConsumerSelect && ( - <> - - - - - - )} - - {canShowActions && - defaultActionGroupId && - ruleTypeModel && - rule.ruleTypeId && - selectedRuleType ? ( - <> - {!!errors.actionConnectors.length ? ( - <> - - - - - ) : null} - - - isActionGroupDisabledForActionType(selectedRuleType, actionGroupId, actionTypeId) - } - actionGroups={selectedRuleType.actionGroups.map((actionGroup) => - actionGroup.id === selectedRuleType.recoveryActionGroup.id - ? { - ...actionGroup, - omitMessageVariables: selectedRuleType.doesSetRecoveryContext - ? 'keepContext' - : 'all', - defaultActionMessage: - ruleTypeModel?.defaultRecoveryMessage || recoveredActionGroupMessage, - } - : { ...actionGroup, defaultActionMessage: ruleTypeModel?.defaultActionMessage } - )} - recoveryActionGroup={recoveryActionGroup} - setActionUseAlertDataForTemplate={(enabled: boolean, index: number) => { - setActionProperty('useAlertDataForTemplate', enabled, index); - }} - setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)} - setActionGroupIdByIndex={(group: string, index: number) => - setActionProperty('group', group, index) - } - setActions={setActions} - setActionParamsProperty={setActionParamsProperty} - actionTypeRegistry={actionTypeRegistry} - setActionFrequencyProperty={setActionFrequencyProperty} - setActionAlertsFilterProperty={setActionAlertsFilterProperty} - defaultSummaryMessage={ruleTypeModel?.defaultSummaryMessage || summaryMessage} - minimumThrottleInterval={[ruleInterval, ruleIntervalUnit]} - /> - - ) : null} - - ); - - return ( - - - - - } - isInvalid={!!errors.name.length && rule.name !== undefined} - error={errors.name as string} - > - { - setRuleProperty('name', e.target.value); - }} - onBlur={() => { - if (!rule.name) { - setRuleProperty('name', ''); - } - }} - /> - - - - - - - } - > - { - const newOptions = [...tagsOptions, { label: searchValue }]; - setRuleProperty( - 'tags', - newOptions.map((newOption) => newOption.label) - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - setRuleProperty( - 'tags', - selectedOptions.map((selectedOption) => selectedOption.label) - ); - }} - onBlur={() => { - if (!rule.tags) { - setRuleProperty('tags', []); - } - }} - /> - - - - -
- {ruleTypeModel ? ( - <>{ruleTypeDetails} - ) : availableRuleTypes.length ? ( - <> - - - - - - - ) - } - label={ - -
- -
-
- } - > - - - { - setInputText(e.target.value); - if (e.target.value === '') { - setSearchText(''); - } - }} - onKeyUp={(e) => { - if (e.keyCode === ENTER_KEY) { - setSearchText(inputText); - } - }} - placeholder={i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle', - { defaultMessage: 'Search' } - )} - /> - - {solutions ? ( - - - setSolutionFilter(selectedSolutions) - } - /> - - ) : null} - -
- - {!!errors.ruleTypeId.length && rule.ruleTypeId !== undefined ? ( - <> - - - - - ) : null} - {ruleTypeNodes} - - ) : ruleTypeIndex && !ruleTypesIsLoading ? ( - - ) : ( - - - - )} -
-
- ); -}; - -const NoAuthorizedRuleTypes = ({ operation }: { operation: string }) => ( - - - - } - body={ -
-

- -

-
- } - /> -); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx deleted file mode 100644 index 25c6de0225edb..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.test.tsx +++ /dev/null @@ -1,232 +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 React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { httpServiceMock } from '@kbn/core/public/mocks'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { RuleFormAdvancedOptions } from './rule_form_advanced_options'; -import { useKibana } from '../../../common/lib/kibana'; -import userEvent from '@testing-library/user-event'; -import { ApplicationStart } from '@kbn/core-application-browser'; - -jest.mock('../../../common/lib/kibana'); - -const useKibanaMock = useKibana as jest.Mocked; - -const queryClient = new QueryClient(); - -const http = httpServiceMock.createStartContract(); - -const mockFlappingSettings = { - lookBackWindow: 5, - statusChangeThreshold: 5, -}; - -const mockOnflappingChange = jest.fn(); -const mockAlertDelayChange = jest.fn(); - -describe('ruleFormAdvancedOptions', () => { - beforeEach(() => { - http.get.mockResolvedValue({ - look_back_window: 10, - status_change_threshold: 3, - enabled: true, - }); - useKibanaMock().services.http = http; - useKibanaMock().services.application.capabilities = { - rulesSettings: { - writeFlappingSettingsUI: true, - }, - } as unknown as ApplicationStart['capabilities']; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('should render correctly', async () => { - render( - - - - - - ); - - expect(await screen.findByTestId('ruleFormAdvancedOptions')).toBeInTheDocument(); - expect(await screen.findByTestId('alertDelayFormRow')).toBeInTheDocument(); - expect(await screen.findByTestId('alertFlappingFormRow')).toBeInTheDocument(); - }); - - test('should initialize correctly when global flapping is on and override is not applied', async () => { - render( - - - - - - ); - - expect(await screen.findByText('ON')).toBeInTheDocument(); - expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked(); - expect(screen.queryByText('Custom')).not.toBeInTheDocument(); - expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - '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')); - expect(mockOnflappingChange).toHaveBeenCalledWith({ - lookBackWindow: 10, - statusChangeThreshold: 3, - }); - }); - - test('should initialize correctly when global flapping is on and override is appplied', async () => { - render( - - - - - - ); - - expect(await screen.findByTestId('ruleFormAdvancedOptionsOverrideSwitch')).toBeChecked(); - expect(screen.getByText('Custom')).toBeInTheDocument(); - expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6'); - expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4'); - expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent( - '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')); - expect(mockOnflappingChange).toHaveBeenCalledWith(null); - }); - - test('should not allow override when global flapping is off', async () => { - http.get.mockResolvedValue({ - look_back_window: 10, - status_change_threshold: 3, - enabled: false, - }); - useKibanaMock().services.http = http; - - render( - - - - - - ); - - expect(await screen.findByText('OFF')).toBeInTheDocument(); - 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 () => { - render( - - - - - - ); - - expect(await screen.findByTestId('lookBackWindowRangeInput')).toBeInTheDocument(); - - const lookBackWindowInput = screen.getByTestId('lookBackWindowRangeInput'); - const statusChangeThresholdInput = screen.getByTestId('statusChangeThresholdRangeInput'); - - // Change lookBackWindow to a smaller value - fireEvent.change(lookBackWindowInput, { target: { value: 5 } }); - // statusChangeThresholdInput gets pinned to be 5 - expect(mockOnflappingChange).toHaveBeenLastCalledWith({ - lookBackWindow: 5, - statusChangeThreshold: 5, - }); - - // Try making statusChangeThreshold bigger - fireEvent.change(statusChangeThresholdInput, { target: { value: 20 } }); - // Still pinned - expect(mockOnflappingChange).toHaveBeenLastCalledWith({ - lookBackWindow: 10, - statusChangeThreshold: 10, - }); - - fireEvent.change(statusChangeThresholdInput, { target: { value: 3 } }); - expect(mockOnflappingChange).toHaveBeenLastCalledWith({ - lookBackWindow: 10, - statusChangeThreshold: 3, - }); - }); - - test('should not render flapping if enableFlapping is false', () => { - render( - - - - - - ); - - expect(screen.queryByTestId('alertFlappingFormRow')).not.toBeInTheDocument(); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx deleted file mode 100644 index 00ad6186d58e8..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_advanced_options.tsx +++ /dev/null @@ -1,166 +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 React, { useCallback, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIconTip, - EuiPanel, - EuiLoadingSpinner, -} from '@elastic/eui'; -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( - 'xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel', - { - defaultMessage: 'Alert delay', - } -); - -const alertDelayIconTipDescription = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp', - { - defaultMessage: - 'An alert occurs only when the specified number of consecutive runs meet the rule conditions.', - } -); - -const alertDelayPrependLabel = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel', - { - defaultMessage: 'Alert after', - } -); - -const alertDelayAppendLabel = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel', - { - defaultMessage: 'consecutive matches', - } -); - -const flappingFormRowLabel = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.flappingLabel', - { - defaultMessage: 'Alert flapping detection', - } -); - -const INTEGER_REGEX = /^[1-9][0-9]*$/; - -export interface RuleFormAdvancedOptionsProps { - alertDelay?: number; - flappingSettings?: RuleSpecificFlappingProperties | null; - onAlertDelayChange: (value: string) => void; - onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void; - enabledFlapping?: boolean; -} - -export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => { - const { - alertDelay, - flappingSettings, - enabledFlapping = true, - onAlertDelayChange, - onFlappingChange, - } = props; - - const { - application: { - capabilities: { rulesSettings }, - }, - http, - } = useKibana().services; - - const { writeFlappingSettingsUI } = rulesSettings || {}; - - const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState(false); - - const { data: spaceFlappingSettings, isInitialLoading } = useFetchFlappingSettings({ - http, - enabled: enabledFlapping, - }); - - const internalOnAlertDelayChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - if (value === '' || INTEGER_REGEX.test(value)) { - onAlertDelayChange(value); - } - }, - [onAlertDelayChange] - ); - - return ( - - - - - {alertDelayFormRowLabel} - - - - - } - data-test-subj="alertDelayFormRow" - display="rowCompressed" - > - - - - {isInitialLoading && } - {spaceFlappingSettings && enabledFlapping && ( - - - {flappingFormRowLabel} - - - - - } - data-test-subj="alertFlappingFormRow" - display="rowCompressed" - > - - - - )} - - - ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx deleted file mode 100644 index c98733d377f0e..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.test.tsx +++ /dev/null @@ -1,222 +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 React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { RuleFormConsumerSelection } from './rule_form_consumer_selection'; -import { RuleCreationValidConsumer } from '../../../types'; -import { useKibana } from '../../../common/lib/kibana'; - -const mockConsumers: RuleCreationValidConsumer[] = ['logs', 'infrastructure', 'stackAlerts']; - -const mockOnChange = jest.fn(); - -jest.mock('../../../common/lib/kibana'); -const useKibanaMock = useKibana as jest.Mocked; - -describe('RuleFormConsumerSelectionModal', () => { - beforeEach(() => { - jest.clearAllMocks(); - useKibanaMock().services.isServerless = false; - }); - - it('renders correctly', async () => { - render( - - ); - - expect(screen.getByTestId('ruleFormConsumerSelect')).toBeInTheDocument(); - expect(screen.getByTestId('comboBoxSearchInput')).toHaveAttribute( - 'placeholder', - 'Select a scope' - ); - expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue(''); - fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); - expect(screen.getByText('Logs')).toBeInTheDocument(); - expect(screen.getByText('Metrics')).toBeInTheDocument(); - expect(screen.getByText('Stack Rules')).toBeInTheDocument(); - }); - - it('should be able to initialize to the prop initialSelectedConsumer', () => { - render( - - ); - expect(mockOnChange).toHaveBeenLastCalledWith('logs'); - }); - - it('should NOT initialize if initialSelectedConsumer is equal to null', () => { - render( - - ); - expect(mockOnChange).not.toBeCalled(); - }); - - it('should initialize to the first valid consumers if initialSelectedConsumer is not valid', () => { - render( - - ); - expect(mockOnChange).toHaveBeenLastCalledWith('logs'); - }); - - it('should initialize to stackAlerts if the initialSelectedConsumer is not a valid and consumers has stackAlerts', () => { - render( - - ); - expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); - }); - - it('should initialize to stackAlerts if the initialSelectedConsumer is undefined and consumers has stackAlerts', () => { - render( - - ); - expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); - }); - - it('should be able to select infrastructure and call onChange', () => { - render( - - ); - - fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); - fireEvent.click(screen.getByTestId('infrastructure')); - expect(mockOnChange).toHaveBeenLastCalledWith('infrastructure'); - }); - - it('should be able to select logs and call onChange', () => { - render( - - ); - - fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); - fireEvent.click(screen.getByTestId('logs')); - expect(mockOnChange).toHaveBeenLastCalledWith('logs'); - }); - - it('should be able to show errors when there is one', () => { - render( - - ); - expect(screen.queryAllByText('Scope is required')).toHaveLength(1); - }); - - it('should display nothing if there is only 1 consumer to select', () => { - render( - - ); - - expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts'); - expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument(); - }); - - it('should display the initial selected consumer', () => { - render( - - ); - - expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('Logs'); - }); - - it('should not display the initial selected consumer if it is not a selectable option', () => { - render( - - ); - expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue(''); - }); - - it('should not show the role visibility dropdown on serverless on an o11y project', () => { - useKibanaMock().services.isServerless = true; - - render( - - ); - expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument(); - }); - - it('should set the consumer correctly on an o11y project', () => { - useKibanaMock().services.isServerless = true; - - render( - - ); - expect(mockOnChange).toHaveBeenLastCalledWith('observability'); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx deleted file mode 100644 index 9fff99c1c9998..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_consumer_selection.tsx +++ /dev/null @@ -1,197 +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 React, { useMemo, useCallback, useEffect } from 'react'; -import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AlertConsumers, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; -import { IErrorObject, RuleCreationValidConsumer } from '../../../types'; -import { useKibana } from '../../../common/lib/kibana'; - -const SELECT_LABEL: string = i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel', - { - defaultMessage: 'Role visibility', - } -); - -const featureNameMap: Record = { - [AlertConsumers.LOGS]: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs', - { - defaultMessage: 'Logs', - } - ), - [AlertConsumers.INFRASTRUCTURE]: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure', - { - defaultMessage: 'Metrics', - } - ), - [AlertConsumers.APM]: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm', - { - defaultMessage: 'APM and User Experience', - } - ), - [AlertConsumers.UPTIME]: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime', - { - defaultMessage: 'Synthetics and Uptime', - } - ), - [AlertConsumers.SLO]: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo', - { - defaultMessage: 'SLOs', - } - ), - stackAlerts: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts', - { - defaultMessage: 'Stack Rules', - } - ), -}; - -export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ - AlertConsumers.LOGS, - AlertConsumers.INFRASTRUCTURE, - 'stackAlerts', - 'alerts', -]; - -export interface RuleFormConsumerSelectionProps { - consumers: RuleCreationValidConsumer[]; - onChange: (consumer: RuleCreationValidConsumer | null) => void; - errors: IErrorObject; - selectedConsumer?: RuleCreationValidConsumer | null; - /* FUTURE ENGINEER - * if this prop is set to null then we wont initialize the value and the user will have to set it - * if this prop is set to a valid consumers then we will set it up to what was passed - * if this prop is not valid or undefined but the valid consumers has stackAlerts then we will default it to stackAlerts - */ - initialSelectedConsumer?: RuleCreationValidConsumer | null; -} - -const SINGLE_SELECTION = { asPlainText: true }; - -export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps) => { - const { isServerless } = useKibana().services; - - const { consumers, errors, onChange, selectedConsumer, initialSelectedConsumer } = props; - const isInvalid = (errors?.consumer as string[])?.length > 0; - - const handleOnChange = useCallback( - (selected: Array>) => { - if (selected.length > 0) { - const newSelectedConsumer = selected[0]; - onChange(newSelectedConsumer.value!); - } else { - onChange(null); - } - }, - [onChange] - ); - - const validatedSelectedConsumer = useMemo(() => { - if ( - selectedConsumer && - consumers.includes(selectedConsumer) && - featureNameMap[selectedConsumer] - ) { - return selectedConsumer; - } - return null; - }, [selectedConsumer, consumers]); - - const selectedOptions = useMemo( - () => - validatedSelectedConsumer - ? [{ value: validatedSelectedConsumer, label: featureNameMap[validatedSelectedConsumer] }] - : [], - [validatedSelectedConsumer] - ); - - const formattedSelectOptions: Array> = - useMemo(() => { - return consumers - .reduce>>((result, consumer) => { - if (featureNameMap[consumer]) { - result.push({ - value: consumer, - 'data-test-subj': consumer, - label: featureNameMap[consumer], - }); - } - return result; - }, []) - .sort((a, b) => { - return a.value!.localeCompare(b.value!); - }); - }, [consumers]); - - useEffect(() => { - // At initialization, select initialSelectedConsumer or the first value - if (!validatedSelectedConsumer) { - if (initialSelectedConsumer === null) { - return; - } else if (initialSelectedConsumer && consumers.includes(initialSelectedConsumer)) { - onChange(initialSelectedConsumer); - return; - } else if (consumers.includes(STACK_ALERTS_FEATURE_ID)) { - onChange(STACK_ALERTS_FEATURE_ID); - return; - } - onChange(consumers[0]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (consumers.length === 1) { - onChange(consumers[0] as RuleCreationValidConsumer); - } else if (isServerless && consumers.includes(AlertConsumers.OBSERVABILITY)) { - onChange(AlertConsumers.OBSERVABILITY as RuleCreationValidConsumer); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [consumers]); - - if (consumers.length <= 1 || (isServerless && consumers.includes(AlertConsumers.OBSERVABILITY))) { - return null; - } - - return ( - - - - ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx deleted file mode 100644 index 9be2cde33d058..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx +++ /dev/null @@ -1,102 +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 React, { useEffect } from 'react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { RuleForm } from '@kbn/response-ops-rule-form'; -import { getRuleDetailsRoute } from '@kbn/rule-data-utils'; -import { useLocation, useParams } from 'react-router-dom'; -import { useKibana } from '../../../common/lib/kibana'; -import { getAlertingSectionBreadcrumb } from '../../lib/breadcrumb'; -import { getCurrentDocTitle } from '../../lib/doc_title'; - -export const RuleFormRoute = () => { - const { - http, - application, - notifications, - charts, - settings, - data, - dataViews, - unifiedSearch, - docLinks, - ruleTypeRegistry, - actionTypeRegistry, - chrome, - isServerless, - setBreadcrumbs, - ...startServices - } = useKibana().services; - - const location = useLocation<{ returnApp?: string; returnPath?: string }>(); - const { id, ruleTypeId } = useParams<{ - id?: string; - ruleTypeId?: string; - }>(); - const { returnApp, returnPath } = location.state || {}; - - // Set breadcrumb and page title - useEffect(() => { - if (id) { - setBreadcrumbs([ - getAlertingSectionBreadcrumb('rules', true), - getAlertingSectionBreadcrumb('editRule'), - ]); - chrome.docTitle.change(getCurrentDocTitle('editRule')); - } - if (ruleTypeId) { - setBreadcrumbs([ - getAlertingSectionBreadcrumb('rules', true), - getAlertingSectionBreadcrumb('createRule'), - ]); - chrome.docTitle.change(getCurrentDocTitle('createRule')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - { - if (returnApp && returnPath) { - application.navigateToApp(returnApp, { path: returnPath }); - } else { - application.navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/rules`, - }); - } - }} - onSubmit={(ruleId) => { - application.navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(ruleId)}`, - }); - }} - /> - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleFormRoute as default }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx deleted file mode 100644 index f5f2509f570fd..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.test.tsx +++ /dev/null @@ -1,144 +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 React from 'react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { Rule } from '../../../types'; -import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common'; -import { RuleNotifyWhen } from './rule_notify_when'; - -describe('rule_notify_when', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - const onNotifyWhenChange = jest.fn(); - const onThrottleChange = jest.fn(); - - describe('action_frequency_form new rule', () => { - let wrapper: ReactWrapper; - - async function setup(overrides = {}) { - const initialRule = { - name: 'test', - params: {}, - consumer: ALERTING_FEATURE_ID, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - muteAll: false, - enabled: false, - mutedInstanceIds: [], - notifyWhen: 'onActionGroupChange', - ...overrides, - } as unknown as Rule; - - wrapper = mountWithIntl( - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it(`should determine initial selection from throttle value if 'notifyWhen' is null`, async () => { - await setup({ notifyWhen: null }); - const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]'); - expect(notifyWhenSelect.exists()).toBeTruthy(); - expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert'); - }); - - it(`should correctly select 'onActionGroupChange' option on initial render`, async () => { - await setup(); - const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]'); - expect(notifyWhenSelect.exists()).toBeTruthy(); - expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActionGroupChange'); - expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy(); - }); - - it(`should correctly select 'onActiveAlert' option on initial render`, async () => { - await setup({ - notifyWhen: 'onActiveAlert', - }); - const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]'); - expect(notifyWhenSelect.exists()).toBeTruthy(); - expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert'); - expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy(); - }); - - it(`should correctly select 'onThrottleInterval' option on initial render and render throttle inputs`, async () => { - await setup({ - notifyWhen: 'onThrottleInterval', - }); - const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]'); - expect(notifyWhenSelect.exists()).toBeTruthy(); - expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onThrottleInterval'); - - const throttleInput = wrapper.find('[data-test-subj="throttleInput"]'); - expect(throttleInput.exists()).toBeTruthy(); - expect(throttleInput.at(1).prop('value')).toEqual(1); - - const throttleUnitInput = wrapper.find('[data-test-subj="throttleUnitInput"]'); - expect(throttleUnitInput.exists()).toBeTruthy(); - expect(throttleUnitInput.at(1).prop('value')).toEqual('m'); - }); - - it('should update action frequency type correctly', async () => { - await setup(); - - wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="onActiveAlert"]').simulate('click'); - wrapper.update(); - expect(onNotifyWhenChange).toHaveBeenCalledWith('onActiveAlert'); - expect(onThrottleChange).toHaveBeenCalledWith(null, 'm'); - - wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="onActionGroupChange"]').simulate('click'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy(); - expect(onNotifyWhenChange).toHaveBeenCalledWith('onActionGroupChange'); - expect(onThrottleChange).toHaveBeenCalledWith(null, 'm'); - }); - - it('should renders throttle input when custom throttle is selected and update throttle value', async () => { - await setup({ - notifyWhen: 'onThrottleInterval', - }); - - const newThrottle = 17; - const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); - expect(throttleField.exists()).toBeTruthy(); - throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } }); - const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); - expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); - expect(onThrottleChange).toHaveBeenCalledWith(17, 'm'); - - const newThrottleUnit = 'h'; - const throttleUnitField = wrapper.find('[data-test-subj="throttleUnitInput"]'); - expect(throttleUnitField.exists()).toBeTruthy(); - throttleUnitField.at(1).simulate('change', { target: { value: newThrottleUnit } }); - expect(onThrottleChange).toHaveBeenCalledWith(null, 'h'); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx deleted file mode 100644 index 642d0316cdbf9..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_notify_when.tsx +++ /dev/null @@ -1,236 +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 React, { useState, useEffect, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiFormRow, - EuiFieldNumber, - EuiSelect, - EuiText, - EuiSpacer, - EuiSuperSelect, - EuiSuperSelectOption, -} from '@elastic/eui'; -import { some, filter, map } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { InitialRule } from './rule_reducer'; -import { getTimeOptions } from '../../../common/lib/get_time_options'; -import { RuleNotifyWhenType } from '../../../types'; - -const DEFAULT_NOTIFY_WHEN_VALUE: RuleNotifyWhenType = 'onActionGroupChange'; - -export const NOTIFY_WHEN_OPTIONS: Array> = [ - { - value: 'onActionGroupChange', - inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display', - { - defaultMessage: 'On status changes', - } - ), - 'data-test-subj': 'onActionGroupChange', - dropdownDisplay: ( - <> - - - - -

- -

-
- - ), - }, - { - value: 'onActiveAlert', - inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display', - { - defaultMessage: 'On check intervals', - } - ), - 'data-test-subj': 'onActiveAlert', - dropdownDisplay: ( - <> - - - - -

- -

-
- - ), - }, - { - value: 'onThrottleInterval', - inputDisplay: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display', - { - defaultMessage: 'On custom action intervals', - } - ), - 'data-test-subj': 'onThrottleInterval', - dropdownDisplay: ( - <> - - - - -

- -

-
- - ), - }, -]; - -interface RuleNotifyWhenProps { - rule: InitialRule; - throttle: number | null; - throttleUnit: string; - onNotifyWhenChange: (notifyWhen: RuleNotifyWhenType) => void; - onThrottleChange: (throttle: number | null, throttleUnit: string) => void; -} - -export const RuleNotifyWhen = ({ - rule, - throttle, - throttleUnit, - onNotifyWhenChange, - onThrottleChange, -}: RuleNotifyWhenProps) => { - const [ruleThrottle, setRuleThrottle] = useState(throttle || 1); - const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState(false); - const [notifyWhenValue, setNotifyWhenValue] = - useState(DEFAULT_NOTIFY_WHEN_VALUE); - - useEffect(() => { - if (rule.notifyWhen) { - setNotifyWhenValue(rule.notifyWhen); - } else { - // If 'notifyWhen' is not set, derive value from existence of throttle value - setNotifyWhenValue(rule.throttle ? 'onThrottleInterval' : 'onActiveAlert'); - } - }, [rule]); - - useEffect(() => { - setShowCustomThrottleOpts(notifyWhenValue === 'onThrottleInterval'); - }, [notifyWhenValue]); - - const onNotifyWhenValueChange = useCallback((newValue: RuleNotifyWhenType) => { - onThrottleChange(newValue === 'onThrottleInterval' ? ruleThrottle : null, throttleUnit); - onNotifyWhenChange(newValue); - setNotifyWhenValue(newValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const labelForRuleRenotify = ( - <> - {' '} - - - ); - - return ( - <> - - - - - {showCustomThrottleOpts && ( - <> - - - - - { - pipe( - some(e.target.value.trim()), - filter((value) => value !== ''), - map((value) => parseInt(value, 10)), - filter((value) => !isNaN(value)), - map((value) => { - setRuleThrottle(value); - onThrottleChange(value, throttleUnit); - }) - ); - }} - /> - - - { - onThrottleChange(throttle, e.target.value); - }} - /> - - - - - )} - - - - - ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts deleted file mode 100644 index 2d2082b64b6e2..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.test.ts +++ /dev/null @@ -1,277 +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 { getRuleReducer } from './rule_reducer'; -import { ActionTypeModel, Rule } from '../../../types'; -import { SanitizedRuleAction } from '@kbn/alerting-plugin/common'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; - -const actionTypeRegistry = actionTypeRegistryMock.create(); -const actionType = { - id: 'test', - name: 'Test', - isSystemActionType: false, -} as unknown as ActionTypeModel; -actionTypeRegistry.get.mockReturnValue(actionType); -describe('rule reducer', () => { - const ruleReducer = getRuleReducer(actionTypeRegistry); - let initialRule: Rule; - beforeAll(() => { - initialRule = { - params: {}, - consumer: 'rules', - ruleTypeId: null, - schedule: { - interval: '1m', - }, - actions: [], - tags: [], - notifyWhen: 'onActionGroupChange', - alertDelay: { - active: 5, - }, - } as unknown as Rule; - }); - - // setRule - test('if modified rule was reset to initial', () => { - const rule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setProperty' }, - payload: { - key: 'name', - value: 'new name', - }, - } - ); - expect(rule.rule.name).toBe('new name'); - - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRule' }, - payload: { - key: 'rule', - value: initialRule, - }, - } - ); - expect(updatedRule.rule.name).toBeUndefined(); - }); - - test('if property name was changed', () => { - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setProperty' }, - payload: { - key: 'name', - value: 'new name', - }, - } - ); - expect(updatedRule.rule.name).toBe('new name'); - }); - - test('if initial schedule property was updated', () => { - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setScheduleProperty' }, - payload: { - key: 'interval', - value: '10s', - }, - } - ); - expect(updatedRule.rule.schedule.interval).toBe('10s'); - }); - - test('if rule params property was added and updated', () => { - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleParams' }, - payload: { - key: 'testParam', - value: 'new test params property', - }, - } - ); - expect(updatedRule.rule.params.testParam).toBe('new test params property'); - - const updatedRuleParamsProperty = ruleReducer( - { rule: updatedRule.rule }, - { - command: { type: 'setRuleParams' }, - payload: { - key: 'testParam', - value: 'test params property updated', - }, - } - ); - expect(updatedRuleParamsProperty.rule.params.testParam).toBe('test params property updated'); - }); - - test('if rule action params property was added and updated', () => { - initialRule.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Rule', - params: {}, - uuid: '123-456', - }); - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionParams' }, - payload: { - key: 'testActionParam', - value: 'new test action params property', - index: 0, - }, - } - ); - expect(updatedRule.rule.actions[0].params.testActionParam).toBe( - 'new test action params property' - ); - - const updatedRuleActionParamsProperty = ruleReducer( - { rule: updatedRule.rule }, - { - command: { type: 'setRuleActionParams' }, - payload: { - key: 'testActionParam', - value: 'test action params property updated', - index: 0, - }, - } - ); - expect(updatedRuleActionParamsProperty.rule.actions[0].params.testActionParam).toBe( - 'test action params property updated' - ); - }); - - test('if the existing rule action params property was set to undefined (when other connector was selected)', () => { - initialRule.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Rule', - params: { - testActionParam: 'some value', - }, - uuid: '123-456', - }); - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionParams' }, - payload: { - key: 'testActionParam', - value: undefined, - index: 0, - }, - } - ); - expect(updatedRule.rule.actions[0].params.testActionParam).toBe(undefined); - }); - - test('if rule action property was updated', () => { - initialRule.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Rule', - params: {}, - uuid: '123-456', - }); - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionProperty' }, - payload: { - key: 'group', - value: 'Warning', - index: 0, - }, - } - ); - expect((updatedRule.rule.actions[0] as SanitizedRuleAction).group).toBe('Warning'); - }); - - test('if rule action frequency was updated', () => { - initialRule.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Rule', - params: {}, - uuid: '123-456', - }); - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionFrequency' }, - payload: { - key: 'notifyWhen', - value: 'onThrottleInterval', - index: 0, - }, - } - ); - expect((updatedRule.rule.actions[0] as SanitizedRuleAction).frequency?.notifyWhen).toBe( - 'onThrottleInterval' - ); - }); - - test('if initial alert delay property was updated', () => { - const updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setAlertDelayProperty' }, - payload: { - key: 'active', - value: 10, - }, - } - ); - expect(updatedRule.rule.alertDelay?.active).toBe(10); - }); - - test('if rule action alerts filter was toggled on, then off', () => { - initialRule.actions.push({ - id: '', - actionTypeId: 'testId', - group: 'Rule', - params: {}, - uuid: '123-456', - }); - let updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionAlertsFilter' }, - payload: { - key: 'query', - value: 'hello', - index: 0, - }, - } - ); - expect((updatedRule.rule.actions[0] as SanitizedRuleAction).alertsFilter).toBeDefined(); - updatedRule = ruleReducer( - { rule: initialRule }, - { - command: { type: 'setRuleActionAlertsFilter' }, - payload: { - key: 'query', - value: undefined, - index: 0, - }, - } - ); - expect((updatedRule.rule.actions[0] as SanitizedRuleAction).alertsFilter).toBeUndefined(); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts deleted file mode 100644 index 33a7284ac2bef..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts +++ /dev/null @@ -1,319 +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 { SavedObjectAttribute } from '@kbn/core/public'; -import { isEqual, isUndefined, omitBy } from 'lodash'; -import { Reducer } from 'react'; -import { - RuleActionParam, - IntervalSchedule, - RuleActionAlertsFilterProperty, - AlertsFilter, - AlertDelay, - SanitizedRuleAction, -} from '@kbn/alerting-plugin/common'; -import { isEmpty } from 'lodash/fp'; -import { ActionTypeRegistryContract, Rule, RuleUiAction } from '../../../types'; -import { DEFAULT_FREQUENCY } from '../../../common/constants'; - -export type InitialRule = Partial & - Pick; - -interface CommandType< - T extends - | 'setRule' - | 'setProperty' - | 'setScheduleProperty' - | 'setRuleParams' - | 'setRuleActionParams' - | 'setRuleActionProperty' - | 'setRuleActionFrequency' - | 'setRuleActionAlertsFilter' - | 'setAlertDelayProperty' -> { - type: T; -} - -export interface RuleState { - rule: InitialRule; -} - -interface Payload { - key: Keys; - value: Value; - index?: number; -} - -interface RulePayload { - key: Key; - value: Rule[Key] | null; - index?: number; -} - -interface RuleActionPayload { - key: Key; - value: RuleUiAction[Key] | null; - index?: number; -} - -interface RuleSchedulePayload { - key: Key; - value: IntervalSchedule[Key]; - index?: number; -} - -interface AlertDelayPayload { - key: Key; - value: AlertDelay[Key] | null; - index?: number; -} - -export type RuleReducerAction = - | { - command: CommandType<'setRule'>; - payload: Payload<'rule', InitialRule>; - } - | { - command: CommandType<'setProperty'>; - payload: RulePayload; - } - | { - command: CommandType<'setScheduleProperty'>; - payload: RuleSchedulePayload; - } - | { - command: CommandType<'setRuleParams'>; - payload: Payload; - } - | { - command: CommandType<'setRuleActionParams'>; - payload: Payload; - } - | { - command: CommandType<'setRuleActionProperty'>; - payload: Payload; - } - | { - command: CommandType<'setRuleActionFrequency'>; - payload: Payload; - } - | { - command: CommandType<'setRuleActionAlertsFilter'>; - payload: Payload; - } - | { - command: CommandType<'setAlertDelayProperty'>; - payload: AlertDelayPayload; - }; - -export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>; -export type ConcreteRuleReducer = Reducer<{ rule: Rule }, RuleReducerAction>; - -export const getRuleReducer = - (actionTypeRegistry: ActionTypeRegistryContract) => - (state: { rule: RulePhase }, action: RuleReducerAction) => { - const { rule } = state; - - switch (action.command.type) { - case 'setRule': { - const { key, value } = action.payload as Payload<'rule', RulePhase>; - if (key === 'rule') { - return { - ...state, - rule: value, - }; - } else { - return state; - } - } - case 'setProperty': { - const { key, value } = action.payload as RulePayload; - if (isEqual(rule[key], value)) { - return state; - } else { - return { - ...state, - rule: { - ...rule, - [key]: value, - }, - }; - } - } - case 'setScheduleProperty': { - const { key, value } = action.payload as RuleSchedulePayload; - if (rule.schedule && isEqual(rule.schedule[key], value)) { - return state; - } else { - return { - ...state, - rule: { - ...rule, - schedule: { - ...rule.schedule, - [key]: value, - }, - }, - }; - } - } - case 'setRuleParams': { - const { key, value } = action.payload as Payload>; - if (isEqual(rule.params[key], value)) { - return state; - } else { - return { - ...state, - rule: { - ...rule, - params: { - ...rule.params, - [key]: value, - }, - }, - }; - } - } - case 'setRuleActionParams': { - const { key, value, index } = action.payload as Payload< - keyof RuleUiAction, - SavedObjectAttribute - >; - if ( - index === undefined || - rule.actions[index] == null || - (!!rule.actions[index][key] && isEqual(rule.actions[index][key], value)) - ) { - return state; - } else { - const oldAction = rule.actions.splice(index, 1)[0]; - const updatedAction = { - ...oldAction, - params: { - ...oldAction.params, - [key]: value, - }, - }; - rule.actions.splice(index, 0, updatedAction); - return { - ...state, - rule: { - ...rule, - actions: [...rule.actions], - }, - }; - } - } - case 'setRuleActionFrequency': { - const { key, value, index } = action.payload as Payload< - keyof RuleUiAction, - SavedObjectAttribute - >; - if ( - index === undefined || - rule.actions[index] == null || - (!!rule.actions[index][key] && isEqual(rule.actions[index][key], value)) - ) { - return state; - } else { - const oldAction = rule.actions.splice(index, 1)[0]; - if (actionTypeRegistry.get(oldAction.actionTypeId).isSystemActionType) { - return state; - } - const oldSanitizedAction = oldAction as SanitizedRuleAction; - const updatedAction = { - ...oldSanitizedAction, - frequency: { - ...(oldSanitizedAction?.frequency ?? DEFAULT_FREQUENCY), - [key]: value, - }, - }; - rule.actions.splice(index, 0, updatedAction); - return { - ...state, - rule: { - ...rule, - actions: [...rule.actions], - }, - }; - } - } - case 'setRuleActionAlertsFilter': { - const { key, value, index } = action.payload as Payload< - keyof AlertsFilter, - RuleActionAlertsFilterProperty - >; - if (index === undefined || rule.actions[index] == null) { - return state; - } else { - const oldAction = rule.actions.splice(index, 1)[0]; - if (actionTypeRegistry.get(oldAction.actionTypeId).isSystemActionType) { - return state; - } - const oldSanitizedAction = oldAction as SanitizedRuleAction; - if ( - oldSanitizedAction.alertsFilter && - isEqual(oldSanitizedAction.alertsFilter[key], value) - ) - return state; - - const { alertsFilter, ...rest } = oldSanitizedAction; - const updatedAlertsFilter = omitBy({ ...alertsFilter, [key]: value }, isUndefined); - - const updatedAction = { - ...rest, - ...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}), - }; - rule.actions.splice(index, 0, updatedAction); - return { - ...state, - rule: { - ...rule, - actions: [...rule.actions], - }, - }; - } - } - case 'setRuleActionProperty': { - const { key, value, index } = action.payload as RuleActionPayload; - if (index === undefined || isEqual(rule.actions[index][key], value)) { - return state; - } else { - const oldAction = rule.actions.splice(index, 1)[0]; - const updatedAction = { - ...oldAction, - [key]: value, - }; - rule.actions.splice(index, 0, updatedAction); - return { - ...state, - rule: { - ...rule, - actions: [...rule.actions], - }, - }; - } - } - case 'setAlertDelayProperty': { - const { key, value } = action.payload as Payload; - if (rule.alertDelay && isEqual(rule.alertDelay[key], value)) { - return state; - } else { - return { - ...state, - rule: { - ...rule, - alertDelay: { - ...rule.alertDelay, - [key]: value, - }, - }, - }; - } - } - } - }; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.test.tsx deleted file mode 100644 index 1fb7ebaca3de2..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.test.tsx +++ /dev/null @@ -1,242 +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 React from 'react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { render, cleanup } from '@testing-library/react'; -import { ShowRequestModal, ShowRequestModalProps } from './show_request_modal'; -import { Rule, RuleTypeParams, RuleUpdates } from '../../../types'; -import { InitialRule } from './rule_reducer'; - -const testDate = new Date('2024-04-04T19:34:24.902Z'); -const shared = { - params: { - searchType: 'esQuery', - timeWindowSize: 5, - timeWindowUnit: 'm', - threshold: [1000], - thresholdComparator: '>', - size: 100, - esQuery: '{\n "query":{\n "match_all" : {}\n }\n }', - aggType: 'count', - groupBy: 'all', - termSize: 5, - excludeHitsFromPreviousRun: false, - sourceFields: [], - index: ['.kibana'], - timeField: 'created_at', - }, - consumer: 'stackAlerts', - ruleTypeId: '.es-query', - schedule: { interval: '1m' }, - actions: [ - { - id: '0be65bf4-58b8-4c44-ba4d-5112c65103f5', - actionTypeId: '.server-log', - group: 'query matched', - params: { - level: 'info', - message: - "Elasticsearch query rule '{{rule.name}}' is active:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}\n- Link: {{context.link}}", - }, - frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false }, - uuid: 'a330a154-61fb-42a8-9bce-9dfd8513a12d', - }, - ], - tags: ['test'], - name: 'test', -}; - -const rule: Rule | InitialRule = { ...shared }; - -const editRule: Rule = { - createdBy: 'elastic', - updatedBy: 'elastic', - createdAt: testDate, - updatedAt: testDate, - apiKeyOwner: 'elastic', - muteAll: false, - mutedInstanceIds: [], - snoozeSchedule: [], - executionStatus: { - lastExecutionDate: testDate, - lastDuration: 46, - status: 'ok', - }, - scheduledTaskId: '0de7273e-c5db-4d5c-8e28-1aab363e1abc', - lastRun: { - outcomeMsg: null, - outcomeOrder: 0, - alertsCount: { active: 0, new: 0, recovered: 0, ignored: 0 }, - outcome: 'succeeded', - warning: null, - }, - nextRun: testDate, - apiKeyCreatedByUser: false, - id: '0de7273e-c5db-4d5c-8e28-1aab363e1abc', - enabled: true, - revision: 0, - running: false, - monitoring: { - run: { - history: [{ success: true, timestamp: 1712259266100, duration: 65 }], - calculated_metrics: { success_ratio: 1, p50: 45, p95: 64.65, p99: 968 }, - last_run: { - timestamp: '2024-04-04T20:39:01.655Z', - metrics: { - duration: 46, - total_search_duration_ms: null, - total_indexing_duration_ms: null, - total_alerts_detected: null, - total_alerts_created: null, - gap_duration_s: null, - }, - }, - }, - }, - ...shared, -}; - -const ShowRequestModalWithProviders: React.FunctionComponent = (props) => ( - - - -); - -describe('showRequestModal', () => { - afterEach(() => { - jest.clearAllMocks(); - cleanup(); - }); - - test('renders create request correctly', async () => { - const modalProps: ShowRequestModalProps = { - rule: { - ...rule, - } as RuleUpdates, - onClose: jest.fn(), - }; - const result = render(); - expect(result.getByTestId('modalHeaderTitle').textContent).toBe('Create alerting rule request'); - expect(result.getByTestId('modalSubtitle').textContent).toBe( - 'This Kibana request will create this rule.' - ); - expect(result.getByTestId('modalRequestCodeBlock').textContent).toMatchInlineSnapshot(` - "POST kbn:/api/alerting/rule - { - \\"params\\": { - \\"searchType\\": \\"esQuery\\", - \\"timeWindowSize\\": 5, - \\"timeWindowUnit\\": \\"m\\", - \\"threshold\\": [ - 1000 - ], - \\"thresholdComparator\\": \\">\\", - \\"size\\": 100, - \\"esQuery\\": \\"{\\\\n \\\\\\"query\\\\\\":{\\\\n \\\\\\"match_all\\\\\\" : {}\\\\n }\\\\n }\\", - \\"aggType\\": \\"count\\", - \\"groupBy\\": \\"all\\", - \\"termSize\\": 5, - \\"excludeHitsFromPreviousRun\\": false, - \\"sourceFields\\": [], - \\"index\\": [ - \\".kibana\\" - ], - \\"timeField\\": \\"created_at\\" - }, - \\"consumer\\": \\"stackAlerts\\", - \\"schedule\\": { - \\"interval\\": \\"1m\\" - }, - \\"tags\\": [ - \\"test\\" - ], - \\"name\\": \\"test\\", - \\"rule_type_id\\": \\".es-query\\", - \\"actions\\": [ - { - \\"group\\": \\"query matched\\", - \\"id\\": \\"0be65bf4-58b8-4c44-ba4d-5112c65103f5\\", - \\"params\\": { - \\"level\\": \\"info\\", - \\"message\\": \\"Elasticsearch query rule '{{rule.name}}' is active:\\\\n\\\\n- Value: {{context.value}}\\\\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\\\\n- Timestamp: {{context.date}}\\\\n- Link: {{context.link}}\\" - }, - \\"frequency\\": { - \\"notify_when\\": \\"onActionGroupChange\\", - \\"throttle\\": null, - \\"summary\\": false - } - } - ] - }" - `); - }); - - test('renders edit request correctly', async () => { - const modalProps: ShowRequestModalProps = { - edit: true, - ruleId: editRule.id, - rule: { - ...editRule, - } as RuleUpdates, - onClose: jest.fn(), - }; - const result = render(); - expect(result.getByTestId('modalHeaderTitle').textContent).toBe('Edit alerting rule request'); - expect(result.getByTestId('modalSubtitle').textContent).toBe( - 'This Kibana request will edit this rule.' - ); - expect(result.getByTestId('modalRequestCodeBlock').textContent).toMatchInlineSnapshot(` - "PUT kbn:/api/alerting/rule/0de7273e-c5db-4d5c-8e28-1aab363e1abc - { - \\"name\\": \\"test\\", - \\"tags\\": [ - \\"test\\" - ], - \\"schedule\\": { - \\"interval\\": \\"1m\\" - }, - \\"params\\": { - \\"searchType\\": \\"esQuery\\", - \\"timeWindowSize\\": 5, - \\"timeWindowUnit\\": \\"m\\", - \\"threshold\\": [ - 1000 - ], - \\"thresholdComparator\\": \\">\\", - \\"size\\": 100, - \\"esQuery\\": \\"{\\\\n \\\\\\"query\\\\\\":{\\\\n \\\\\\"match_all\\\\\\" : {}\\\\n }\\\\n }\\", - \\"aggType\\": \\"count\\", - \\"groupBy\\": \\"all\\", - \\"termSize\\": 5, - \\"excludeHitsFromPreviousRun\\": false, - \\"sourceFields\\": [], - \\"index\\": [ - \\".kibana\\" - ], - \\"timeField\\": \\"created_at\\" - }, - \\"actions\\": [ - { - \\"group\\": \\"query matched\\", - \\"id\\": \\"0be65bf4-58b8-4c44-ba4d-5112c65103f5\\", - \\"params\\": { - \\"level\\": \\"info\\", - \\"message\\": \\"Elasticsearch query rule '{{rule.name}}' is active:\\\\n\\\\n- Value: {{context.value}}\\\\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\\\\n- Timestamp: {{context.date}}\\\\n- Link: {{context.link}}\\" - }, - \\"frequency\\": { - \\"notify_when\\": \\"onActionGroupChange\\", - \\"throttle\\": null, - \\"summary\\": false - }, - \\"uuid\\": \\"a330a154-61fb-42a8-9bce-9dfd8513a12d\\" - } - ] - }" - `); - }); -}); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.tsx deleted file mode 100644 index fbe8bebc18687..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/show_request_modal.tsx +++ /dev/null @@ -1,85 +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 { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiText, - EuiTextColor, -} from '@elastic/eui'; -import { - UPDATE_FIELDS_WITH_ACTIONS, - transformCreateRuleBody as rewriteCreateBodyRequest, - transformUpdateRuleBody as rewriteUpdateBodyRequest, -} from '@kbn/response-ops-rule-form'; -import { pick } from 'lodash'; -import React from 'react'; -import { RuleUpdates } from '../../../types'; -import { BASE_ALERTING_API_PATH } from '../../constants'; -import * as i18n from '../translations'; - -const stringify = (rule: RuleUpdates, edit: boolean): string => { - try { - const request = edit - ? rewriteUpdateBodyRequest(pick(rule, UPDATE_FIELDS_WITH_ACTIONS)) - : rewriteCreateBodyRequest(rule); - return JSON.stringify(request, null, 2); - } catch { - return i18n.SHOW_REQUEST_MODAL_ERROR; - } -}; - -export interface ShowRequestModalProps { - onClose: () => void; - rule: RuleUpdates; - ruleId?: string; - edit?: boolean; -} - -export const ShowRequestModal: React.FC = ({ - onClose, - rule, - edit = false, - ruleId, -}) => { - const formattedRequest = stringify(rule, edit); - - return ( - - - - - - {i18n.SHOW_REQUEST_MODAL_TITLE(edit)} - - - - -

- - {i18n.SHOW_REQUEST_MODAL_SUBTITLE(edit)} - -

-
-
-
-
- - - {`${edit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${ - edit ? `/${ruleId}` : '' - }\n${formattedRequest}`} - - -
- ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx deleted file mode 100644 index 4cf703478fc84..0000000000000 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/solution_filter.tsx +++ /dev/null @@ -1,84 +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 React, { useEffect, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFilterGroup, - EuiPopover, - EuiFilterButton, - EuiFilterSelectItem, - useEuiTheme, -} from '@elastic/eui'; - -interface SolutionFilterProps { - solutions: Map; - onChange?: (selectedSolutions: string[]) => void; -} - -export const SolutionFilter: React.FunctionComponent = ({ - solutions, - onChange, -}: SolutionFilterProps) => { - const { euiTheme } = useEuiTheme(); - const [selectedValues, setSelectedValues] = useState([]); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - useEffect(() => { - if (onChange) { - onChange(selectedValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedValues]); - - return ( - - setIsPopoverOpen(false)} - button={ - 0} - numActiveFilters={selectedValues.length} - numFilters={selectedValues.length} - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="solutionsFilterButton" - > - - - } - > - {/* EUI NOTE: Please use EuiSelectable (which already has height/scrolling built in) - instead of EuiFilterSelectItem (which is pending deprecation). - @see https://elastic.github.io/eui/#/forms/filter-group#multi-select */} -
- {[...solutions.entries()].map(([id, title]) => ( - { - const isPreviouslyChecked = selectedValues.includes(id); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== id)); - } else { - setSelectedValues([...selectedValues, id]); - } - }} - checked={selectedValues.includes(id) ? 'on' : undefined} - data-test-subj={`solution${id}FilterOption`} - > - {title} - - ))} -
-
-
- ); -}; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 01f1ad83e95be..7d7ca64155f0e 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -14,16 +14,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; import { RuleTypeModal } from '@kbn/response-ops-rule-form'; -import React, { - lazy, - useEffect, - useState, - ReactNode, - useCallback, - useMemo, - useRef, - Suspense, -} from 'react'; +import React, { useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'; import { EuiSpacer, EuiPageTemplate, @@ -37,14 +28,12 @@ import { useHistory } from 'react-router-dom'; import { RuleExecutionStatus, - ALERTING_FEATURE_ID, RuleExecutionStatusErrorReasons, RuleLastRunOutcomeValues, } from '@kbn/alerting-plugin/common'; import { RuleCreationValidConsumer, ruleDetailsRoute as commonRuleDetailsRoute, - STACK_ALERTS_FEATURE_ID, getCreateRuleRoute, getEditRuleRoute, } from '@kbn/rule-data-utils'; @@ -111,11 +100,6 @@ import { RulesSettingsLink } from '../../../components/rules_setting/rules_setti import { useRulesListUiState as useUiState } from '../../../hooks/use_rules_list_ui_state'; import { useRulesListFilterStore } from './hooks/use_rules_list_filter_store'; -// Directly lazy import the flyouts because the suspendedComponentWithProps component -// cause a visual hitch due to the loading spinner -const RuleAdd = lazy(() => import('../../rule_form/rule_add')); -const RuleEdit = lazy(() => import('../../rule_form/rule_edit')); - export interface RulesListProps { ruleTypeIds?: string[]; consumers?: string[]; @@ -142,7 +126,6 @@ export interface RulesListProps { onRefresh?: (refresh: Date) => void; setHeaderActions?: (components?: React.ReactNode[]) => void; initialSelectedConsumer?: RuleCreationValidConsumer | null; - useNewRuleForm?: boolean; } export const percentileFields = { @@ -184,8 +167,6 @@ export const RulesList = ({ onTypeFilterChange, onRefresh, setHeaderActions, - initialSelectedConsumer = STACK_ALERTS_FEATURE_ID, - useNewRuleForm = false, }: RulesListProps) => { const history = useHistory(); const kibanaServices = useKibana().services; @@ -205,10 +186,6 @@ export const RulesList = ({ const [inputText, setInputText] = useState(searchFilter); const [ruleTypeModalVisible, setRuleTypeModalVisibility] = useState(false); - const [ruleTypeIdToCreate, setRuleTypeIdToCreate] = useState(undefined); - const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); - const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); @@ -216,8 +193,6 @@ export const RulesList = ({ const cloneRuleId = useRef(null); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); - // TODO: Remove this when removing the v1 flyout code - const isUsingRuleCreateFlyout = false; // getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); const [percentileOptions, setPercentileOptions] = useState(initialPercentileOptions); @@ -322,18 +297,13 @@ export const RulesList = ({ }); const onRuleEdit = (ruleItem: RuleTableItem) => { - if (!isUsingRuleCreateFlyout && useNewRuleForm) { - navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`, - state: { - returnApp: 'management', - returnPath: `insightsAndAlerting/triggersActions/rules`, - }, - }); - } else { - setEditFlyoutVisibility(true); - setCurrentRuleToEdit(ruleItem); - } + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`, + state: { + returnApp: 'management', + returnPath: `insightsAndAlerting/triggersActions/rules`, + }, + }); }; const onRunRule = async (id: string) => { @@ -1030,15 +1000,9 @@ export const RulesList = ({ setRuleTypeModalVisibility(false)} onSelectRuleType={(ruleTypeId) => { - if (!isUsingRuleCreateFlyout) { - navigateToApp('management', { - path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`, - }); - } else { - setRuleTypeIdToCreate(ruleTypeId); - setRuleTypeModalVisibility(false); - setRuleFlyoutVisibility(true); - } + navigateToApp('management', { + path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`, + }); }} http={http} toasts={toasts} @@ -1046,39 +1010,6 @@ export const RulesList = ({ filteredRuleTypes={filteredRuleTypes} /> )} - {ruleFlyoutVisible && ( - }> - { - setRuleFlyoutVisibility(false); - }} - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - ruleTypeIndex={ruleTypesState.data} - onSave={refreshRules} - initialSelectedConsumer={initialSelectedConsumer} - ruleTypeId={ruleTypeIdToCreate} - canChangeTrigger={false} - /> - - )} - {editFlyoutVisible && currentRuleToEdit && ( - }> - { - setEditFlyoutVisibility(false); - }} - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - ruleType={ - ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType - } - onSave={refreshRules} - /> - - )} ); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_experimental_features.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_experimental_features.test.tsx index b33622423b92d..5cf9e23142b2a 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_experimental_features.test.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/common/get_experimental_features.test.tsx @@ -24,7 +24,6 @@ describe('getIsExperimentalFeatureEnabled', () => { ruleKqlBar: true, isMustacheAutocompleteOn: false, showMustacheAutocompleteSwitch: false, - isUsingRuleCreateFlyout: false, }, }); @@ -64,10 +63,6 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(false); - result = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout'); - - expect(result).toEqual(false); - expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError( `Invalid enable value doesNotExist. Allowed values are: ${allowedExperimentalValueKeys.join( ', ' diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/types.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/types.ts index 0d7f3fea23477..fe1694bb1b6a0 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/types.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/types.ts @@ -398,7 +398,6 @@ export interface RuleDefinitionProps Promise; hideEditButton?: boolean; filteredRuleTypes?: string[]; - useNewRuleForm?: boolean; } export enum Percentiles { diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index ad1b01664873c..7ced9447e06bb 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -38,11 +38,6 @@ export function createTestConfig( serverArgs: [ ...svlSharedConfig.get('kbnTestServer.serverArgs'), `--serverless=${options.serverlessProject}`, - // Ensures the existing E2E tests are backwards compatible with the old rule create flyout - // Remove this experiment once all of the migration has been completed - `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ - 'isUsingRuleCreateFlyout', - ])}`, ...(options.kbnServerArgs ?? []), ], }, From 768889a1371d87f4dc696c412b69715bbd8dcae5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:47:47 +0000 Subject: [PATCH 62/62] [CI] Auto-commit changed files from 'node scripts/styled_components_mapping' --- x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json index 67a089ef0328a..69299beb9cb3b 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/tsconfig.json @@ -70,7 +70,6 @@ "@kbn/visualization-utils", "@kbn/core-ui-settings-browser", "@kbn/observability-alerting-rule-utils", - "@kbn/core-application-browser", "@kbn/cloud-plugin", "@kbn/response-ops-rule-form", "@kbn/core-user-profile-browser-mocks",