[Alerting] adds an Run When field in the alert flyout to assign the action to an Action Group (#82472)

Adds a `RunsWhen` field to actions in the Alerts Flyout when creating / editing an Alert which allows the user to assign specific actions to a certain Action Groups
This commit is contained in:
Gidi Meir Morris 2020-11-09 12:56:56 +00:00 committed by GitHub
parent 858befef44
commit 3c525d7341
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 846 additions and 554 deletions

View file

@ -127,9 +127,9 @@ export const PeopleinSpaceExpression: React.FunctionComponent<PeopleinSpaceParam
});
const errorsCallout = flatten(
Object.entries(errors).map(([field, errs]: [string, string[]]) =>
errs.map((e) => (
<p>
Object.entries(errors).map(([field, errs]: [string, string[]], fieldIndex) =>
errs.map((e, index) => (
<p key={`astros-error-${fieldIndex}-${index}`}>
<EuiTextColor color="accent">{field}:</EuiTextColor>`: ${errs}`
</p>
))

View file

@ -5,25 +5,31 @@
*/
import uuid from 'uuid';
import { range } from 'lodash';
import { range, random } 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' },
{ id: 'medium', name: 'medium' },
{ id: 'large', name: 'large' },
];
export const alertType: AlertType = {
id: 'example.always-firing',
name: 'Always firing',
actionGroups: [{ id: 'default', name: 'default' }],
defaultActionGroupId: 'default',
actionGroups: ACTION_GROUPS,
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: ACTION_GROUPS[random(0, 2)].id! }))
.forEach((instance: { id: string; tshirtSize: string }) => {
services
.alertInstanceFactory(instance.id)
.replaceState({ triggerdOnCycle: count })
.scheduleActions('default');
.scheduleActions(instance.tshirtSize);
});
return {

View file

@ -1319,19 +1319,19 @@ ActionForm Props definition:
interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionTypeRegistry: ActionTypeRegistryContract;
toastNotifications: ToastsSetup;
docLinks: DocLinksStart;
actionTypes?: ActionType[];
messageVariables?: ActionVariable[];
defaultActionMessage?: string;
consumer: string;
capabilities: ApplicationStart['capabilities'];
}
```
@ -1339,17 +1339,20 @@ interface ActionAccordionFormProps {
|Property|Description|
|---|---|
|actions|List of actions comes from alert.actions property.|
|defaultActionGroupId|Default action group id to which each new action will belong to.|
|defaultActionGroupId|Default action group id to which each new action will belong by default.|
|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified|
|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.|
|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.|
|setAlertProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.|
|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.|
|http|HttpSetup needed for executing API calls.|
|actionTypeRegistry|Registry for action types.|
|toastNotifications|Toast messages.|
|toastNotifications|Toast messages Plugin Setup Contract.|
|docLinks|Documentation links Plugin Start Contract.|
|actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.|
|actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.|
|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.|
|consumer|Name of the plugin that creates an action.|
|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
AlertsContextProvider value options:

View file

@ -3,9 +3,15 @@
}
.actAccordionActionForm {
.euiCard {
box-shadow: none;
}
background-color: $euiColorLightestShade;
}
.actAccordionActionForm .euiCard {
box-shadow: none;
}
.actAccordionActionForm__button {
padding: $euiSizeM;
}
.actConnectorsListGrid {

View file

@ -6,7 +6,6 @@
import React, { Fragment, lazy } from 'react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
@ -112,8 +111,6 @@ describe('action_form', () => {
};
describe('action_form in alert', () => {
let wrapper: ReactWrapper<any>;
async function setup(customActions?: AlertAction[]) {
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce([
@ -217,7 +214,7 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown) as Alert;
wrapper = mountWithIntl(
const wrapper = mountWithIntl(
<ActionForm
actions={initialAlert.actions}
messageVariables={[
@ -228,6 +225,10 @@ describe('action_form', () => {
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,10 +298,12 @@ 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"]`
);
@ -314,7 +317,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 +325,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 +401,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 +414,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"]`
);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, Suspense, useState, useEffect } from 'react';
import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -14,25 +14,13 @@ import {
EuiIcon,
EuiTitle,
EuiSpacer,
EuiFormRow,
EuiComboBox,
EuiKeyPadMenuItem,
EuiAccordion,
EuiButtonIcon,
EuiEmptyPrompt,
EuiButtonEmpty,
EuiToolTip,
EuiIconTip,
EuiLink,
EuiCallOut,
EuiHorizontalRule,
EuiText,
EuiLoadingSpinner,
} from '@elastic/eui';
import { HttpSetup, ToastsSetup, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
IErrorObject,
ActionTypeModel,
ActionTypeRegistryContract,
AlertAction,
@ -43,15 +31,19 @@ import {
} from '../../../types';
import { SectionLoading } from '../../components/section_loading';
import { ConnectorAddModal } from './connector_add_modal';
import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionGroup } from '../../../../../alerts/common';
interface ActionAccordionFormProps {
export interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
@ -74,7 +66,9 @@ interface ActiveActionConnectorState {
export const ActionForm = ({
actions,
defaultActionGroupId,
actionGroups,
setActionIdByIndex,
setActionGroupIdByIndex,
setAlertProperty,
setActionParamsProperty,
http,
@ -88,8 +82,6 @@ export const ActionForm = ({
capabilities,
docLinks,
}: ActionAccordionFormProps) => {
const canSave = hasSaveActionsCapability(capabilities);
const [addModalVisible, setAddModalVisibility] = useState<boolean>(false);
const [activeActionItem, setActiveActionItem] = useState<ActiveActionConnectorState | undefined>(
undefined
@ -101,6 +93,10 @@ export const ActionForm = ({
const [actionTypesIndex, setActionTypesIndex] = useState<ActionTypeIndex | undefined>(undefined);
const [emptyActionsIds, setEmptyActionsIds] = useState<string[]>([]);
const closeAddConnectorModal = useCallback(() => setAddModalVisibility(false), [
setAddModalVisibility,
]);
// load action types
useEffect(() => {
(async () => {
@ -183,359 +179,6 @@ export const ActionForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions, connectors]);
const preconfiguredMessage = i18n.translate(
'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
{
defaultMessage: '(preconfigured)',
}
);
const getSelectedOptions = (actionItemId: string) => {
const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
if (
!selectedConnector ||
// if selected connector is not preconfigured and action type is for preconfiguration only,
// do not show regular connectors of this type
(actionTypesIndex &&
!actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
!selectedConnector.isPreconfigured)
) {
return [];
}
const optionTitle = `${selectedConnector.name} ${
selectedConnector.isPreconfigured ? preconfiguredMessage : ''
}`;
return [
{
label: optionTitle,
value: optionTitle,
id: actionItemId,
'data-test-subj': 'itemActionConnector',
},
];
};
const getActionTypeForm = (
actionItem: AlertAction,
actionConnector: ActionConnector,
actionParamsErrors: {
errors: IErrorObject;
},
index: number
) => {
if (!actionTypesIndex) {
return null;
}
const actionType = actionTypesIndex[actionItem.actionTypeId];
const optionsList = connectors
.filter(
(connectorItem) =>
connectorItem.actionTypeId === actionItem.actionTypeId &&
// include only enabled by config connectors or preconfigured
(actionType.enabledInConfig || connectorItem.isPreconfigured)
)
.map(({ name, id, isPreconfigured }) => ({
label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
key: id,
id,
}));
const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
const checkEnabledResult = checkActionFormActionTypeEnabled(
actionTypesIndex[actionConnector.actionTypeId],
connectors.filter((connector) => connector.isPreconfigured)
);
const accordionContent = checkEnabledResult.isEnabled ? (
<Fragment>
<EuiFlexGroup component="div">
<EuiFlexItem>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.actionIdLabel"
defaultMessage="{connectorInstance} connector"
values={{
connectorInstance: actionTypesIndex
? actionTypesIndex[actionConnector.actionTypeId].name
: actionConnector.actionTypeId,
}}
/>
}
labelAppend={
canSave &&
actionTypesIndex &&
actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
<EuiButtonEmpty
size="xs"
data-test-subj={`addNewActionConnectorButton-${actionItem.actionTypeId}`}
onClick={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
setAddModalVisibility(true);
}}
>
<FormattedMessage
defaultMessage="Add connector"
id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton"
/>
</EuiButtonEmpty>
) : null
}
>
<EuiComboBox
fullWidth
singleSelection={{ asPlainText: true }}
options={optionsList}
id={`selectActionConnector-${actionItem.id}`}
data-test-subj={`selectActionConnector-${actionItem.actionTypeId}`}
selectedOptions={getSelectedOptions(actionItem.id)}
onChange={(selectedOptions) => {
setActionIdByIndex(selectedOptions[0].id ?? '', index);
}}
isClearable={false}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
{ParamsFieldsComponent ? (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<ParamsFieldsComponent
actionParams={actionItem.params as any}
index={index}
errors={actionParamsErrors.errors}
editAction={setActionParamsProperty}
messageVariables={messageVariables}
defaultMessage={defaultActionMessage ?? undefined}
docLinks={docLinks}
http={http}
toastNotifications={toastNotifications}
actionConnector={actionConnector}
/>
</Suspense>
) : null}
</Fragment>
) : (
checkEnabledResult.messageCard
);
return (
<Fragment key={index}>
<EuiAccordion
initialIsOpen={true}
id={index.toString()}
className="actAccordionActionForm"
buttonContentClassName="actAccordionActionForm__button"
data-test-subj={`alertActionAccordion-${defaultActionGroupId}`}
buttonContent={
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={actionTypeRegistered.iconClass} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<FormattedMessage
defaultMessage="{actionConnectorName}"
id="xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle"
values={{
actionConnectorName: `${actionConnector.name} ${
actionConnector.isPreconfigured ? preconfiguredMessage : ''
}`,
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{checkEnabledResult.isEnabled === false && (
<Fragment>
<EuiIconTip
type="alert"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle',
{
defaultMessage: 'This action is disabled',
}
)}
position="right"
/>
</Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
extraAction={
<EuiButtonIcon
iconType="cross"
color="danger"
className="actAccordionActionForm__extraAction"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel',
{
defaultMessage: 'Delete',
}
)}
onClick={() => {
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
0
);
setActiveActionItem(undefined);
}}
/>
}
paddingSize="l"
>
{accordionContent}
</EuiAccordion>
<EuiSpacer size="xs" />
</Fragment>
);
};
const getAddConnectorsForm = (actionItem: AlertAction, index: number) => {
const actionTypeName = actionTypesIndex
? actionTypesIndex[actionItem.actionTypeId].name
: actionItem.actionTypeId;
const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
const noConnectorsLabel = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel"
defaultMessage="No {actionTypeName} connectors"
values={{
actionTypeName,
}}
/>
);
return (
<Fragment key={index}>
<EuiAccordion
initialIsOpen={true}
id={index.toString()}
className="actAccordionActionForm"
buttonContentClassName="actAccordionActionForm__button"
data-test-subj={`alertActionAccordion-${defaultActionGroupId}`}
buttonContent={
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={actionTypeRegistered.iconClass} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<div>
<FormattedMessage
defaultMessage="{actionConnectorName}"
id="xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle"
values={{
actionConnectorName: actionTypeRegistered.actionTypeTitle,
}}
/>
</div>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
extraAction={
<EuiButtonIcon
iconType="cross"
color="danger"
className="actAccordionActionForm__extraAction"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel',
{
defaultMessage: 'Delete',
}
)}
onClick={() => {
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
0
);
setActiveActionItem(undefined);
}}
/>
}
paddingSize="l"
>
{canSave ? (
<EuiEmptyPrompt
title={
emptyActionsIds.find((emptyId: string) => actionItem.id === emptyId) ? (
noConnectorsLabel
) : (
<EuiCallOut
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle',
{
defaultMessage: 'Unable to load connector.',
}
)}
color="warning"
/>
)
}
actions={[
<EuiButton
color="primary"
fill
size="s"
data-test-subj="createActionConnectorButton"
onClick={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
setAddModalVisibility(true);
}}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel"
defaultMessage="Create a connector"
/>
</EuiButton>,
]}
/>
) : (
<EuiCallOut title={noConnectorsLabel}>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors"
defaultMessage="Only authorized users can configure a connector. Contact your administrator."
/>
</p>
</EuiCallOut>
)}
</EuiAccordion>
<EuiSpacer size="xs" />
</Fragment>
);
};
function addActionType(actionTypeModel: ActionTypeModel) {
if (!defaultActionGroupId) {
toastNotifications!.addDanger({
@ -628,116 +271,172 @@ export const ActionForm = ({
});
}
const alertActionsList = actions.map((actionItem: AlertAction, index: number) => {
const actionConnector = connectors.find((field) => field.id === actionItem.id);
// connectors doesn't exists
if (!actionConnector) {
return getAddConnectorsForm(actionItem, index);
}
const actionErrors: { errors: IErrorObject } = actionTypeRegistry
.get(actionItem.actionTypeId)
?.validateParams(actionItem.params);
return getActionTypeForm(actionItem, actionConnector, actionErrors, index);
});
return (
return isLoadingConnectors ? (
<SectionLoading>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.loadingConnectorsDescription"
defaultMessage="Loading connectors…"
/>
</SectionLoading>
) : (
<Fragment>
{isLoadingConnectors ? (
<SectionLoading>
<EuiTitle size="s">
<h4>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.loadingConnectorsDescription"
defaultMessage="Loading connectors…"
defaultMessage="Actions"
id="xpack.triggersActionsUI.sections.alertForm.actionSectionsTitle"
/>
</SectionLoading>
) : (
<Fragment>
<EuiTitle size="s">
<h4>
<FormattedMessage
defaultMessage="Actions"
id="xpack.triggersActionsUI.sections.alertForm.actionSectionsTitle"
</h4>
</EuiTitle>
<EuiSpacer size="m" />
{actionTypesIndex &&
actions.map((actionItem: AlertAction, index: number) => {
const actionConnector = connectors.find((field) => field.id === actionItem.id);
// connectors doesn't exists
if (!actionConnector) {
return (
<AddConnectorInline
actionTypesIndex={actionTypesIndex}
actionItem={actionItem}
index={index}
key={`action-form-action-at-${index}`}
actionTypeRegistry={actionTypeRegistry}
defaultActionGroupId={defaultActionGroupId}
capabilities={capabilities}
emptyActionsIds={emptyActionsIds}
onDeleteConnector={() => {
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
.length === 0
);
setActiveActionItem(undefined);
}}
onAddConnector={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
setAddModalVisibility(true);
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="m" />
{alertActionsList}
{isAddActionPanelOpen === false ? (
<div>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
data-test-subj="addAlertActionButton"
onClick={() => setIsAddActionPanelOpen(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addActionButtonLabel"
defaultMessage="Add action"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
) : null}
{isAddActionPanelOpen ? (
<Fragment>
<EuiFlexGroup id="alertActionTypeTitle" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
);
}
const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry
.get(actionItem.actionTypeId)
?.validateParams(actionItem.params);
return (
<ActionTypeForm
actionItem={actionItem}
actionConnector={actionConnector}
actionParamsErrors={actionParamsErrors}
index={index}
key={`action-form-action-at-${index}`}
setActionParamsProperty={setActionParamsProperty}
actionTypesIndex={actionTypesIndex}
connectors={connectors}
http={http}
toastNotifications={toastNotifications}
docLinks={docLinks}
capabilities={capabilities}
actionTypeRegistry={actionTypeRegistry}
defaultActionGroupId={defaultActionGroupId}
defaultActionMessage={defaultActionMessage}
messageVariables={messageVariables}
actionGroups={actionGroups}
setActionGroupIdByIndex={setActionGroupIdByIndex}
onAddConnector={() => {
setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
setAddModalVisibility(true);
}}
onConnectorSelected={(id: string) => {
setActionIdByIndex(id, index);
}}
onDeleteAction={() => {
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
);
setAlertProperty(updatedActions);
setIsAddActionPanelOpen(
updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
0
);
setActiveActionItem(undefined);
}}
/>
);
})}
<EuiSpacer size="m" />
{isAddActionPanelOpen ? (
<Fragment>
<EuiFlexGroup id="alertActionTypeTitle" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
<FormattedMessage
defaultMessage="Select an action type"
id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
{hasDisabledByLicenseActionTypes && (
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
<EuiLink
href={VIEW_LICENSE_OPTIONS_LINK}
target="_blank"
external
className="actActionForm__getMoreActionsLink"
>
<FormattedMessage
defaultMessage="Select an action type"
id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle"
defaultMessage="Get more actions"
id="xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
{hasDisabledByLicenseActionTypes && (
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
<EuiLink
href={VIEW_LICENSE_OPTIONS_LINK}
target="_blank"
external
className="actActionForm__getMoreActionsLink"
>
<FormattedMessage
defaultMessage="Get more actions"
id="xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle"
/>
</EuiLink>
</h5>
</EuiTitle>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup gutterSize="m" wrap>
{isLoadingActionTypes ? (
<SectionLoading>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription"
defaultMessage="Loading action types…"
/>
</SectionLoading>
) : (
actionTypeNodes
)}
</EuiFlexGroup>
</Fragment>
) : null}
</EuiLink>
</h5>
</EuiTitle>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup gutterSize="m" wrap>
{isLoadingActionTypes ? (
<SectionLoading>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription"
defaultMessage="Loading action types…"
/>
</SectionLoading>
) : (
actionTypeNodes
)}
</EuiFlexGroup>
</Fragment>
) : (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
data-test-subj="addAlertActionButton"
onClick={() => setIsAddActionPanelOpen(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addActionButtonLabel"
defaultMessage="Add action"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
{actionTypesIndex && activeActionItem ? (
{actionTypesIndex && activeActionItem && addModalVisible ? (
<ConnectorAddModal
key={activeActionItem.index}
actionType={actionTypesIndex[activeActionItem.actionTypeId]}
addModalVisible={addModalVisible}
setAddModalVisibility={setAddModalVisibility}
onClose={closeAddConnectorModal}
postSaveEventHandler={(savedAction: ActionConnector) => {
connectors.push(savedAction);
setActionIdByIndex(savedAction.id, activeActionItem.index);

View file

@ -0,0 +1,339 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, Suspense, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiFormRow,
EuiComboBox,
EuiAccordion,
EuiButtonIcon,
EuiButtonEmpty,
EuiIconTip,
EuiText,
EuiFormLabel,
EuiFormControlLayout,
EuiSuperSelect,
EuiLoadingSpinner,
EuiBadge,
} from '@elastic/eui';
import { IErrorObject, AlertAction, ActionTypeIndex, ActionConnector } from '../../../types';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionAccordionFormProps } from './action_form';
export type ActionTypeFormProps = {
actionItem: AlertAction;
actionConnector: ActionConnector;
actionParamsErrors: {
errors: IErrorObject;
};
index: number;
onAddConnector: () => void;
onConnectorSelected: (id: string) => void;
onDeleteAction: () => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
actionTypesIndex: ActionTypeIndex;
connectors: ActionConnector[];
} & Pick<
ActionAccordionFormProps,
| 'defaultActionGroupId'
| 'actionGroups'
| 'setActionGroupIdByIndex'
| 'setActionParamsProperty'
| 'http'
| 'actionTypeRegistry'
| 'toastNotifications'
| 'docLinks'
| 'messageVariables'
| 'defaultActionMessage'
| 'capabilities'
>;
const preconfiguredMessage = i18n.translate(
'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
{
defaultMessage: '(preconfigured)',
}
);
export const ActionTypeForm = ({
actionItem,
actionConnector,
actionParamsErrors,
index,
onAddConnector,
onConnectorSelected,
onDeleteAction,
setActionParamsProperty,
actionTypesIndex,
connectors,
http,
toastNotifications,
docLinks,
capabilities,
actionTypeRegistry,
defaultActionGroupId,
defaultActionMessage,
messageVariables,
actionGroups,
setActionGroupIdByIndex,
}: ActionTypeFormProps) => {
const [isOpen, setIsOpen] = useState(true);
const canSave = hasSaveActionsCapability(capabilities);
const getSelectedOptions = (actionItemId: string) => {
const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
if (
!selectedConnector ||
// if selected connector is not preconfigured and action type is for preconfiguration only,
// do not show regular connectors of this type
(actionTypesIndex &&
!actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
!selectedConnector.isPreconfigured)
) {
return [];
}
const optionTitle = `${selectedConnector.name} ${
selectedConnector.isPreconfigured ? preconfiguredMessage : ''
}`;
return [
{
label: optionTitle,
value: optionTitle,
id: actionItemId,
'data-test-subj': 'itemActionConnector',
},
];
};
const actionType = actionTypesIndex[actionItem.actionTypeId];
const optionsList = connectors
.filter(
(connectorItem) =>
connectorItem.actionTypeId === actionItem.actionTypeId &&
// include only enabled by config connectors or preconfigured
(actionType.enabledInConfig || connectorItem.isPreconfigured)
)
.map(({ name, id, isPreconfigured }) => ({
label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
key: id,
id,
}));
const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
if (!actionTypeRegistered) return null;
const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
const checkEnabledResult = checkActionFormActionTypeEnabled(
actionTypesIndex[actionConnector.actionTypeId],
connectors.filter((connector) => connector.isPreconfigured)
);
const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
const selectedActionGroup =
actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
const accordionContent = checkEnabledResult.isEnabled ? (
<Fragment>
{actionGroups && selectedActionGroup && setActionGroupIdByIndex && (
<Fragment>
<EuiFlexGroup component="div">
<EuiFlexItem grow={true}>
<EuiFormControlLayout
fullWidth
prepend={
<EuiFormLabel
htmlFor={`addNewActionConnectorActionGroup-${actionItem.actionTypeId}`}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.actionRunWhenInActionGroup"
defaultMessage="Run When"
/>
</EuiFormLabel>
}
>
<EuiSuperSelect
fullWidth
id={`addNewActionConnectorActionGroup-${actionItem.actionTypeId}`}
data-test-subj={`addNewActionConnectorActionGroup-${index}`}
options={actionGroups.map(({ id: value, name }) => ({
value,
inputDisplay: name,
'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`,
}))}
valueOfSelected={selectedActionGroup.id}
onChange={(group) => {
setActionGroupIdByIndex(group, index);
}}
/>
</EuiFormControlLayout>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</Fragment>
)}
<EuiFlexGroup component="div">
<EuiFlexItem>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.actionIdLabel"
defaultMessage="{connectorInstance} connector"
values={{
connectorInstance: actionTypesIndex
? actionTypesIndex[actionConnector.actionTypeId].name
: actionConnector.actionTypeId,
}}
/>
}
labelAppend={
canSave &&
actionTypesIndex &&
actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
<EuiButtonEmpty
size="xs"
data-test-subj={`addNewActionConnectorButton-${actionItem.actionTypeId}`}
onClick={onAddConnector}
>
<FormattedMessage
defaultMessage="Add connector"
id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton"
/>
</EuiButtonEmpty>
) : null
}
>
<EuiComboBox
fullWidth
singleSelection={{ asPlainText: true }}
options={optionsList}
id={`selectActionConnector-${actionItem.id}`}
data-test-subj={`selectActionConnector-${actionItem.actionTypeId}`}
selectedOptions={getSelectedOptions(actionItem.id)}
onChange={(selectedOptions) => {
onConnectorSelected(selectedOptions[0].id ?? '');
}}
isClearable={false}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
{ParamsFieldsComponent ? (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<ParamsFieldsComponent
actionParams={actionItem.params as any}
index={index}
errors={actionParamsErrors.errors}
editAction={setActionParamsProperty}
messageVariables={messageVariables}
defaultMessage={defaultActionMessage ?? undefined}
docLinks={docLinks}
http={http}
toastNotifications={toastNotifications}
actionConnector={actionConnector}
/>
</Suspense>
) : null}
</Fragment>
) : (
checkEnabledResult.messageCard
);
return (
<Fragment key={index}>
<EuiAccordion
initialIsOpen={true}
id={index.toString()}
onToggle={setIsOpen}
paddingSize="l"
className="actAccordionActionForm"
buttonContentClassName="actAccordionActionForm__button"
data-test-subj={`alertActionAccordion-${index}`}
buttonContent={
<EuiFlexGroup gutterSize="l" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={actionTypeRegistered.iconClass} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<FormattedMessage
defaultMessage="{actionConnectorName}"
id="xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle"
values={{
actionConnectorName: `${actionConnector.name} ${
actionConnector.isPreconfigured ? preconfiguredMessage : ''
}`,
}}
/>
</EuiFlexItem>
{selectedActionGroup && !isOpen && (
<EuiFlexItem grow={false}>
<EuiBadge>{selectedActionGroup.name}</EuiBadge>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
{checkEnabledResult.isEnabled === false && (
<Fragment>
<EuiIconTip
type="alert"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle',
{
defaultMessage: 'This action is disabled',
}
)}
position="right"
/>
</Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
extraAction={
<EuiButtonIcon
iconType="minusInCircle"
color="danger"
className="actAccordionActionForm__extraAction"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel',
{
defaultMessage: 'Delete',
}
)}
onClick={onDeleteAction}
/>
}
>
{accordionContent}
</EuiAccordion>
<EuiSpacer size="m" />
</Fragment>
);
};

View file

@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiAccordion,
EuiButtonIcon,
EuiEmptyPrompt,
EuiCallOut,
EuiText,
} from '@elastic/eui';
import { AlertAction, ActionTypeIndex } from '../../../types';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionAccordionFormProps } from './action_form';
type AddConnectorInFormProps = {
actionTypesIndex: ActionTypeIndex;
actionItem: AlertAction;
index: number;
onAddConnector: () => void;
onDeleteConnector: () => void;
emptyActionsIds: string[];
} & Pick<ActionAccordionFormProps, 'actionTypeRegistry' | 'defaultActionGroupId' | 'capabilities'>;
export const AddConnectorInline = ({
actionTypesIndex,
actionItem,
index,
onAddConnector,
onDeleteConnector,
actionTypeRegistry,
emptyActionsIds,
defaultActionGroupId,
capabilities,
}: AddConnectorInFormProps) => {
const canSave = hasSaveActionsCapability(capabilities);
const actionTypeName = actionTypesIndex
? actionTypesIndex[actionItem.actionTypeId].name
: actionItem.actionTypeId;
const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
const noConnectorsLabel = (
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel"
defaultMessage="No {actionTypeName} connectors"
values={{
actionTypeName,
}}
/>
);
return (
<Fragment key={index}>
<EuiAccordion
initialIsOpen={true}
id={index.toString()}
className="actAccordionActionForm"
buttonContentClassName="actAccordionActionForm__button"
data-test-subj={`alertActionAccordion-${index}`}
buttonContent={
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={actionTypeRegistered.iconClass} size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<div>
<FormattedMessage
defaultMessage="{actionConnectorName}"
id="xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle"
values={{
actionConnectorName: actionTypeRegistered.actionTypeTitle,
}}
/>
</div>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
extraAction={
<EuiButtonIcon
iconType="minusInCircle"
color="danger"
className="actAccordionActionForm__extraAction"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel',
{
defaultMessage: 'Delete',
}
)}
onClick={onDeleteConnector}
/>
}
paddingSize="l"
>
{canSave ? (
<EuiEmptyPrompt
title={
emptyActionsIds.find((emptyId: string) => actionItem.id === emptyId) ? (
noConnectorsLabel
) : (
<EuiCallOut
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle',
{
defaultMessage: 'Unable to load connector.',
}
)}
color="warning"
/>
)
}
actions={[
<EuiButton
color="primary"
fill
size="s"
data-test-subj="createActionConnectorButton"
onClick={onAddConnector}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel"
defaultMessage="Create a connector"
/>
</EuiButton>,
]}
/>
) : (
<EuiCallOut title={noConnectorsLabel}>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors"
defaultMessage="Only authorized users can configure a connector. Contact your administrator."
/>
</p>
</EuiCallOut>
)}
</EuiAccordion>
<EuiSpacer size="xs" />
</Fragment>
);
};

View file

@ -65,8 +65,7 @@ describe('connector_add_modal', () => {
const wrapper = mountWithIntl(
<ConnectorAddModal
addModalVisible={true}
setAddModalVisibility={() => {}}
onClose={() => {}}
actionType={actionType}
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}

View file

@ -32,8 +32,7 @@ import {
interface ConnectorAddModalProps {
actionType: ActionType;
addModalVisible: boolean;
setAddModalVisibility: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
postSaveEventHandler?: (savedAction: ActionConnector) => void;
http: HttpSetup;
actionTypeRegistry: ActionTypeRegistryContract;
@ -48,8 +47,7 @@ interface ConnectorAddModalProps {
export const ConnectorAddModal = ({
actionType,
addModalVisible,
setAddModalVisibility,
onClose,
postSaveEventHandler,
http,
toastNotifications,
@ -79,14 +77,11 @@ export const ConnectorAddModal = ({
>(undefined);
const closeModal = useCallback(() => {
setAddModalVisibility(false);
setConnector(initialConnector);
setServerError(undefined);
}, [initialConnector, setAddModalVisibility]);
onClose();
}, [initialConnector, onClose]);
if (!addModalVisible) {
return null;
}
const actionTypeModel = actionTypeRegistry.get(actionType.id);
const errors = {
...actionTypeModel?.validateConnector(connector).errors,

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useState, useEffect, Suspense } from 'react';
import React, { Fragment, useState, useEffect, Suspense, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -153,9 +153,17 @@ export const AlertForm = ({
setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null);
}, [alert, alertTypeRegistry]);
const setAlertProperty = (key: string, value: any) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
};
const setAlertProperty = useCallback(
(key: string, value: any) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
},
[dispatch]
);
const setActions = useCallback(
(updatedActions: AlertAction[]) => setAlertProperty('actions', updatedActions),
[setAlertProperty]
);
const setAlertParams = (key: string, value: any) => {
dispatch({ command: { type: 'setAlertParams' }, payload: { key, value } });
@ -169,9 +177,12 @@ export const AlertForm = ({
dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } });
};
const setActionParamsProperty = (key: string, value: any, index: number) => {
dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
};
const setActionParamsProperty = useCallback(
(key: string, value: any, index: number) => {
dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
},
[dispatch]
);
const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];
@ -202,6 +213,7 @@ export const AlertForm = ({
label={item.name}
onClick={() => {
setAlertProperty('alertTypeId', item.id);
setActions([]);
setAlertTypeModel(item);
setAlertProperty('params', {});
if (alertTypesIndex && alertTypesIndex.has(item.id)) {
@ -289,26 +301,25 @@ export const AlertForm = ({
/>
</Suspense>
) : null}
{canShowActions && defaultActionGroupId ? (
{canShowActions &&
defaultActionGroupId &&
alertTypeModel &&
alertTypesIndex?.has(alert.alertTypeId) ? (
<ActionForm
actions={alert.actions}
setHasActionsDisabled={setHasActionsDisabled}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
messageVariables={
alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)
? actionVariablesFromAlertType(alertTypesIndex.get(alert.alertTypeId)!).sort((a, b) =>
a.name.toUpperCase().localeCompare(b.name.toUpperCase())
)
: undefined
}
messageVariables={actionVariablesFromAlertType(
alertTypesIndex.get(alert.alertTypeId)!
).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()))}
defaultActionGroupId={defaultActionGroupId}
actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
setAlertProperty={(updatedActions: AlertAction[]) =>
setAlertProperty('actions', updatedActions)
}
setActionParamsProperty={(key: string, value: any, index: number) =>
setActionParamsProperty(key, value, index)
setActionGroupIdByIndex={(group: string, index: number) =>
setActionProperty('group', group, index)
}
setAlertProperty={setActions}
setActionParamsProperty={setActionParamsProperty}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}

View file

@ -55,6 +55,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await nameInput.click();
}
async function defineAlwaysFiringAlert(alertName: string) {
await pageObjects.triggersActionsUI.clickCreateAlertButton();
await testSubjects.setValue('alertNameInput', alertName);
await testSubjects.click('test.always-firing-SelectOption');
}
describe('create alert', function () {
before(async () => {
await pageObjects.common.navigateToApp('triggersActions');
@ -106,6 +112,57 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
});
it('should create an alert with actions in multiple groups', async () => {
const alertName = generateUniqueKey();
await defineAlwaysFiringAlert(alertName);
// create Slack connector and attach an action using it
await testSubjects.click('.slack-ActionTypeSelectOption');
await testSubjects.click('addNewActionConnectorButton-.slack');
const slackConnectorName = generateUniqueKey();
await testSubjects.setValue('nameInput', slackConnectorName);
await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
const createdConnectorToastTitle = await pageObjects.common.closeToast();
expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
await testSubjects.setValue('messageTextArea', 'test message ');
await (
await find.byCssSelector(
'[data-test-subj="alertActionAccordion-0"] [data-test-subj="messageTextArea"]'
)
).type('some text ');
await testSubjects.click('addAlertActionButton');
await testSubjects.click('.slack-ActionTypeSelectOption');
await testSubjects.setValue('messageTextArea', 'test message ');
await (
await find.byCssSelector(
'[data-test-subj="alertActionAccordion-1"] [data-test-subj="messageTextArea"]'
)
).type('some text ');
await testSubjects.click('addNewActionConnectorActionGroup-1');
await testSubjects.click('addNewActionConnectorActionGroup-1-option-other');
await testSubjects.click('saveAlertButton');
const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql(`Created alert "${alertName}"`);
await pageObjects.triggersActionsUI.searchAlerts(alertName);
const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
expect(searchResultsAfterSave).to.eql([
{
name: alertName,
tagsText: '',
alertType: 'Always Firing',
interval: '1m',
},
]);
// clean up created alert
const alertsToDelete = await getAlertsByName(alertName);
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
});
it('should show save confirmation before creating alert with no actions', async () => {
const alertName = generateUniqueKey();
await defineAlert(alertName);

View file

@ -78,6 +78,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
defaultActionGroupId: 'default',
producer: 'alerts',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;