[Alerting UI]Changed rules table to support visual indication for disabled and muted alerts (#104190)

* [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é <mikecote@users.noreply.github.com>

* 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é <mikecote@users.noreply.github.com>
This commit is contained in:
Yuliia Naumenko 2021-07-20 18:41:15 +03:00 committed by GitHub
parent 05c29efab5
commit d91c6d0cfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 412 additions and 257 deletions

View file

@ -22829,7 +22829,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": "編集",
@ -22883,10 +22882,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": "タグ",
@ -22908,10 +22905,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": "閉じる",

View file

@ -23185,7 +23185,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": "编辑",
@ -23239,10 +23238,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": "标签",
@ -23265,10 +23262,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": "关闭",

View file

@ -359,16 +359,16 @@ describe('disable button', () => {
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.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', () => {
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
)
.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();

View file

@ -219,10 +219,10 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
<EuiFlexGroup justifyContent="flexEnd" wrap responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<EuiSwitch
name="disable"
name="enable"
disabled={!canSaveAlert || !alertType.enabledInLicense}
checked={!isEnabled}
data-test-subj="disableSwitch"
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
if (isEnabled) {
setIsEnabled(false);
@ -237,8 +237,8 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle"
defaultMessage="Disable"
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle"
defaultMessage="Enable"
/>
}
/>

View file

@ -133,6 +133,7 @@ export const AlertEdit = ({
aria-labelledby="flyoutAlertEditTitle"
size="m"
maxWidth={620}
ownFocus={false}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s" data-test-subj="editAlertFlyoutTitle">

View file

@ -95,13 +95,13 @@ export const AlertStatusFilter: React.FunctionComponent<AlertStatusFilterProps>
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';
}

View file

@ -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 */
}
}

View file

@ -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', () => {

View file

@ -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<string[]>([]);
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
const [dismissAlertErrors, setDismissAlertErrors] = useState<boolean>(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const [currentRuleToEdit, setCurrentRuleToEdit] = useState<AlertTableItem | null>(null);
const [sort, setSort] = useState<EuiTableSortingType<AlertTableItem>['sort']>({
field: 'name',
direction: 'asc',
@ -131,6 +138,10 @@ export const AlertsList: React.FunctionComponent = () => {
totalItemCount: 0,
});
const [alertsToDelete, setAlertsToDelete] = useState<string[]>([]);
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
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))
);
.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 (
<RuleEnabledSwitch
disableAlert={async () => 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,37 +387,12 @@ 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',
name: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText',
{ defaultMessage: 'Tags' }
),
sortable: false,
'data-test-subj': 'alertsTableCell-tagsText',
},
{
field: 'actionsCount',
name: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount',
{ defaultMessage: 'Actions' }
),
render: (count: number, item: AlertTableItem) => {
return (
<EuiBadge color="hollow" key={item.id}>
{count}
</EuiBadge>
);
},
sortable: false,
'data-test-subj': 'alertsTableCell-actionsCount',
},
{
field: 'alertType',
name: i18n.translate(
@ -396,29 +401,115 @@ export const AlertsList: React.FunctionComponent = () => {
),
sortable: false,
truncateText: true,
render: (_count: number, item: AlertTableItem) => (
<EuiBadge color="default">{item.alertType}</EuiBadge>
),
'data-test-subj': 'alertsTableCell-alertType',
},
{
field: 'schedule.interval',
field: 'tagsText',
name: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle',
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText',
{ defaultMessage: 'Tags' }
),
sortable: false,
'data-test-subj': 'alertsTableCell-tagsText',
render: (_count: number, item: AlertTableItem) => (
<div className="eui-textTruncate" title={item.tagsText}>
{item.tagsText}
</div>
),
},
{
field: 'schedule.interval',
width: '6%',
name: i18n.translate(
'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 (
<EuiFlexGroup wrap responsive={false} gutterSize="s">
<EuiFlexItem grow={false}>{item.actionsCount}</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
{item.muteAll ? (
<EuiBadge data-test-subj="mutedActionsBadge" color="hollow">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.mutedBadge"
defaultMessage="Muted"
/>
</EuiBadge>
) : null}
</div>
</EuiFlexItem>
</EuiFlexGroup>
);
},
'data-test-subj': 'alertsTableCell-actions',
},
{
name: '',
width: '40px',
width: '10%',
render(item: AlertTableItem) {
return (
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem grow={false} className="alertSidebarItem">
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonIcon
color={'primary'}
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editButtonTooltip',
{ defaultMessage: 'Edit' }
)}
className="alertSidebarItem__action"
onClick={() => onRuleEdit(item)}
iconType={'pencil'}
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel',
{ defaultMessage: 'Edit' }
)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color={'danger'}
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip',
{ defaultMessage: 'Delete' }
)}
className="alertSidebarItem__action"
onClick={() => setAlertsToDelete([item.id])}
iconType={'trash'}
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel',
{ defaultMessage: 'Delete' }
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CollapsedItemActions
key={item.id}
item={item}
onAlertChanged={() => loadAlertsData()}
setAlertsToDelete={setAlertsToDelete}
onEditAlert={() => onRuleEdit(item)}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
},
},
@ -621,7 +712,7 @@ export const AlertsList: React.FunctionComponent = () => {
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiHealth color="primary" data-test-subj="totalActiveAlertsCount">
<EuiHealth color="success" data-test-subj="totalActiveAlertsCount">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription"
defaultMessage="Active: {totalStausesActive}"
@ -650,7 +741,7 @@ export const AlertsList: React.FunctionComponent = () => {
</EuiHealth>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiHealth color="success" data-test-subj="totalPendingAlertsCount">
<EuiHealth color="accent" data-test-subj="totalPendingAlertsCount">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription"
defaultMessage="Pending: {totalStausesPending}"
@ -802,6 +893,17 @@ export const AlertsList: React.FunctionComponent = () => {
onSave={loadAlertsData}
/>
)}
{editFlyoutVisible && currentRuleToEdit && (
<AlertEdit
initialAlert={currentRuleToEdit}
onClose={() => {
setEditFlyoutVisibility(false);
}}
actionTypeRegistry={actionTypeRegistry}
alertTypeRegistry={alertTypeRegistry}
onSave={loadAlertsData}
/>
)}
</section>
);
};

View file

@ -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;
}

View file

@ -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<React.SetStateAction<string[]>>;
onEditAlert: (item: AlertTableItem) => void;
} & BulkOperationsComponentOpts;
export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
@ -42,6 +32,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
unmuteAlert,
muteAlert,
setAlertsToDelete,
onEditAlert,
}: ComponentOpts) => {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [isDisabled, setIsDisabled] = useState<boolean>(!item.enabled);
@ -54,7 +45,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
const button = (
<EuiButtonIcon
disabled={!item.isEditable}
iconType="boxesVertical"
iconType="boxesHorizontal"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle',
@ -63,58 +54,15 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
/>
);
return (
<EuiPopover
button={button}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
ownFocus
panelPaddingSize="none"
data-test-subj="collapsedItemActions"
>
<EuiContextMenuPanel className="actCollapsedItemActions" hasFocus={false}>
<div className="actCollapsedItemActions__item">
<EuiSwitch
name="disable"
disabled={!item.isEditable || !item.enabledInLicense}
compressed
checked={isDisabled}
data-test-subj="disableSwitch"
onChange={async () => {
const enabled = !isDisabled;
asyncScheduler.schedule(async () => {
if (enabled) {
await disableAlert({ ...item, enabled });
} else {
await enableAlert({ ...item, enabled });
}
onAlertChanged();
}, 10);
setIsDisabled(!isDisabled);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableTitle"
defaultMessage="Disable"
/>
}
/>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.disableHelpText"
defaultMessage="When disabled, the rule is not checked."
/>
</EuiText>
</div>
<div className="actCollapsedItemActions__item">
<EuiSwitch
name="mute"
checked={isMuted}
disabled={!(item.isEditable && !isDisabled) || !item.enabledInLicense}
compressed
data-test-subj="muteSwitch"
onChange={async () => {
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) {
@ -125,45 +73,82 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
onAlertChanged();
}, 10);
setIsMuted(!isMuted);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle"
defaultMessage="Mute"
/>
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 });
}
/>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteHelpText"
defaultMessage="When muted, the rule is checked, but no action is performed."
/>
</EuiText>
</div>
<EuiHorizontalRule margin="none" />
<EuiContextMenuItem
disabled={!item.isEditable}
data-test-subj="deleteAlert"
onClick={() => setAlertsToDelete([item.id])}
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 (
<EuiPopover
button={button}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
ownFocus
panelPaddingSize="none"
data-test-subj="collapsedItemActions"
>
<div className="actCollapsedItemActions__delete">
<div className="actCollapsedItemActions__deleteIcon">
<EuiIcon color="danger" type="trash" />
</div>
<div className="actCollapsedItemActions__deleteLabel">
<EuiText size="s" color="danger">
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.deleteTitle"
defaultMessage="Delete"
/>
</p>
</EuiText>
</div>
</div>
</EuiContextMenuItem>
</EuiContextMenuPanel>
<EuiContextMenu initialPanelId={0} panels={panels} className="actCollapsedItemActions" />
</EuiPopover>
);
};

View file

@ -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<void>;
disableAlert: (alert: Alert) => Promise<void>;
}
export const RuleEnabledSwitch: React.FunctionComponent<ComponentOpts> = ({
item,
onAlertChanged,
disableAlert,
enableAlert,
}: ComponentOpts) => {
const [isEnabled, setIsEnabled] = useState<boolean>(!item.enabled);
useEffect(() => {
setIsEnabled(item.enabled);
}, [item.enabled]);
return (
<EuiSwitch
name="enable"
disabled={!item.isEditable || !item.enabledInLicense}
compressed
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
const enabled = isEnabled;
asyncScheduler.schedule(async () => {
if (enabled) {
await disableAlert({ ...item, enabled });
} else {
await enableAlert({ ...item, enabled });
}
onAlertChanged();
}, 10);
setIsEnabled(!isEnabled);
}}
label=""
/>
);
};

View file

@ -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 () => {
@ -273,11 +273,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 () => {
@ -296,13 +296,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// Mute all button shows after clicking unmute all
await testSubjects.existOrFail('muteAll');
await retry.tryForTime(30000, async () => {
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 testSubjects.missingOrFail('mutedActionsBadge');
});
});
it('should disable all selection', async () => {
@ -319,13 +316,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 () => {
@ -344,13 +339,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 () => {

View file

@ -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 () => {

View file

@ -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);