From cdebc809df299d6f180ffd7bdc2e161a0e902ae5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 20 Jul 2021 23:31:17 +0300 Subject: [PATCH] [Alerting UI]Changed rules table to support visual indication for disabled and muted alerts (#104190) (#106282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Alerting UI]Changed rules table to support visual indication for disabled or muted alerts * changed columns styles due to the mockup * added tests * fixed quotas * fixed popover * fixed due to the lates UI updates * fixed errors * moved enabled to a separate component * fixed tests * fixed due to comments * Update x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss Co-authored-by: Mike Côté * removed test code * fixed tests * fixed due to comments * fixed due to comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Mike Côté Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Mike Côté --- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../components/alert_details.test.tsx | 18 +- .../components/alert_details.tsx | 10 +- .../sections/alert_form/alert_edit.tsx | 1 + .../components/alert_status_filter.tsx | 4 +- .../alerts_list/components/alerts_list.scss | 26 +++ .../components/alerts_list.test.tsx | 24 ++- .../alerts_list/components/alerts_list.tsx | 196 +++++++++++++----- .../components/collapsed_item_actions.scss | 18 +- .../components/collapsed_item_actions.tsx | 195 ++++++++--------- .../components/rule_enabled_switch.tsx | 54 +++++ .../apps/triggers_actions_ui/alerts_list.ts | 77 ++++--- .../apps/triggers_actions_ui/details.ts | 28 +-- .../page_objects/triggers_actions_ui_page.ts | 2 - 15 files changed, 410 insertions(+), 255 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2287d74e199b0..6dec4466f4b2d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22827,7 +22827,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "アクティブ", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "OK", "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "無効なルール", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "無効にする", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "閉じる", "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "編集", @@ -22881,10 +22880,8 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "ルールの読み取り中にエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "ルールの実行中にエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "ステータス", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", @@ -22906,10 +22903,7 @@ "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToUnmuteRulesMessage": "ルールをミュート解除できませんでした", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "ミュート解除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle": "削除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableHelpText": "無効にすると、ルールは確認されません。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle": "無効にする", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteHelpText": "ミュートにすると、ルールは確認されますが、アクションは実行されません。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション", "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5fa40af4cb375..c4354cc89fe4d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23180,7 +23180,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "确定", "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "已禁用规则", - "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "禁用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "关闭", "xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel": "编辑", @@ -23234,10 +23233,8 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonReading": "读取规则时发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行规则时发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "状态", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标签", @@ -23260,10 +23257,7 @@ "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.failedToUnmuteRulesMessage": "无法取消静音规则", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle": "取消静音", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle": "删除", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableHelpText": "禁用后,将不检查规则。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle": "禁用", - "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteHelpText": "静音后,将检查规则,但不执行操作。", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作", "xpack.triggersActionsUI.sections.alertsList.dismissBunnerButtonLabel": "关闭", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index cad4dabbe8275..8d66bcf9bd290 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -359,16 +359,16 @@ describe('disable button', () => { ) .find(EuiSwitch) - .find('[name="disable"]') + .find('[name="enable"]') .first(); expect(enableButton.props()).toMatchObject({ - checked: false, + checked: true, disabled: false, }); }); - it('should render a disable button when alert is disabled', () => { + it('should render a enable button when alert is disabled', () => { const alert = mockAlert({ enabled: false, }); @@ -390,11 +390,11 @@ describe('disable button', () => { ) .find(EuiSwitch) - .find('[name="disable"]') + .find('[name="enable"]') .first(); expect(enableButton.props()).toMatchObject({ - checked: true, + checked: false, disabled: false, }); }); @@ -428,7 +428,7 @@ describe('disable button', () => { /> ) .find(EuiSwitch) - .find('[name="disable"]') + .find('[name="enable"]') .first(); enableButton.simulate('click'); @@ -468,7 +468,7 @@ describe('disable button', () => { /> ) .find(EuiSwitch) - .find('[name="disable"]') + .find('[name="enable"]') .first(); enableButton.simulate('click'); @@ -531,14 +531,14 @@ describe('disable button', () => { // Disable the alert await act(async () => { - wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click'); await nextTick(); }); expect(disableAlert).toHaveBeenCalled(); // Enable the alert await act(async () => { - wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click'); await nextTick(); }); expect(enableAlert).toHaveBeenCalled(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 02aa5f9b0b828..20692c45c0865 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -219,10 +219,10 @@ export const AlertDetails: React.FunctionComponent = ({ { if (isEnabled) { setIsEnabled(false); @@ -237,8 +237,8 @@ export const AlertDetails: React.FunctionComponent = ({ }} label={ } /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index bf6f0ef43b820..ad727566957de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -133,6 +133,7 @@ export const AlertEdit = ({ aria-labelledby="flyoutAlertEditTitle" size="m" maxWidth={620} + ownFocus={false} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx index 38343116825dd..aca111df97e34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx @@ -95,13 +95,13 @@ export const AlertStatusFilter: React.FunctionComponent export function getHealthColor(status: AlertExecutionStatuses) { switch (status) { case 'active': - return 'primary'; + return 'success'; case 'error': return 'danger'; case 'ok': return 'subdued'; case 'pending': - return 'success'; + return 'accent'; default: return 'warning'; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss index fda7d6aa0b622..c0e46b77b4156 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss @@ -5,3 +5,29 @@ color: $euiColorDarkShade; } } + +.euiTableRow { + &:hover, + &:focus-within, + &[class*='-isActive'] { + .alertSidebarItem__action { + opacity: 1; + } + } +} + +/** + * 1. Only visually hide the action, so that it's still accessible to screen readers. + * 2. When tabbed to, this element needs to be visible for keyboard accessibility. + */ +.alertSidebarItem__action { + opacity: 0; /* 1 */ + + &.alertSidebarItem__mobile { + opacity: 1; + } + + &:focus { + opacity: 1; /* 2 */ + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 311166f09e466..4be29e6d04f3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -368,7 +368,7 @@ describe('alerts_list component with items', () => { it('sorts alerts when clicking the name column', async () => { await setup(); wrapper - .find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton') + .find('[data-test-subj="tableHeaderCell_name_1"] .euiTableHeaderButton') .first() .simulate('click'); @@ -386,6 +386,28 @@ describe('alerts_list component with items', () => { }) ); }); + + it('sorts alerts when clicking the enabled column', async () => { + await setup(); + wrapper + .find('[data-test-subj="tableHeaderCell_enabled_0"] .euiTableHeaderButton') + .first() + .simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAlerts).toHaveBeenLastCalledWith( + expect.objectContaining({ + sort: { + field: 'enabled', + direction: 'asc', + }, + }) + ); + }); }); describe('alerts_list component empty with show only capability', () => { 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 b48f6f45ce4b8..adf1a27d1ad66 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 @@ -28,12 +28,13 @@ import { EuiText, EuiToolTip, EuiTableSortingType, + EuiButtonIcon, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { isEmpty } from 'lodash'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; -import { AlertAdd } from '../../alert_form'; +import { AlertAdd, AlertEdit } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; @@ -44,6 +45,8 @@ import { loadAlerts, loadAlertAggregations, loadAlertTypes, + disableAlert, + enableAlert, deleteAlerts, } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; @@ -65,6 +68,7 @@ import './alerts_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { ManageLicenseModal } from './manage_license_modal'; import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; +import { RuleEnabledSwitch } from './rule_enabled_switch'; const ENTER_KEY = 13; @@ -102,6 +106,9 @@ export const AlertsList: React.FunctionComponent = () => { const [alertStatusesFilter, setAlertStatusesFilter] = useState([]); const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); const [dismissAlertErrors, setDismissAlertErrors] = useState(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [sort, setSort] = useState['sort']>({ field: 'name', direction: 'asc', @@ -131,6 +138,10 @@ export const AlertsList: React.FunctionComponent = () => { totalItemCount: 0, }); const [alertsToDelete, setAlertsToDelete] = useState([]); + const onRuleEdit = (ruleItem: AlertTableItem) => { + setEditFlyoutVisibility(true); + setCurrentRuleToEdit(ruleItem); + }; useEffect(() => { loadAlertsData(); @@ -169,15 +180,14 @@ export const AlertsList: React.FunctionComponent = () => { (async () => { try { const result = await loadActionTypes({ http }); - setActionTypes( - result - .filter( - // TODO: Remove "DEFAULT_HIDDEN_ACTION_TYPES" when cases connector is available across Kibana. - // Issue: https://github.com/elastic/kibana/issues/82502. - ({ id }) => actionTypeRegistry.has(id) && !DEFAULT_HIDDEN_ACTION_TYPES.includes(id) - ) - .sort((a, b) => a.name.localeCompare(b.name)) - ); + const sortedResult = result + .filter( + // TODO: Remove "DEFAULT_HIDDEN_ACTION_TYPES" when cases connector is available across Kibana. + // Issue: https://github.com/elastic/kibana/issues/82502. + ({ id }) => actionTypeRegistry.has(id) && !DEFAULT_HIDDEN_ACTION_TYPES.includes(id) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + setActionTypes(sortedResult); } catch (e) { toasts.addDanger({ title: i18n.translate( @@ -309,6 +319,26 @@ export const AlertsList: React.FunctionComponent = () => { }; const alertsTableColumns = [ + { + field: 'enabled', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle', + { defaultMessage: 'Enabled' } + ), + width: '90px', + render(_enabled: boolean | undefined, item: AlertTableItem) { + return ( + await disableAlert({ http, id: item.id })} + enableAlert={async () => await enableAlert({ http, id: item.id })} + item={item} + onAlertChanged={() => loadAlertsData()} + /> + ); + }, + sortable: true, + 'data-test-subj': 'alertsTableCell-enabled', + }, { field: 'name', name: i18n.translate( @@ -317,7 +347,7 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: true, truncateText: true, - width: '35%', + width: '30%', 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { const ruleType = alertTypesState.data.get(alert.alertTypeId); @@ -357,68 +387,129 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: true, truncateText: false, - width: '150px', + width: '120px', 'data-test-subj': 'alertsTableCell-status', - render: (executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { return renderAlertExecutionStatus(item.executionStatus, item); }, }, { - field: 'tagsText', + field: 'alertType', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText', - { defaultMessage: 'Tags' } + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle', + { defaultMessage: 'Type' } ), sortable: false, - 'data-test-subj': 'alertsTableCell-tagsText', - }, - { - field: 'actionsCount', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount', - { defaultMessage: 'Actions' } + truncateText: true, + render: (_count: number, item: AlertTableItem) => ( + {item.alertType} ), - render: (count: number, item: AlertTableItem) => { - return ( - - {count} - - ); - }, - sortable: false, - 'data-test-subj': 'alertsTableCell-actionsCount', + 'data-test-subj': 'alertsTableCell-alertType', }, { - field: 'alertType', + field: 'tagsText', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle', - { defaultMessage: 'Type' } + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText', + { defaultMessage: 'Tags' } ), sortable: false, - truncateText: true, - 'data-test-subj': 'alertsTableCell-alertType', + 'data-test-subj': 'alertsTableCell-tagsText', + render: (_count: number, item: AlertTableItem) => ( +
+ {item.tagsText} +
+ ), }, { field: 'schedule.interval', + width: '6%', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle', + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle', { defaultMessage: 'Runs every' } ), sortable: false, truncateText: false, 'data-test-subj': 'alertsTableCell-interval', }, + { + width: '9%', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle', + { defaultMessage: 'Actions' } + ), + render: (item: AlertTableItem) => { + return ( + + {item.actionsCount} + +
+ {item.muteAll ? ( + + + + ) : null} +
+
+
+ ); + }, + 'data-test-subj': 'alertsTableCell-actions', + }, { name: '', - width: '40px', + width: '10%', render(item: AlertTableItem) { return ( - loadAlertsData()} - setAlertsToDelete={setAlertsToDelete} - /> + + + + + onRuleEdit(item)} + iconType={'pencil'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } + )} + /> + + + setAlertsToDelete([item.id])} + iconType={'trash'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel', + { defaultMessage: 'Delete' } + )} + /> + + + + + loadAlertsData()} + setAlertsToDelete={setAlertsToDelete} + onEditAlert={() => onRuleEdit(item)} + /> + + ); }, }, @@ -621,7 +712,7 @@ export const AlertsList: React.FunctionComponent = () => {
- + { - + { onSave={loadAlertsData} /> )} + {editFlyoutVisible && currentRuleToEdit && ( + { + setEditFlyoutVisibility(false); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + onSave={loadAlertsData} + /> + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss index 774d1566f2a63..9ebd43e107e27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.scss @@ -4,20 +4,6 @@ } } -.actCollapsedItemActions__item { - padding: $euiSizeM; -} - -.actCollapsedItemActions__delete { - display: flex; - - .actCollapsedItemActions__deleteIcon { - width: $euiSwitchWidthCompressed; - text-align: center; - } - - .actCollapsedItemActions__deleteLabel { - padding-left: $euiSizeS; - padding-top: $euiSizeXS * .5; - } +button[data-test-subj='deleteAlert'] { + color: $euiColorDanger; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index 43fdb9fe819e7..b4bf4e786bca3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -8,18 +8,7 @@ import { i18n } from '@kbn/i18n'; import { asyncScheduler } from 'rxjs'; import React, { useEffect, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonIcon, - EuiPopover, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiSwitch, - EuiHorizontalRule, - EuiText, - EuiSpacer, - EuiIcon, -} from '@elastic/eui'; +import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; import { AlertTableItem } from '../../../../types'; import { @@ -32,6 +21,7 @@ export type ComponentOpts = { item: AlertTableItem; onAlertChanged: () => void; setAlertsToDelete: React.Dispatch>; + onEditAlert: (item: AlertTableItem) => void; } & BulkOperationsComponentOpts; export const CollapsedItemActions: React.FunctionComponent = ({ @@ -42,6 +32,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ unmuteAlert, muteAlert, setAlertsToDelete, + onEditAlert, }: ComponentOpts) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isDisabled, setIsDisabled] = useState(!item.enabled); @@ -54,7 +45,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ const button = ( setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle', @@ -63,6 +54,91 @@ export const CollapsedItemActions: React.FunctionComponent = ({ /> ); + const panels = [ + { + id: 0, + hasFocus: false, + items: [ + { + disabled: !(item.isEditable && !isDisabled) || !item.enabledInLicense, + 'data-test-subj': 'muteButton', + onClick: async () => { + const muteAll = isMuted; + asyncScheduler.schedule(async () => { + if (muteAll) { + await unmuteAlert({ ...item, muteAll }); + } else { + await muteAlert({ ...item, muteAll }); + } + onAlertChanged(); + }, 10); + setIsMuted(!isMuted); + setIsPopoverOpen(!isPopoverOpen); + }, + name: isMuted + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.unmuteTitle', + { defaultMessage: 'Unmute' } + ) + : i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle', + { defaultMessage: 'Mute' } + ), + }, + { + disabled: !item.isEditable || !item.enabledInLicense, + 'data-test-subj': 'disableButton', + onClick: async () => { + const enabled = !isDisabled; + asyncScheduler.schedule(async () => { + if (enabled) { + await disableAlert({ ...item, enabled }); + } else { + await enableAlert({ ...item, enabled }); + } + onAlertChanged(); + }, 10); + setIsDisabled(!isDisabled); + setIsPopoverOpen(!isPopoverOpen); + }, + name: isDisabled + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle', + { defaultMessage: 'Enable' } + ) + : i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle', + { defaultMessage: 'Disable' } + ), + }, + { + disabled: !item.isEditable, + 'data-test-subj': 'editAlert', + onClick: () => { + setIsPopoverOpen(!isPopoverOpen); + onEditAlert(item); + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.editTitle', + { defaultMessage: 'Edit rule' } + ), + }, + { + disabled: !item.isEditable, + 'data-test-subj': 'deleteAlert', + onClick: () => { + setIsPopoverOpen(!isPopoverOpen); + setAlertsToDelete([item.id]); + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteRuleTitle', + { defaultMessage: 'Delete rule' } + ), + }, + ], + }, + ]; + return ( = ({ panelPaddingSize="none" data-test-subj="collapsedItemActions" > - -
- { - const enabled = !isDisabled; - asyncScheduler.schedule(async () => { - if (enabled) { - await disableAlert({ ...item, enabled }); - } else { - await enableAlert({ ...item, enabled }); - } - onAlertChanged(); - }, 10); - setIsDisabled(!isDisabled); - }} - label={ - - } - /> - - - - -
-
- { - const muteAll = isMuted; - asyncScheduler.schedule(async () => { - if (muteAll) { - await unmuteAlert({ ...item, muteAll }); - } else { - await muteAlert({ ...item, muteAll }); - } - onAlertChanged(); - }, 10); - setIsMuted(!isMuted); - }} - label={ - - } - /> - - - - -
- - setAlertsToDelete([item.id])} - > -
-
- -
-
- -

- -

-
-
-
-
-
+
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx new file mode 100644 index 0000000000000..c031f189ffa4d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { asyncScheduler } from 'rxjs'; +import React, { useEffect, useState } from 'react'; +import { EuiSwitch } from '@elastic/eui'; + +import { Alert, AlertTableItem } from '../../../../types'; + +interface ComponentOpts { + item: AlertTableItem; + onAlertChanged: () => void; + enableAlert: (alert: Alert) => Promise; + disableAlert: (alert: Alert) => Promise; +} + +export const RuleEnabledSwitch: React.FunctionComponent = ({ + item, + onAlertChanged, + disableAlert, + enableAlert, +}: ComponentOpts) => { + const [isEnabled, setIsEnabled] = useState(!item.enabled); + useEffect(() => { + setIsEnabled(item.enabled); + }, [item.enabled]); + + return ( + { + const enabled = isEnabled; + asyncScheduler.schedule(async () => { + if (enabled) { + await disableAlert({ ...item, enabled }); + } else { + await enableAlert({ ...item, enabled }); + } + onAlertChanged(); + }, 10); + setIsEnabled(!isEnabled); + }} + label="" + /> + ); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 79eb341f9bbfe..082a2ca3bb42a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -177,11 +177,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); + await testSubjects.click('disableButton'); await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( createdAlert.name, - 'disableSwitch', + 'enableSwitch', 'true' ); }); @@ -194,10 +194,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); + await testSubjects.click('disableButton'); await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( createdAlert.name, - 'disableSwitch', + 'enableSwitch', 'false' ); }); @@ -209,12 +209,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( - createdAlert.name, - 'muteSwitch', - 'true' - ); + await testSubjects.click('muteButton'); + + await retry.tryForTime(30000, async () => { + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + const muteBadge = await testSubjects.find('mutedActionsBadge'); + expect(await muteBadge.isDisplayed()).to.eql(true); + }); }); it('should unmute single alert', async () => { @@ -226,12 +227,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); - await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( - createdAlert.name, - 'muteSwitch', - 'false' - ); + await testSubjects.click('muteButton'); + await retry.tryForTime(30000, async () => { + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + await testSubjects.missingOrFail('mutedActionsBadge'); + }); }); it('should delete single alert', async () => { @@ -274,11 +274,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await testSubjects.click('collapsedItemActions'); - - const muteSwitch = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await retry.tryForTime(30000, async () => { + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + const muteBadge = await testSubjects.find('mutedActionsBadge'); + expect(await muteBadge.isDisplayed()).to.eql(true); + }); }); it('should unmute all selection', async () => { @@ -297,13 +297,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Mute all button shows after clicking unmute all await testSubjects.existOrFail('muteAll'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const muteSwitch = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await retry.tryForTime(30000, async () => { + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + await testSubjects.missingOrFail('mutedActionsBadge'); + }); }); it('should disable all selection', async () => { @@ -320,13 +317,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Enable all button shows after clicking disable all await testSubjects.existOrFail('enableAll'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitch = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'enableSwitch', + 'false' + ); }); it('should enable all selection', async () => { @@ -345,13 +340,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Disable all button shows after clicking enable all await testSubjects.existOrFail('disableAll'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitch = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'enableSwitch', + 'true' + ); }); it('should delete all selection', async () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 8cf92f77d939c..0b8bb38010fc1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -146,23 +146,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should disable the alert', async () => { - const disableSwitch = await testSubjects.find('disableSwitch'); + const enableSwitch = await testSubjects.find('enableSwitch'); - const isChecked = await disableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); - await disableSwitch.click(); + await enableSwitch.click(); - const disableSwitchAfterDisabling = await testSubjects.find('disableSwitch'); + const disableSwitchAfterDisabling = await testSubjects.find('enableSwitch'); const isCheckedAfterDisabling = await disableSwitchAfterDisabling.getAttribute( 'aria-checked' ); - expect(isCheckedAfterDisabling).to.eql('true'); + expect(isCheckedAfterDisabling).to.eql('false'); }); it('shouldnt allow you to mute a disabled alert', async () => { - const disabledDisableSwitch = await testSubjects.find('disableSwitch'); - expect(await disabledDisableSwitch.getAttribute('aria-checked')).to.eql('true'); + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); const muteSwitch = await testSubjects.find('muteSwitch'); expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); @@ -177,18 +177,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should reenable a disabled the alert', async () => { - const disableSwitch = await testSubjects.find('disableSwitch'); + const enableSwitch = await testSubjects.find('enableSwitch'); - const isChecked = await disableSwitch.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); - await disableSwitch.click(); + await enableSwitch.click(); - const disableSwitchAfterReenabling = await testSubjects.find('disableSwitch'); + const disableSwitchAfterReenabling = await testSubjects.find('enableSwitch'); const isCheckedAfterDisabling = await disableSwitchAfterReenabling.getAttribute( 'aria-checked' ); - expect(isCheckedAfterDisabling).to.eql('false'); + expect(isCheckedAfterDisabling).to.eql('true'); }); it('should mute the alert', async () => { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 8eeabf1f5d670..890315698f74c 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -190,8 +190,6 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) ) { await retry.tryForTime(30000, async () => { await this.searchAlerts(ruleName); - await testSubjects.click('collapsedItemActions'); - const switchControl = await testSubjects.find(switchName); const isChecked = await switchControl.getAttribute('aria-checked'); expect(isChecked).to.eql(shouldBeCheckedAsString);