diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 485b24dced346..d67cc06463942 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20148,7 +20148,6 @@ "xpack.triggersActionsUI.home.connectorsTabTitle": "コネクター", "xpack.triggersActionsUI.home.sectionDescription": "アラートを使用して条件を検出し、コネクターを使用してアクションを実行します。", "xpack.triggersActionsUI.managementSection.displayName": "アラートとアクション", - "xpack.triggersActionsUI.sections.actionAdd.indexAction.indexTextFieldLabel": "タグ (任意)", "xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.actionConnectorAdd.manageLicensePlanBannerLinkTitle": "ライセンスの管理", "xpack.triggersActionsUI.sections.actionConnectorAdd.saveAndTestButtonLabel": "保存してテスト", @@ -20256,7 +20255,6 @@ "xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel": "コネクターを作成する", "xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton": "新規追加", "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名前", - "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "確認間隔", "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "条件を評価する頻度を定義します。", "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "{actionTypeName}コネクターがありません", @@ -20273,7 +20271,6 @@ "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクションタイプを選択してください", - "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", "xpack.triggersActionsUI.sections.alertForm.unableToAddAction": "デフォルトアクショングループの定義がないのでアクションを追加できません", "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "コネクターを読み込めません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 98d13011d3306..103aef656c4e1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20167,7 +20167,6 @@ "xpack.triggersActionsUI.home.connectorsTabTitle": "连接器", "xpack.triggersActionsUI.home.sectionDescription": "使用告警检测条件,并使用连接器采取操作。", "xpack.triggersActionsUI.managementSection.displayName": "告警和操作", - "xpack.triggersActionsUI.sections.actionAdd.indexAction.indexTextFieldLabel": "标记(可选)", "xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.actionConnectorAdd.manageLicensePlanBannerLinkTitle": "管理许可证", "xpack.triggersActionsUI.sections.actionConnectorAdd.saveAndTestButtonLabel": "保存并测试", @@ -20276,7 +20275,6 @@ "xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel": "创建连接器", "xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton": "新添", "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名称", - "xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertForm.checkFieldLabel": "检查频率", "xpack.triggersActionsUI.sections.alertForm.checkWithTooltip": "定义评估条件的频率。", "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "无 {actionTypeName} 连接器", @@ -20293,7 +20291,6 @@ "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "选择操作类型", - "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", "xpack.triggersActionsUI.sections.alertForm.unableToAddAction": "无法添加操作,因为未定义默认操作组", "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "无法加载连接器", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index fc48a8e977c7d..5c1e0aa0100e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -16,8 +16,8 @@ import { CoreStart, ScopedHistory, } from 'kibana/public'; -import { Section, routeToAlertDetails } from './constants'; import { KibanaFeature } from '../../../features/common'; +import { Section, routeToAlertDetails } from './constants'; import { AppContextProvider } from './app_context'; import { ActionTypeRegistryContract, AlertTypeRegistryContract } from '../types'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index a4293f94268ba..0b2f777d13f25 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -18,6 +18,7 @@ import { DataPublicPluginStartUi, IndexPatternsContract, } from 'src/plugins/data/public'; +import { KibanaFeature } from '../../../../features/common'; import { AlertTypeRegistryContract, ActionTypeRegistryContract } from '../../types'; export interface AlertsContextValue> { @@ -34,6 +35,7 @@ export interface AlertsContextValue> { metadata?: MetaData; dataUi?: DataPublicPluginStartUi; dataIndexPatterns?: IndexPatternsContract; + kibanaFeatures?: KibanaFeature[]; } const AlertsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index d66c5ba5121b8..4b5f8596501e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -182,8 +182,6 @@ describe('alert_add', () => { wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); - expect(wrapper.contains('Metadata: some value. Fields: test.')).toBeTruthy(); - expect(wrapper.find('input#alertName').props().value).toBe(''); expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe(''); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss new file mode 100644 index 0000000000000..5d6ac684002fb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.scss @@ -0,0 +1,4 @@ +.triggersActionsUI__alertTypeNodeHeading { + margin-left: $euiSizeS; + margin-right: $euiSizeS; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 4041f6f451a23..493b870a1a6d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -187,7 +187,7 @@ describe('alert_form', () => { it('renders alert type description', async () => { await setup(); - wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); + wrapper.find('button[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); const alertDescription = wrapper.find('[data-test-subj="alertDescription"]'); expect(alertDescription.exists()).toBeTruthy(); expect(alertDescription.first().text()).toContain('Alert when testing'); @@ -195,7 +195,7 @@ describe('alert_form', () => { it('renders alert type documentation link', async () => { await setup(); - wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); + wrapper.find('button[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); const alertDocumentationLink = wrapper.find('[data-test-subj="alertDocumentationLink"]'); expect(alertDocumentationLink.exists()).toBeTruthy(); expect(alertDocumentationLink.first().prop('href')).toBe('https://localhost.local/docs'); 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 20ad9a8d7c701..213d1d7ad36df 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 @@ -9,15 +9,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, - EuiIcon, + EuiTextColor, EuiTitle, EuiForm, EuiSpacer, EuiFieldText, + EuiFieldSearch, EuiFlexGrid, EuiFormRow, EuiComboBox, - EuiKeyPadMenuItem, EuiFieldNumber, EuiSelect, EuiIconTip, @@ -25,11 +25,16 @@ import { EuiHorizontalRule, EuiLoadingSpinner, EuiEmptyPrompt, + EuiListGroupItem, + EuiListGroup, EuiLink, EuiText, + EuiNotificationBadge, } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; +import { capitalize } from 'lodash'; +import { KibanaFeature } from '../../../../../features/public'; import { getDurationNumberInItsUnit, getDurationUnitValue, @@ -37,12 +42,23 @@ import { import { loadAlertTypes } from '../../lib/alert_api'; import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; -import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from '../../../types'; +import { + AlertTypeModel, + Alert, + IErrorObject, + AlertAction, + AlertTypeIndex, + AlertType, +} from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; +import { SolutionFilter } from './solution_filter'; +import './alert_form.scss'; + +const ENTER_KEY = 13; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -77,6 +93,10 @@ export function validateBaseProperties(alertObject: Alert) { return validationResult; } +function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { + return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; +} + interface AlertFormProps { alert: Alert; dispatch: React.Dispatch; @@ -104,12 +124,12 @@ export const AlertForm = ({ actionTypeRegistry, docLinks, capabilities, + kibanaFeatures, } = alertsContext; const canShowActions = hasShowActionsCapability(capabilities); const [alertTypeModel, setAlertTypeModel] = useState(null); - const [alertTypesIndex, setAlertTypesIndex] = useState(undefined); const [alertInterval, setAlertInterval] = useState( alert.schedule.interval ? getDurationNumberInItsUnit(alert.schedule.interval) : undefined ); @@ -123,20 +143,53 @@ export const AlertForm = ({ alert.throttle ? getDurationUnitValue(alert.throttle) : 'm' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); + const [alertTypesIndex, setAlertTypesIndex] = useState(null); + + const [availableAlertTypes, setAvailableAlertTypes] = useState< + Array<{ alertTypeModel: AlertTypeModel; alertType: AlertType }> + >([]); + const [filteredAlertTypes, setFilteredAlertTypes] = useState< + Array<{ alertTypeModel: AlertTypeModel; alertType: AlertType }> + >([]); + const [searchText, setSearchText] = useState(); + const [inputText, setInputText] = useState(); + const [solutions, setSolutions] = useState | undefined>(undefined); + const [solutionsFilter, setSolutionFilter] = useState([]); // load alert types useEffect(() => { (async () => { try { - const alertTypes = await loadAlertTypes({ http }); + const alertTypesResult = await loadAlertTypes({ http }); const index: AlertTypeIndex = new Map(); - for (const alertTypeItem of alertTypes) { + for (const alertTypeItem of alertTypesResult) { index.set(alertTypeItem.id, alertTypeItem); } if (alert.alertTypeId && index.has(alert.alertTypeId)) { setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } setAlertTypesIndex(index); + const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); + setAvailableAlertTypes(availableAlertTypesResult); + + const solutionsResult = availableAlertTypesResult.reduce( + (result: Map, alertTypeItem) => { + if (!result.has(alertTypeItem.alertType.producer)) { + result.set( + alertTypeItem.alertType.producer, + (kibanaFeatures + ? getProducerFeatureName(alertTypeItem.alertType.producer, kibanaFeatures) + : capitalize(alertTypeItem.alertType.producer)) ?? + capitalize(alertTypeItem.alertType.producer) + ); + } + return result; + }, + new Map() + ); + setSolutions( + new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b))) + ); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -184,47 +237,143 @@ export const AlertForm = ({ [dispatch] ); + useEffect(() => { + const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null; + setFilteredAlertTypes( + availableAlertTypes + .filter((alertTypeItem) => + solutionsFilter.length > 0 + ? solutionsFilter.find((item) => alertTypeItem.alertType!.producer === item) + : alertTypeItem + ) + .filter((alertTypeItem) => + searchValue + ? alertTypeItem.alertTypeModel.name + .toString() + .toLocaleLowerCase() + .includes(searchValue) || + alertTypeItem.alertType!.producer.toLocaleLowerCase().includes(searchValue) || + alertTypeItem.alertTypeModel.description.toLocaleLowerCase().includes(searchValue) + : alertTypeItem + ) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alertTypeRegistry, availableAlertTypes, searchText, JSON.stringify(solutionsFilter)]); + + const getAvailableAlertTypes = (alertTypesResult: AlertType[]) => + alertTypeRegistry + .list() + .reduce( + ( + arr: Array<{ alertType: AlertType; alertTypeModel: AlertTypeModel }>, + alertTypeRegistryItem: AlertTypeModel + ) => { + const alertType = alertTypesResult.find((item) => alertTypeRegistryItem.id === item.id); + if (alertType) { + arr.push({ + alertType, + alertTypeModel: alertTypeRegistryItem, + }); + } + return arr; + }, + [] + ) + .filter((item) => item.alertType && hasAllPrivilege(alert, item.alertType)) + .filter((item) => + alert.consumer === ALERTS_FEATURE_ID + ? !item.alertTypeModel.requiresAppContext + : item.alertType!.producer === alert.consumer + ); + const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : []; const AlertParamsExpressionComponent = alertTypeModel ? alertTypeModel.alertParamsExpression : null; - const alertTypeRegistryList = alertTypesIndex - ? alertTypeRegistry - .list() - .filter( - (alertTypeRegistryItem: AlertTypeModel) => - alertTypesIndex.has(alertTypeRegistryItem.id) && - hasAllPrivilege(alert, alertTypesIndex.get(alertTypeRegistryItem.id)) - ) - .filter((alertTypeRegistryItem: AlertTypeModel) => - alert.consumer === ALERTS_FEATURE_ID - ? !alertTypeRegistryItem.requiresAppContext - : alertTypesIndex.get(alertTypeRegistryItem.id)!.producer === alert.consumer - ) - : []; + const alertTypesByProducer = filteredAlertTypes.reduce( + ( + result: Record>, + alertTypeValue + ) => { + const producer = alertTypeValue.alertType.producer; + if (producer) { + (result[producer] = result[producer] || []).push({ + name: + typeof alertTypeValue.alertTypeModel.name === 'string' + ? alertTypeValue.alertTypeModel.name + : alertTypeValue.alertTypeModel.name.props.defaultMessage, + id: alertTypeValue.alertTypeModel.id, + alertTypeItem: alertTypeValue.alertTypeModel, + }); + } + return result; + }, + {} + ); - const alertTypeNodes = alertTypeRegistryList.map(function (item, index) { - return ( - { - setAlertProperty('alertTypeId', item.id); - setActions([]); - setAlertTypeModel(item); - setAlertProperty('params', {}); - if (alertTypesIndex && alertTypesIndex.has(item.id)) { - setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); - } - }} - > - - - ); - }); + const alertTypeNodes = Object.entries(alertTypesByProducer) + .sort(([a], [b]) => + solutions ? solutions.get(a)!.localeCompare(solutions.get(b)!) : a.localeCompare(b) + ) + .map(([solution, items], groupIndex) => ( + + + + + + {(kibanaFeatures + ? getProducerFeatureName(solution, kibanaFeatures) + : capitalize(solution)) ?? capitalize(solution)} + + + + + {items.length} + + + + + {items + .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) + .map((item, index) => ( + + + {item.name} + +

{item.alertTypeItem.description}

+
+ + } + onClick={() => { + setAlertProperty('alertTypeId', item.id); + setActions([]); + setAlertTypeModel(item.alertTypeItem); + setAlertProperty('params', {}); + if (alertTypesIndex && alertTypesIndex.has(item.id)) { + setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); + } + }} + /> +
+ ))} +
+ +
+ )); const alertTypeDetails = ( @@ -401,12 +550,9 @@ export const AlertForm = ({ {alertTypeModel ? ( {alertTypeDetails} - ) : alertTypeNodes.length ? ( + ) : availableAlertTypes.length ? ( - -
- -
-
+ +
+ +
+ + } + > + + + setInputText(e.target.value)} + onKeyUp={(e) => { + if (e.keyCode === ENTER_KEY) { + setSearchText(inputText); + } + }} + placeholder={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle', + { defaultMessage: 'Search' } + )} + /> + + {solutions ? ( + + setSolutionFilter(selectedSolutions)} + /> + + ) : null} + +
- - {alertTypeNodes} - + {alertTypeNodes}
) : alertTypesIndex ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx new file mode 100644 index 0000000000000..7caee22cf7633 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFilterGroup, EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; + +interface SolutionFilterProps { + solutions: Map; + onChange?: (selectedSolutions: string[]) => void; +} + +export const SolutionFilter: React.FunctionComponent = ({ + solutions, + onChange, +}: SolutionFilterProps) => { + const [selectedValues, setSelectedValues] = useState([]); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + useEffect(() => { + if (onChange) { + onChange(selectedValues); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedValues]); + + return ( + + setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="solutionsFilterButton" + > + + + } + > +
+ {[...solutions.entries()].map(([id, title]) => ( + { + const isPreviouslyChecked = selectedValues.includes(id); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== id)); + } else { + setSelectedValues([...selectedValues, id]); + } + }} + checked={selectedValues.includes(id) ? 'on' : undefined} + data-test-subj={`solution${id}FilterOption`} + > + {title} + + ))} +
+
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 3b0cd0b177b1b..75f359888a858 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -672,6 +672,7 @@ export const AlertsList: React.FunctionComponent = () => { capabilities, dataUi: dataPlugin.ui, dataIndexPatterns: dataPlugin.indexPatterns, + kibanaFeatures, }} >