From 74b779ad0f992a2af90e0be4aac1946608129d61 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 12 Nov 2020 13:29:43 +0000 Subject: [PATCH] added generic action group UI --- .../alerting_example/common/constants.ts | 9 + .../public/alert_types/always_firing.tsx | 145 ++++++++++++++-- .../server/alert_types/always_firing.ts | 47 ++++-- .../alert_form/alert_conditions.test.tsx | 53 +++++- .../sections/alert_form/alert_conditions.tsx | 155 +++++++++++++++--- .../sections/alert_form/alert_form.tsx | 6 +- .../application/sections/alert_form/index.tsx | 6 + .../public/application/sections/index.tsx | 6 + .../triggers_actions_ui/public/index.ts | 7 +- .../triggers_actions_ui/public/types.ts | 2 + 10 files changed, 380 insertions(+), 56 deletions(-) diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts index dd9cc21954e61..40cc298db795a 100644 --- a/x-pack/examples/alerting_example/common/constants.ts +++ b/x-pack/examples/alerting_example/common/constants.ts @@ -8,6 +8,15 @@ export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample'; // always firing export const DEFAULT_INSTANCES_TO_GENERATE = 5; +export interface AlwaysFiringParams { + instances?: number; + thresholds?: { + small?: number; + medium?: number; + large?: number; + }; +} +export type AlwaysFiringActionGroupIds = keyof AlwaysFiringParams['thresholds']; // Astros export enum Craft { diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index a5d158fca836b..06fba57456263 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -4,17 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldNumber, + EuiFormRow, + EuiPopover, + EuiExpression, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../plugins/triggers_actions_ui/public'; -import { DEFAULT_INSTANCES_TO_GENERATE } from '../../common/constants'; - -interface AlwaysFiringParamsProps { - alertParams: { instances?: number }; - setAlertParams: (property: string, value: any) => void; - errors: { [key: string]: string[] }; -} +import { omit, pick } from 'lodash'; +import { + ActionGroupWithCondition, + AlertConditions, + AlertConditionsGroup, + AlertTypeModel, + AlertTypeParamsExpressionProps, + AlertsContextValue, +} from '../../../../plugins/triggers_actions_ui/public'; +import { + AlwaysFiringParams, + AlwaysFiringActionGroupIds, + DEFAULT_INSTANCES_TO_GENERATE, +} from '../../common/constants'; export function getAlertType(): AlertTypeModel { return { @@ -24,7 +38,7 @@ export function getAlertType(): AlertTypeModel { iconClass: 'bolt', documentationUrl: null, alertParamsExpression: AlwaysFiringExpression, - validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { + validate: (alertParams: AlwaysFiringParams) => { const { instances } = alertParams; const validationResult = { errors: { @@ -44,11 +58,21 @@ export function getAlertType(): AlertTypeModel { }; } -export const AlwaysFiringExpression: React.FunctionComponent = ({ - alertParams, - setAlertParams, -}) => { - const { instances = DEFAULT_INSTANCES_TO_GENERATE } = alertParams; +const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = { + small: 0, + medium: 5000, + large: 10000, +}; + +export const AlwaysFiringExpression: React.FunctionComponent> = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { + const { + instances = DEFAULT_INSTANCES_TO_GENERATE, + thresholds = pick(DEFAULT_THRESHOLDS, defaultActionGroupId), + } = alertParams; + return ( @@ -67,6 +91,95 @@ export const AlwaysFiringExpression: React.FunctionComponent + + + + + Number.isInteger(thresholds[actionGroup.id as AlwaysFiringActionGroupIds]) + ? { + ...actionGroup, + conditions: thresholds[actionGroup.id as AlwaysFiringActionGroupIds]!, + } + : actionGroup + )} + onInitializeConditionsFor={(actionGroup) => { + setAlertParams('thresholds', { + ...thresholds, + ...pick(DEFAULT_THRESHOLDS, actionGroup.id), + }); + }} + > + { + setAlertParams('thresholds', omit(thresholds, actionGroup.id)); + }} + > + { + setAlertParams('thresholds', { + ...thresholds, + [actionGroup.id]: actionGroup.conditions, + }); + }} + /> + + + + + ); }; + +interface TShirtSelectorProps { + actionGroup?: ActionGroupWithCondition; + setTShirtThreshold: (actionGroup: ActionGroupWithCondition) => void; +} +const TShirtSelector = ({ actionGroup, setTShirtThreshold }: TShirtSelectorProps) => { + const [isOpen, setIsOpen] = useState(false); + + if (!actionGroup) { + return null; + } + + return ( + setIsOpen(true)} + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + ownFocus + anchorPosition="downLeft" + > + + + {'Is Above'} + + + { + const conditions = parseInt(e.target.value, 10); + if (e.target.value && !isNaN(conditions)) { + setTShirtThreshold({ + ...actionGroup, + conditions, + }); + } + }} + /> + + + + ); +}; 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 d02406a23045e..3fe24932fccc9 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 @@ -7,29 +7,54 @@ import uuid from 'uuid'; import { range, random } from 'lodash'; import { AlertType } from '../../../../plugins/alerts/server'; -import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; +import { + DEFAULT_INSTANCES_TO_GENERATE, + ALERTING_EXAMPLE_APP_ID, + AlwaysFiringParams, +} from '../../common/constants'; const ACTION_GROUPS = [ - { id: 'small', name: 'small' }, - { id: 'medium', name: 'medium' }, - { id: 'large', name: 'large' }, + { id: 'small', name: 'Small t-shirt' }, + { id: 'medium', name: 'Medium t-shirt' }, + { id: 'large', name: 'Large t-shirt' }, ]; +const DEFAULT_ACTION_GROUP = 'small'; -export const alertType: AlertType = { +function getTShirtSizeByIdAndThreshold(id: string, thresholds: AlwaysFiringParams['thresholds']) { + const idAsNumber = parseInt(id, 10); + if (!isNaN(idAsNumber)) { + if (thresholds?.large && thresholds.large < idAsNumber) { + return 'large'; + } + if (thresholds?.medium && thresholds.medium < idAsNumber) { + return 'medium'; + } + if (thresholds?.small && thresholds.small < idAsNumber) { + return 'small'; + } + } + return DEFAULT_ACTION_GROUP; +} + +export const alertType: AlertType = { id: 'example.always-firing', name: 'Always firing', actionGroups: ACTION_GROUPS, - defaultActionGroupId: 'small', - async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) { + defaultActionGroupId: DEFAULT_ACTION_GROUP, + async executor({ + services, + params: { instances = DEFAULT_INSTANCES_TO_GENERATE, thresholds }, + state, + }) { const count = (state.count ?? 0) + 1; range(instances) - .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! })) - .forEach((instance: { id: string; tshirtSize: string }) => { + .map(() => uuid.v4()) + .forEach((id: string) => { services - .alertInstanceFactory(instance.id) + .alertInstanceFactory(id) .replaceState({ triggerdOnCycle: count }) - .scheduleActions(instance.tshirtSize); + .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx index aa2738ddbbf10..2b9d109d210a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.test.tsx @@ -7,13 +7,14 @@ import * as React from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; -import { ActionGroupWithCondition, AlertConditions } from './alert_conditions'; +import AlertConditions, { ActionGroupWithCondition } from './alert_conditions'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiButtonEmpty, } from '@elastic/eui'; describe('alert_conditions', () => { @@ -153,6 +154,56 @@ describe('alert_conditions', () => { expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2); }); + it('render add buttons for action group without conditions', async () => { + const onInitializeConditionsFor = jest.fn(); + + const ConditionForm = ({ + actionGroup, + }: { + actionGroup?: ActionGroupWithCondition<{ someProp: string }>; + }) => { + return ( + + ID + {actionGroup?.id} + + ); + }; + + const wrapper = await setup( + + + + ); + + expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(` + + Should Not Render + + `); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(onInitializeConditionsFor).toHaveBeenCalledWith({ + id: 'shouldntRender', + name: 'Should Not Render', + }); + }); + it('passes in any additional props the container passes in', async () => { const callbackProp = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx index 4e2f8d6faaebe..9c3400a4a74c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions.tsx @@ -3,53 +3,156 @@ * 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, { PropsWithChildren } from 'react'; +import React, { Fragment, PropsWithChildren } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import { + EuiFlexItem, + EuiText, + EuiFlexGroup, + EuiTitle, + EuiFormRow, + EuiButtonIcon, + EuiButtonEmpty, +} from '@elastic/eui'; +import { partition } from 'lodash'; import { ActionGroup } from '../../../../../alerts/common'; export interface ActionGroupWithCondition extends ActionGroup { conditions?: T; } -interface AlertConditionsProps { +export interface AlertConditionsProps { headline?: string; actionGroups: Array>; - // getActionGroupComponent: (actionGroup: ActionGroupWithCondition) => ReactElement; + onInitializeConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; + onResetConditionsFor?: (actionGroup: ActionGroupWithCondition) => void; } export const AlertConditions = ({ headline, actionGroups, + onInitializeConditionsFor, + onResetConditionsFor, children, }: PropsWithChildren>) => { + const [withConditions, withoutConditions] = partition(actionGroups, (actionGroup) => + actionGroup.hasOwnProperty('conditions') + ); + return ( - - - - -
- -
-
-
- {headline && {headline}} -
- - {actionGroups - .filter((actionGroup) => !!actionGroup.conditions) - .map((actionGroup) => ( + + + + + +
+ +
+
+ {headline && ( + + + {headline} + + + )} +
+
+
+ + + {withConditions.map((actionGroup) => ( {React.isValidElement(children) && - React.cloneElement(React.Children.only(children), { - actionGroup, - })} + React.cloneElement( + React.Children.only(children), + onResetConditionsFor + ? { + actionGroup, + onResetConditionsFor, + } + : { actionGroup } + )} ))} - + {onInitializeConditionsFor && withoutConditions.length > 0 && ( + + + + + + {withoutConditions.map((actionGroup) => ( + + onInitializeConditionsFor(actionGroup)} + > + {actionGroup.name} + + + ))} + + + )} +
+
); }; + +export type AlertConditionsGroup = { + actionGroup?: ActionGroupWithCondition; +} & Pick, 'onResetConditionsFor'>; + +export const AlertConditionsGroup = ({ + actionGroup, + onResetConditionsFor, + children, + ...otherProps +}: PropsWithChildren>) => { + if (!actionGroup) { + return null; + } + + return ( + onResetConditionsFor(actionGroup)} + /> + ) + } + > + {React.isValidElement(children) ? ( + React.cloneElement(React.Children.only(children), { + actionGroup, + ...otherProps, + }) + ) : ( + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { AlertConditions as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 213d1d7ad36df..ee308a3cb06ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -437,7 +437,9 @@ export const AlertForm = ({
)} - {AlertParamsExpressionComponent ? ( + {AlertParamsExpressionComponent && + defaultActionGroupId && + alertTypesIndex?.has(alert.alertTypeId) ? ( }> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx index 79720edc4672e..1c499511ec921 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/index.tsx @@ -5,6 +5,12 @@ */ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +export { + AlertConditions, + AlertConditionsGroup, + ActionGroupWithCondition, + AlertConditionsProps, +} from './alert_conditions'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 677ee139271c0..490aeb5be8bd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -6,6 +6,12 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; +export { + ActionGroupWithCondition, + AlertConditionsProps, + AlertConditions, + AlertConditionsGroup, +} from './alert_form'; export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./alert_form/alert_edit'))); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 3794112e1d502..bd8dde3fb7830 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -9,7 +9,12 @@ import { Plugin } from './plugin'; export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; -export { AlertEdit } from './application/sections'; +export { + AlertEdit, + AlertConditions, + AlertConditionsGroup, + ActionGroupWithCondition, +} from './application/sections'; export { ActionForm } from './application/sections/action_connector_form'; export { AlertAction, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1a6b68080c9a4..678d4b5debba9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -169,6 +169,8 @@ export interface AlertTypeParamsExpressionProps< setAlertProperty: (key: string, value: any) => void; errors: IErrorObject; alertsContext: AlertsContextValue; + defaultActionGroupId: string; + actionGroups: ActionGroup[]; } export interface AlertTypeModel {