diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts index c669138934b48..4ad8c12b6c404 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/constants.ts @@ -6,16 +6,27 @@ * Side Public License, v 1. */ +import { + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + RuleCreationValidConsumer, + AlertConsumers, +} from '@kbn/rule-data-utils'; import { RuleFormData } from './types'; export const DEFAULT_RULE_INTERVAL = '1m'; +export const ALERTING_FEATURE_ID = 'alerts'; + export const GET_DEFAULT_FORM_DATA = ({ ruleTypeId, name, + consumer, }: { ruleTypeId: RuleFormData['ruleTypeId']; name: RuleFormData['name']; + consumer: RuleFormData['consumer']; }) => { return { tags: [], @@ -23,11 +34,24 @@ export const GET_DEFAULT_FORM_DATA = ({ schedule: { interval: DEFAULT_RULE_INTERVAL, }, - consumer: 'alerts', + consumer, ruleTypeId, name, }; }; +export const MULTI_CONSUMER_RULE_TYPE_IDS = [ + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ES_QUERY_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, +]; + +export const DEFAULT_VALID_CONSUMERS: RuleCreationValidConsumer[] = [ + AlertConsumers.LOGS, + AlertConsumers.INFRASTRUCTURE, + 'stackAlerts', + 'alerts', +]; + export const createRuleRoute = '/rule/create/:ruleTypeId' as const; export const editRuleRoute = '/rule/edit/:id' as const; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx index caf5d647e54e5..c4daa9def1c4f 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/create_rule_form.tsx @@ -7,46 +7,57 @@ */ import React, { useCallback } from 'react'; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; -// import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import type { RuleFormData, RuleFormPlugins } from './types'; -import { GET_DEFAULT_FORM_DATA } from './constants'; +import { ALERTING_FEATURE_ID, DEFAULT_VALID_CONSUMERS, GET_DEFAULT_FORM_DATA } from './constants'; import { RuleFormStateProvider } from './rule_form_state'; import { useCreateRule } from '../common/hooks'; - import { RulePage } from './rule_page'; -import { RuleFormHealthCheckError } from './rule_form_health_check_error'; -import { - RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE, - RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT, -} from './translations'; +import { RuleFormHealthCheckError, RuleFormRuleOrRuleTypeError } from './rule_form_errors'; import { useLoadDependencies } from './hooks/use_load_dependencies'; +import { getInitialMultiConsumer } from './utils'; +import { RULE_CREATE_SUCCESS_TEXT } from './translations'; export interface CreateRuleFormProps { ruleTypeId: string; plugins: RuleFormPlugins; - // formData?: RuleFormData; - // consumer: RuleCreationValidConsumer; - // canChangeTrigger?: boolean; - // hideGrouping?: boolean; - // filteredRuleTypes?: string[]; - // validConsumers?: RuleCreationValidConsumer[]; - // useRuleProducer?: boolean; - // initialSelectedConsumer?: RuleCreationValidConsumer | null; + consumer?: string; + multiConsumerSelection?: RuleCreationValidConsumer | null; + hideInterval?: boolean; + validConsumers?: RuleCreationValidConsumer[]; + filteredRuleTypes?: string[]; + useRuleProducer?: boolean; } export const CreateRuleForm = (props: CreateRuleFormProps) => { - // const { formData, plugins, ruleTypeModel, ruleType, validConsumers } = props; - const { ruleTypeId, plugins } = props; + const { + ruleTypeId, + plugins, + consumer = ALERTING_FEATURE_ID, + multiConsumerSelection, + validConsumers = DEFAULT_VALID_CONSUMERS, + filteredRuleTypes = [], + } = props; + const { http, docLinks, notification, ruleTypeRegistry } = plugins; + const { toasts } = notification; - const { mutate, isLoading: isSaving } = useCreateRule({ http }); + const { mutate, isLoading: isSaving } = useCreateRule({ + http, + onSuccess: ({ name }) => { + toasts.addSuccess(RULE_CREATE_SUCCESS_TEXT(name)); + }, + }); const { isLoading, ruleType, ruleTypeModel, uiConfig, healthCheckError } = useLoadDependencies({ http, toasts: notification.toasts, ruleTypeRegistry, ruleTypeId, + consumer, + validConsumers, + filteredRuleTypes, }); const onSave = useCallback( @@ -61,18 +72,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { } if (!ruleType || !ruleTypeModel) { - return ( - {RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE}} - body={ - -

{RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT}

-
- } - /> - ); + return ; } if (healthCheckError) { @@ -85,17 +85,22 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { formData: GET_DEFAULT_FORM_DATA({ ruleTypeId, name: `${ruleType.name} rule`, + consumer, }), plugins, minimumScheduleInterval: uiConfig?.minimumScheduleInterval, selectedRuleTypeModel: ruleTypeModel, + selectedRuleType: ruleType, + multiConsumerSelection: getInitialMultiConsumer({ + multiConsumerSelection, + validConsumers, + ruleType, + }), }} > diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx index b1d269c45d26c..853cdd99f8fd2 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/edit_rule_form.tsx @@ -7,18 +7,15 @@ */ import React, { useCallback } from 'react'; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import type { RuleFormData, RuleFormPlugins } from './types'; import { RuleFormStateProvider } from './rule_form_state'; import { useUpdateRule } from '../common/hooks'; - import { RulePage } from './rule_page'; -import { RuleFormHealthCheckError } from './rule_form_health_check_error'; -import { - RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE, - RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT, -} from './translations'; +import { RuleFormHealthCheckError } from './rule_form_errors/rule_form_health_check_error'; import { useLoadDependencies } from './hooks/use_load_dependencies'; +import { RuleFormRuleOrRuleTypeError } from './rule_form_errors'; +import { RULE_EDIT_SUCCESS_TEXT } from './translations'; export interface EditRuleFormProps { id: string; @@ -28,8 +25,14 @@ export interface EditRuleFormProps { export const EditRuleForm = (props: EditRuleFormProps) => { const { id, plugins } = props; const { http, notification, docLinks, ruleTypeRegistry } = plugins; + const { toasts } = notification; - const { mutate, isLoading: isSaving } = useUpdateRule({ http }); + const { mutate, isLoading: isSaving } = useUpdateRule({ + http, + onSuccess: ({ name }) => { + toasts.addSuccess(RULE_EDIT_SUCCESS_TEXT(name)); + }, + }); const { isLoading, ruleType, ruleTypeModel, uiConfig, healthCheckError, fetchedFormData } = useLoadDependencies({ @@ -54,18 +57,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { } if (!ruleType || !ruleTypeModel || !fetchedFormData) { - return ( - {RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE}} - body={ - -

{RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT}

-
- } - /> - ); + return ; } if (healthCheckError) { @@ -80,16 +72,11 @@ export const EditRuleForm = (props: EditRuleFormProps) => { id, plugins, minimumScheduleInterval: uiConfig?.minimumScheduleInterval, + selectedRuleType: ruleType, selectedRuleTypeModel: ruleTypeModel, }} > - + ); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts index 23eaebf2ed8f3..99105e66f3356 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/hooks/use_load_dependencies.ts @@ -8,6 +8,7 @@ import { HttpStart } from '@kbn/core-http-browser'; import type { ToastsStart } from '@kbn/core-notifications-browser'; +import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import { useMemo } from 'react'; import { useHealthCheck, @@ -16,17 +17,30 @@ import { useResolveRule, } from '../../common/hooks'; import { RuleTypeRegistryContract } from '../types'; +import { getAvailableRuleTypes } from '../utils'; export interface UseLoadDependencies { http: HttpStart; toasts: ToastsStart; ruleTypeRegistry: RuleTypeRegistryContract; + consumer?: string; id?: string; ruleTypeId?: string; + validConsumers?: RuleCreationValidConsumer[]; + filteredRuleTypes?: string[]; } export const useLoadDependencies = (props: UseLoadDependencies) => { - const { http, toasts, ruleTypeRegistry, id, ruleTypeId } = props; + const { + http, + toasts, + ruleTypeRegistry, + consumer, + validConsumers, + id, + ruleTypeId, + filteredRuleTypes = [], + } = props; const { data: uiConfig, isLoading: isLoadingUiConfig } = useLoadUiConfig({ http }); @@ -39,28 +53,36 @@ export const useLoadDependencies = (props: UseLoadDependencies) => { } = useLoadRuleTypesQuery({ http, toasts, + filteredRuleTypes, }); const computedRuleTypeId = useMemo(() => { return fetchedFormData?.ruleTypeId || ruleTypeId; }, [fetchedFormData, ruleTypeId]); - const ruleTypes = [...ruleTypeIndex.values()]; + const authorizedRuleTypeItems = useMemo(() => { + const computedConsumer = consumer || fetchedFormData?.consumer; + if (!computedConsumer) { + return []; + } + return getAvailableRuleTypes({ + consumer: computedConsumer, + ruleTypes: [...ruleTypeIndex.values()], + ruleTypeRegistry, + validConsumers, + }); + }, [consumer, ruleTypeIndex, ruleTypeRegistry, validConsumers, fetchedFormData]); - const ruleType = ruleTypes.find((rt) => rt.id === computedRuleTypeId); + const [ruleType, ruleTypeModel] = useMemo(() => { + const item = authorizedRuleTypeItems.find(({ ruleType: rt }) => { + return rt.id === computedRuleTypeId; + }); - const ruleTypeModel = useMemo(() => { - let model; - try { - model = ruleTypeRegistry.get(computedRuleTypeId!); - } catch (e) { - return null; - } - return model; - }, [ruleTypeRegistry, computedRuleTypeId]); + return [item?.ruleType, item?.ruleTypeModel]; + }, [authorizedRuleTypeItems, computedRuleTypeId]); const isLoading = useMemo(() => { - if (typeof id === 'undefined') { + if (id === undefined) { return isLoadingUiConfig || isLoadingHealthCheck || isLoadingRuleTypes; } return isLoadingUiConfig || isLoadingHealthCheck || isLoadingRule || isLoadingRuleTypes; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx index ae0da5e17864b..b267196d2a08b 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx @@ -15,6 +15,7 @@ import { CONSUMER_SELECT_COMBO_BOX_TITLE, } from '../translations'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; +import { getValidatedMultiConsumer } from '../utils'; export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ AlertConsumers.LOGS, @@ -24,13 +25,7 @@ export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ ]; export interface RuleConsumerSelectionProps { - consumers: RuleCreationValidConsumer[]; - /* 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; + validConsumers: RuleCreationValidConsumer[]; } const SINGLE_SELECTION = { asPlainText: true }; @@ -38,26 +33,20 @@ const SINGLE_SELECTION = { asPlainText: true }; type ComboBoxOption = EuiComboBoxOptionOption; export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { - const { consumers } = props; + const { validConsumers } = props; - const { formData, errors = {} } = useRuleFormState(); - - const { consumer: selectedConsumer } = formData; + const { multiConsumerSelection, errors = {} } = useRuleFormState(); const dispatch = useRuleFormDispatch(); const isInvalid = (errors.consumer?.length || 0) > 0; const validatedSelectedConsumer = useMemo(() => { - if ( - selectedConsumer && - consumers.includes(selectedConsumer as RuleCreationValidConsumer) && - FEATURE_NAME_MAP[selectedConsumer] - ) { - return selectedConsumer; - } - return null; - }, [selectedConsumer, consumers]); + return getValidatedMultiConsumer({ + multiConsumerSelection, + validConsumers, + }); + }, [multiConsumerSelection, validConsumers]); const selectedOptions = useMemo(() => { if (validatedSelectedConsumer) { @@ -72,7 +61,7 @@ export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { }, [validatedSelectedConsumer]); const formattedSelectOptions = useMemo(() => { - return consumers + return validConsumers .reduce((result, consumer) => { if (FEATURE_NAME_MAP[consumer]) { result.push({ @@ -84,19 +73,19 @@ export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { return result; }, []) .sort((a, b) => a.value!.localeCompare(b.value!)); - }, [consumers]); + }, [validConsumers]); const onConsumerChange = useCallback( (selected: ComboBoxOption[]) => { if (selected.length > 0) { const newSelectedConsumer = selected[0]; dispatch({ - type: 'setConsumer', + type: 'setMultiConsumer', payload: newSelectedConsumer.value!, }); } else { dispatch({ - type: 'setConsumer', + type: 'setMultiConsumer', payload: 'alerts', }); } @@ -104,7 +93,7 @@ export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { [dispatch] ); - if (consumers.length <= 1 || consumers.includes(AlertConsumers.OBSERVABILITY)) { + if (validConsumers.length <= 1 || validConsumers.includes(AlertConsumers.OBSERVABILITY)) { return null; } diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx index 611d7189a3da2..1f65ec0caf27c 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -22,13 +22,7 @@ import { EuiSpacer, EuiErrorBoundary, } from '@elastic/eui'; -import { - RuleCreationValidConsumer, - ES_QUERY_ID, - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - ML_ANOMALY_DETECTION_RULE_TYPE_ID, -} from '@kbn/rule-data-utils'; -import type { RuleTypeModel } from '../types'; +import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import { DOC_LINK_TITLE, LOADING_RULE_TYPE_PARAMS_TITLE, @@ -44,41 +38,40 @@ import { import { RuleAlertDelay } from './rule_alert_delay'; import { RuleConsumerSelection } from './rule_consumer_selection'; import { RuleSchedule } from './rule_schedule'; -import { RuleTypeWithDescription } from '../../common/types'; import { useRuleFormState, useRuleFormDispatch } from '../hooks'; - -const MULTI_CONSUMER_RULE_TYPE_IDS = [ - OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, - ES_QUERY_ID, - ML_ANOMALY_DETECTION_RULE_TYPE_ID, -]; +import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { getAuthorizedConsumers } from '../utils'; interface RuleDefinitionProps { canShowConsumerSelection?: boolean; - authorizedConsumers?: RuleCreationValidConsumer[]; - selectedRuleTypeModel: RuleTypeModel; - selectedRuleType: RuleTypeWithDescription; validConsumers?: RuleCreationValidConsumer[]; } export const RuleDefinition = (props: RuleDefinitionProps) => { - const { - canShowConsumerSelection = false, - authorizedConsumers = [], - selectedRuleTypeModel, - selectedRuleType, - } = props; + const { canShowConsumerSelection = false, validConsumers } = props; - const { formData, plugins, errors, metadata, id } = useRuleFormState(); + const { formData, plugins, errors, metadata, id, selectedRuleType, selectedRuleTypeModel } = + useRuleFormState(); const dispatch = useRuleFormDispatch(); const { charts, data, dataViews, unifiedSearch, docLinks } = plugins!; - const { params, schedule, alertDelay, notifyWhen } = formData; + const { params, schedule, alertDelay, notifyWhen, consumer } = formData; const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(!!alertDelay); + const authorizedConsumers = useMemo(() => { + if (!validConsumers?.length) { + return []; + } + return getAuthorizedConsumers({ + consumer, + ruleType: selectedRuleType, + validConsumers, + }); + }, [consumer, selectedRuleType, validConsumers]); + const shouldShowConsumerSelect = useMemo(() => { if (!canShowConsumerSelection) { return false; @@ -236,7 +229,7 @@ export const RuleDefinition = (props: RuleDefinitionProps) => { } > - + )} diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/index.ts new file mode 100644 index 0000000000000..fe83e761116b5 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './rule_form_health_check_error'; +export * from './rule_form_rule_or_rule_type_error'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_health_check_error.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_health_check_error.tsx similarity index 96% rename from packages/kbn-alerts-ui-shared/src/rule_form/rule_form_health_check_error.tsx rename to packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_health_check_error.tsx index f82c032d61ca5..257112a0e3f87 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_health_check_error.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_health_check_error.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; -import { healthCheckErrors, HealthCheckErrors } from '../common/types'; +import { healthCheckErrors, HealthCheckErrors } from '../../common/types'; import { HEALTH_CHECK_ALERTS_ERROR_TITLE, @@ -21,7 +21,7 @@ import { HEALTH_CHECK_API_KEY_DISABLED_ERROR_TITLE, HEALTH_CHECK_API_KEY_DISABLED_ERROR_TEXT, HEALTH_CHECK_ACTION_TEXT, -} from './translations'; +} from '../translations'; export interface RuleFormHealthCheckErrorProps { error: HealthCheckErrors; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_rule_or_rule_type_error.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_rule_or_rule_type_error.tsx new file mode 100644 index 0000000000000..8dc41b16c1a7d --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_errors/rule_form_rule_or_rule_type_error.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { + RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT, + RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE, +} from '../translations'; + +export const RuleFormRuleOrRuleTypeError = () => { + return ( + {RULE_FORM_RULE_NOT_FOUND_ERROR_TITLE}} + body={ + +

{RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT}

+
+ } + /> + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts index 5b838c63166d4..b5ed4e87d871a 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_form_state/rule_form_state_reducer.ts @@ -56,6 +56,10 @@ export type RuleFormStateReducerAction = type: 'setConsumer'; payload: RuleFormData['consumer']; } + | { + type: 'setMultiConsumer'; + payload: RuleFormState['multiConsumerSelection']; + } | { type: 'setMetadata'; payload: Record; @@ -64,15 +68,28 @@ export type RuleFormStateReducerAction = const getUpdateWithValidation = (ruleFormState: RuleFormState) => (updater: () => RuleFormData): RuleFormState => { - const { minimumScheduleInterval, selectedRuleTypeModel } = ruleFormState; + const { minimumScheduleInterval, selectedRuleTypeModel, multiConsumerSelection } = + ruleFormState; + const formData = updater(); + const formDataWithMultiConsumer = { + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }; + return { ...ruleFormState, formData, errors: { - ...validateRuleBase({ formData, minimumScheduleInterval }), - ...validateRuleParams({ formData, ruleTypeModel: selectedRuleTypeModel }), + ...validateRuleBase({ + formData: formDataWithMultiConsumer, + minimumScheduleInterval, + }), + ...validateRuleParams({ + formData: formDataWithMultiConsumer, + ruleTypeModel: selectedRuleTypeModel, + }), }, }; }; @@ -159,6 +176,13 @@ export const ruleFormStateReducer = ( consumer: payload, })); } + case 'setMultiConsumer': { + const { payload } = action; + return { + ...ruleFormState, + multiConsumerSelection: payload, + }; + } case 'setMetadata': { const { payload } = action; return { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx index da65291042d1d..85e679329e8ba 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page.tsx @@ -25,17 +25,12 @@ import { RuleDetails, RulePageNameInput, RulePageFooter, - RuleTypeModel, RuleFormData, } from '..'; -import { RuleTypeWithDescription } from '../../common/types'; import { useRuleFormState } from '../hooks'; export interface RulePageProps { canShowConsumerSelection?: boolean; - authorizedConsumers?: RuleCreationValidConsumer[]; - selectedRuleTypeModel: RuleTypeModel; - selectedRuleType: RuleTypeWithDescription; validConsumers?: RuleCreationValidConsumer[]; isEdit?: boolean; isSaving?: boolean; @@ -45,9 +40,6 @@ export interface RulePageProps { export const RulePage = (props: RulePageProps) => { const { canShowConsumerSelection = false, - authorizedConsumers, - selectedRuleTypeModel, - selectedRuleType, validConsumers, isEdit = false, isSaving = false, @@ -56,6 +48,8 @@ export const RulePage = (props: RulePageProps) => { const { plugins: { application }, + formData, + multiConsumerSelection, } = useRuleFormState(); const styles = useEuiBackgroundColorCSS().transparent; @@ -66,6 +60,13 @@ export const RulePage = (props: RulePageProps) => { }); }, [application]); + const onSaveInternal = useCallback(() => { + onSave({ + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }); + }, [onSave, formData, multiConsumerSelection]); + const steps: EuiStepsProps['steps'] = useMemo(() => { return [ { @@ -73,9 +74,6 @@ export const RulePage = (props: RulePageProps) => { children: ( ), @@ -101,13 +99,7 @@ export const RulePage = (props: RulePageProps) => { ), }, ]; - }, [ - canShowConsumerSelection, - authorizedConsumers, - selectedRuleTypeModel, - selectedRuleType, - validConsumers, - ]); + }, [canShowConsumerSelection, validConsumers]); return ( @@ -134,7 +126,12 @@ export const RulePage = (props: RulePageProps) => { - + ); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx index 0b37b358310fa..6cca324f5fd4f 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_footer.tsx @@ -15,7 +15,6 @@ import { RULE_PAGE_FOOTER_SAVE_TEXT, } from '../translations'; import { useRuleFormState } from '../hooks'; -import { RuleFormData } from '../types'; import { isValidRule } from '../validation'; import { RulePageShowRequestModal } from './rule_page_show_request_modal'; @@ -23,7 +22,7 @@ export interface RulePageFooterProps { isEdit?: boolean; isSaving?: boolean; onCancel: () => void; - onSave: (formData: RuleFormData) => void; + onSave: () => void; } export const RulePageFooter = (props: RulePageFooterProps) => { @@ -33,10 +32,6 @@ export const RulePageFooter = (props: RulePageFooterProps) => { const { formData, errors } = useRuleFormState(); - const onSaveInternal = useCallback(() => { - onSave(formData); - }, [onSave, formData]); - const hasErrors = useMemo(() => { return !!(errors && !isValidRule(formData, errors)); }, [formData, errors]); @@ -78,7 +73,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => { diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_show_request_modal.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_show_request_modal.tsx index 4da9618f91d4d..074ac84e3fea7 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_show_request_modal.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_page/rule_page_show_request_modal.tsx @@ -54,11 +54,17 @@ export interface RulePageShowRequestModalProps { export const RulePageShowRequestModal = (props: RulePageShowRequestModalProps) => { const { onClose, isEdit = false } = props; - const { formData, id } = useRuleFormState(); + const { formData, id, multiConsumerSelection } = useRuleFormState(); const formattedRequest = useMemo(() => { - return stringifyBodyRequest({ formData, isEdit }); - }, [formData, isEdit]); + return stringifyBodyRequest({ + formData: { + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }, + isEdit, + }); + }, [formData, isEdit, multiConsumerSelection]); return ( diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index cd41291ed4242..5c1cac5682272 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -361,3 +361,19 @@ export const RULE_FORM_RULE_NOT_FOUND_ERROR_TEXT = i18n.translate( 'There was an error loading the rule or rule type. Please ensure you have access to the rule or rule type selected.', } ); + +export const RULE_CREATE_SUCCESS_TEXT = (ruleName: string) => + i18n.translate('alertsUIShared.ruleForm.createSuccessText', { + defaultMessage: 'Created rule "{ruleName}"', + values: { + ruleName, + }, + }); + +export const RULE_EDIT_SUCCESS_TEXT = (ruleName: string) => + i18n.translate('alertsUIShared.ruleForm.editSuccessText', { + defaultMessage: 'Updated "{ruleName}"', + values: { + ruleName, + }, + }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts index dd3bc4774714c..f3f0a76990b0c 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/types.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types.ts @@ -23,8 +23,9 @@ import type { RuleAction, RuleSystemAction, } from '@kbn/alerting-types'; +import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; import { TypeRegistry } from '../common/type_registry'; -// import type { RuleTypeWithDescription } from '../common/types'; +import { RuleTypeWithDescription } from '../common/types'; export type RuleTypeParams = Record; @@ -102,6 +103,11 @@ export interface RuleTypeModel { | React.LazyExoticComponent>; } +export type RuleTypeItems = Array<{ + ruleTypeModel: RuleTypeModel; + ruleType: RuleTypeWithDescription; +}>; + export type RuleTypeRegistryContract = PublicMethodsOf>; export interface RuleFormData { @@ -132,7 +138,9 @@ export interface RuleFormState { formData: RuleFormData; plugins: RuleFormPlugins; errors?: RuleFormErrors; + selectedRuleType: RuleTypeWithDescription; selectedRuleTypeModel: RuleTypeModel; + multiConsumerSelection?: RuleCreationValidConsumer | null; metadata?: Record; minimumScheduleInterval?: MinimumScheduleInterval; canShowConsumerSelection?: boolean; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts new file mode 100644 index 0000000000000..5adcc9a64923e --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_consumers.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { RuleTypeWithDescription } from '../../common/types'; +import { ALERTING_FEATURE_ID } from '../constants'; + +export const getAuthorizedConsumers = ({ + consumer, + ruleType, + validConsumers, +}: { + consumer: string; + ruleType: RuleTypeWithDescription; + validConsumers: RuleCreationValidConsumer[]; +}) => { + // 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 (consumer !== ALERTING_FEATURE_ID) { + return []; + } + + if (!ruleType.authorizedConsumers) { + return []; + } + return Object.entries(ruleType.authorizedConsumers).reduce( + (result, [authorizedConsumer, privilege]) => { + if ( + privilege.all && + validConsumers.includes(authorizedConsumer as RuleCreationValidConsumer) + ) { + result.push(authorizedConsumer as RuleCreationValidConsumer); + } + return result; + }, + [] + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_rule_types.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_rule_types.ts new file mode 100644 index 0000000000000..9d653cb280fb7 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_authorized_rule_types.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { RuleTypeWithDescription } from '../../common/types'; +import { ALERTING_FEATURE_ID, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { RuleTypeItems, RuleTypeModel, RuleTypeRegistryContract } from '../types'; + +const hasAllPrivilege = (consumer: string, ruleType: RuleTypeWithDescription): boolean => { + return ruleType.authorizedConsumers[consumer]?.all ?? false; +}; + +const authorizedToDisplayRuleType = ({ + consumer, + ruleType, + validConsumers, +}: { + consumer: string; + ruleType: RuleTypeWithDescription; + 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((c) => hasAllPrivilege(c, 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(consumer, ruleType); +}; + +export const getAvailableRuleTypes = ({ + consumer, + ruleTypes, + ruleTypeRegistry, + validConsumers, +}: { + consumer: string; + ruleTypes: RuleTypeWithDescription[]; + ruleTypeRegistry: RuleTypeRegistryContract; + validConsumers?: RuleCreationValidConsumer[]; +}): RuleTypeItems => { + return ruleTypeRegistry + .list() + .reduce((arr: RuleTypeItems, ruleTypeRegistryItem: RuleTypeModel) => { + const ruleType = ruleTypes.find((item) => ruleTypeRegistryItem.id === item.id); + if (ruleType) { + arr.push({ + ruleType, + ruleTypeModel: ruleTypeRegistryItem, + }); + } + return arr; + }, []) + .filter(({ ruleType }) => + authorizedToDisplayRuleType({ + consumer, + ruleType, + validConsumers, + }) + ) + .filter((item) => + consumer === ALERTING_FEATURE_ID + ? !item.ruleTypeModel.requiresAppContext + : item.ruleType!.producer === consumer + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_initial_multi_consumer.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_initial_multi_consumer.ts new file mode 100644 index 0000000000000..029c0de434a78 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_initial_multi_consumer.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AlertConsumers, RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { RuleTypeWithDescription } from '../../common/types'; +import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { FEATURE_NAME_MAP } from '../translations'; + +export const getValidatedMultiConsumer = ({ + multiConsumerSelection, + validConsumers, +}: { + multiConsumerSelection?: RuleCreationValidConsumer | null; + validConsumers: RuleCreationValidConsumer[]; +}) => { + if ( + multiConsumerSelection && + validConsumers.includes(multiConsumerSelection) && + FEATURE_NAME_MAP[multiConsumerSelection] + ) { + return multiConsumerSelection; + } + return null; +}; + +export const getInitialMultiConsumer = ({ + multiConsumerSelection, + validConsumers, + ruleType, +}: { + multiConsumerSelection?: RuleCreationValidConsumer | null; + validConsumers: RuleCreationValidConsumer[]; + ruleType: RuleTypeWithDescription; +}): RuleCreationValidConsumer | null => { + // If rule type doesn't support multi-consumer or no valid consumers exists, + // return nothing + if (!MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id) || validConsumers.length === 0) { + return null; + } + + // Use the only value in valid consumers + if (validConsumers.length === 1) { + return validConsumers[0]; + } + + // If o11y is in the valid consumers, just use that + if (validConsumers.includes(AlertConsumers.OBSERVABILITY)) { + return AlertConsumers.OBSERVABILITY; + } + + // User passed in null explicitly, won't set initial consumer + if (multiConsumerSelection === null) { + return null; + } + + const validatedConsumer = getValidatedMultiConsumer({ + multiConsumerSelection, + validConsumers, + }); + + // If validated consumer exists and no o11y in valid consumers, just use that + if (validatedConsumer) { + return validatedConsumer; + } + + // If validated consumer doesn't exist and stack alerts does, use that + if (validConsumers.includes(AlertConsumers.STACK_ALERTS)) { + return AlertConsumers.STACK_ALERTS; + } + + // All else fails, just use the first valid consumer + return validConsumers[0]; +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts index b25e2f561a86a..3e2736ef17e23 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts @@ -9,3 +9,6 @@ export * from './get_errors'; export * from './get_time_options'; export * from './parse_duration'; +export * from './get_authorized_rule_types'; +export * from './get_authorized_consumers'; +export * from './get_initial_multi_consumer'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index cd1ca3004d5f2..6319d75c5eaf7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -200,8 +200,6 @@ export const RuleForm = ({ } = useKibana().services; const canShowActions = hasShowActionsCapability(capabilities); - console.info('errors', errors); - const [ruleTypeModel, setRuleTypeModel] = useState(null); const flyoutBodyOverflowRef = useRef(null);