diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index bb1cb0d97689bf..bf90faa2fcf529 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -5,25 +5,33 @@ */ import uuid from 'uuid'; -import { range } from 'lodash'; +import { range, random, pick } from 'lodash'; import { AlertType } from '../../../../plugins/alerts/server'; import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +const ACTION_GROUPS = [ + { id: 'small', name: 'small', tshirtSize: 1 }, + { id: 'medium', name: 'medium', tshirtSize: 2 }, + { id: 'large', name: 'large', tshirtSize: 3 }, +]; + export const alertType: AlertType = { id: 'example.always-firing', name: 'Always firing', - actionGroups: [{ id: 'default', name: 'default' }], - defaultActionGroupId: 'default', + actionGroups: ACTION_GROUPS.map((actionGroup) => pick(actionGroup, ['id', 'name'])), + defaultActionGroupId: 'small', async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { const count = (state.count ?? 0) + 1; range(instances) - .map(() => ({ id: uuid.v4() })) - .forEach((instance: { id: string }) => { + .map(() => ({ id: uuid.v4(), tshirtSize: random(1, 3) })) + .forEach((instance: { id: string; tshirtSize: number }) => { services .alertInstanceFactory(instance.id) .replaceState({ triggerdOnCycle: count }) - .scheduleActions('default'); + .scheduleActions( + ACTION_GROUPS.find((actionGroup) => actionGroup.tshirtSize === instance.tshirtSize)!.id + ); }); return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts b/x-pack/plugins/security_solution/common/detection_engine/signals/siem_rule_action_groups.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/siem_rule_action_groups.ts rename to x-pack/plugins/security_solution/common/detection_engine/signals/siem_rule_action_groups.ts diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts index bee2e54d0e3eab..b5945e3138c96d 100644 --- a/x-pack/plugins/security_solution/common/shared_exports.ts +++ b/x-pack/plugins/security_solution/common/shared_exports.ts @@ -7,6 +7,7 @@ export { NonEmptyString } from './detection_engine/schemas/types/non_empty_string'; export { DefaultArray } from './detection_engine/schemas/types/default_array'; export { DefaultUuid } from './detection_engine/schemas/types/default_uuid'; +export { siemRuleActionGroups } from './detection_engine/signals/siem_rule_action_groups'; export { DefaultStringArray } from './detection_engine/schemas/types/default_string_array'; export { DefaultVersionNumber, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 4ff1b4e4f20f35..42b5b9402047cd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -21,10 +21,11 @@ import { import { AlertAction } from '../../../../../../alerts/common'; import { useKibana } from '../../../../common/lib/kibana'; import { FORM_ERRORS_TITLE } from './translations'; +import { siemRuleActionGroups } from '../../../../../common'; type ThrottleSelectField = typeof SelectField; -const DEFAULT_ACTION_GROUP_ID = 'default'; +const DEFAULT_ACTION_GROUP = siemRuleActionGroups[0]; const DEFAULT_ACTION_MESSAGE = 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts'; @@ -52,12 +53,19 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables [field.value] ); + const setActionPropByIndex = (prop: 'id' | 'group', value: string, index: number) => { + const updatedActions = [...(actions as Array>)]; + updatedActions[index] = deepMerge(updatedActions[index], { [prop]: value }); + field.setValue(updatedActions); + }; const setActionIdByIndex = useCallback( - (id: string, index: number) => { - const updatedActions = [...(actions as Array>)]; - updatedActions[index] = deepMerge(updatedActions[index], { id }); - field.setValue(updatedActions); - }, + (id: string, index: number) => setActionPropByIndex('id', id, index), + // eslint-disable-next-line react-hooks/exhaustive-deps + [field.setValue, actions] + ); + + const setActionGroupIdByIndex = useCallback( + (group: string, index: number) => setActionPropByIndex('group', group, index), // eslint-disable-next-line react-hooks/exhaustive-deps [field.setValue, actions] ); @@ -118,7 +126,9 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables docLinks={docLinks} capabilities={capabilities} messageVariables={messageVariables} - defaultActionGroupId={DEFAULT_ACTION_GROUP_ID} + defaultActionGroupId={DEFAULT_ACTION_GROUP.id} + actionGroups={siemRuleActionGroups} + setActionGroupIdByIndex={setActionGroupIdByIndex} setActionIdByIndex={setActionIdByIndex} setAlertProperty={setAlertProperty} setActionParamsProperty={setActionParamsProperty} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 802b9472a44870..7f2c0369ee3f73 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -7,11 +7,11 @@ import { Logger } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../../common/constants'; +import { siemRuleActionGroups } from '../../../../common'; import { NotificationAlertTypeDefinition } from './types'; import { getSignalsCount } from './get_signals_count'; import { RuleAlertAttributes } from '../signals/types'; -import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index bb3a0b4fa6f08c..7e66a17f7f2aa7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -43,7 +43,7 @@ import { createSearchAfterReturnTypeFromResponse, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; -import { siemRuleActionGroups } from './siem_rule_action_groups'; +import { siemRuleActionGroups } from '../../../../common'; import { findMlSignals } from './find_ml_signals'; import { findThresholdSignals } from './find_threshold_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7c718e8248e41c..83ed2c6ed8cac9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -112,8 +112,6 @@ describe('action_form', () => { }; describe('action_form in alert', () => { - let wrapper: ReactWrapper; - async function setup(customActions?: AlertAction[]) { const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); loadAllActions.mockResolvedValueOnce([ @@ -217,7 +215,7 @@ describe('action_form', () => { mutedInstanceIds: [], } as unknown) as Alert; - wrapper = mountWithIntl( + const wrapper = mountWithIntl( { setActionIdByIndex={(id: string, index: number) => { initialAlert.actions[index].id = id; }} + actionGroups={[{ id: 'default', name: 'Default' }]} + setActionGroupIdByIndex={(group: string, index: number) => { + initialAlert.actions[index].group = group; + }} setAlertProperty={(_updatedActions: AlertAction[]) => {}} setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) @@ -297,13 +299,16 @@ describe('action_form', () => { await nextTick(); wrapper.update(); }); + + return wrapper; } it('renders available action cards', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); + wrapper.debug(); expect(actionOption.exists()).toBeTruthy(); expect( wrapper @@ -314,7 +319,7 @@ describe('action_form', () => { }); it('does not render action types disabled by config', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find( '[data-test-subj="disabled-by-config-ActionTypeSelectOption"]' ); @@ -322,52 +327,72 @@ describe('action_form', () => { }); it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); expect(actionOption.exists()).toBeTruthy(); }); + it('renders available action groups for the selected action type', async () => { + const wrapper = await setup(); + const actionOption = wrapper.find( + `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` + ); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-0"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-0-option-default", + "inputDisplay": "Default", + "value": "default", + }, + ] + `); + }); + it('renders available connectors for the selected action type', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); actionOption.first().simulate('click'); const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` - Array [ - Object { - "id": "test", - "key": "test", - "label": "Test connector ", - }, - Object { - "id": "test2", - "key": "test2", - "label": "Test connector 2 (preconfigured)", - }, - ] - `); + Array [ + Object { + "id": "test", + "key": "test", + "label": "Test connector ", + }, + Object { + "id": "test2", + "key": "test2", + "label": "Test connector 2 (preconfigured)", + }, + ] + `); }); it('renders only preconfigured connectors for the selected preconfigured action type', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); actionOption.first().simulate('click'); const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]'); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` - Array [ - Object { - "id": "test3", - "key": "test3", - "label": "Preconfigured Only (preconfigured)", - }, - ] - `); + Array [ + Object { + "id": "test3", + "key": "test3", + "label": "Preconfigured Only (preconfigured)", + }, + ] + `); }); it('does not render "Add connector" button for preconfigured only action type', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); actionOption.first().simulate('click'); const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]'); @@ -378,7 +403,7 @@ describe('action_form', () => { }); it('renders action types disabled by license', async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find( '[data-test-subj="disabled-by-license-ActionTypeSelectOption"]' ); @@ -391,7 +416,7 @@ describe('action_form', () => { }); it(`shouldn't render action types without params component`, async () => { - await setup(); + const wrapper = await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionTypeWithoutParams.id}-ActionTypeSelectOption"]` ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index d8dd2ac06ef36e..0ea1b710a08269 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -102,7 +102,7 @@ export const ActionForm = ({ const [activeActionItem, setActiveActionItem] = useState( undefined ); - const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState(actions.length === 0); + const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState(true); const [connectors, setConnectors] = useState([]); const [isLoadingConnectors, setIsLoadingConnectors] = useState(false); const [isLoadingActionTypes, setIsLoadingActionTypes] = useState(false); @@ -341,7 +341,7 @@ export const ActionForm = ({ singleSelection={{ asPlainText: true }} options={optionsList} id={`selectActionConnector-${actionItem.id}`} - data-test-subj={`selectActionConnector-${index}`} + data-test-subj={`selectActionConnector-${actionItem.actionTypeId}`} selectedOptions={getSelectedOptions(actionItem.id)} onChange={(selectedOptions) => { setActionIdByIndex(selectedOptions[0].id ?? '', index);