[RAM] Clarifies function of snooze, hides notify/snooze for SIEM rules (#133641)

* Address design feedback, remove mute from dropdown, and hide notify for siem rules

* Hide badge if rule is not enabled

* Address design feedback

* Fix button size and add tests

* Fix tests and make the notify badge shareable

* Fix tests and remove unused translations

* Add toast messages to snooze actions

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2022-06-13 11:03:27 -07:00 committed by GitHub
parent ef9ab9acdd
commit 363c813e49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 724 additions and 166 deletions

View file

@ -29105,9 +29105,7 @@
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle": "Désactiver",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle": "Modifier la règle",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle": "Activer",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "Muet",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "Actions",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "Réactiver le son",
"xpack.triggersActionsUI.sections.rulesList.daysLabel": "jours",
"xpack.triggersActionsUI.sections.rulesList.disabledRuleStatus": "Désactivé",
"xpack.triggersActionsUI.sections.rulesList.enabledRuleStatus": "Activé",

View file

@ -29268,9 +29268,7 @@
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle": "無効にする",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle": "ルールを編集",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle": "有効にする",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "ミュート",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "アクション",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "ミュート解除",
"xpack.triggersActionsUI.sections.rulesList.daysLabel": "日",
"xpack.triggersActionsUI.sections.rulesList.disabledRuleStatus": "無効",
"xpack.triggersActionsUI.sections.rulesList.enabledRuleStatus": "有効",

View file

@ -29301,9 +29301,7 @@
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.disableTitle": "禁用",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.editTitle": "编辑规则",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.enableTitle": "启用",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle": "静音",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle": "操作",
"xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle": "取消静音",
"xpack.triggersActionsUI.sections.rulesList.daysLabel": "天",
"xpack.triggersActionsUI.sections.rulesList.disabledRuleStatus": "已禁用",
"xpack.triggersActionsUI.sections.rulesList.enabledRuleStatus": "已启用",

View file

@ -0,0 +1,63 @@
/*
* 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 React, { useState } from 'react';
import { RuleTableItem } from '../../../types';
import { getRulesListNotifyBadgeLazy } from '../../../common/get_rules_list_notify_badge';
const mockRule: RuleTableItem = {
id: '1',
enabled: true,
name: 'test rule',
tags: ['tag1'],
ruleTypeId: 'test_rule_type',
consumer: 'rules',
schedule: { interval: '5d' },
actions: [
{ id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } },
],
params: { name: 'test rule type name' },
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: '1m',
notifyWhen: 'onActiveAlert',
muteAll: true,
mutedInstanceIds: [],
executionStatus: {
status: 'active',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
actionsCount: 1,
index: 0,
ruleType: 'Test Rule Type',
isEditable: true,
enabledInLicense: true,
};
export const RulesListNotifyBadgeSandbox = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<div style={{ flex: 1 }}>
{getRulesListNotifyBadgeLazy({
rule: mockRule,
isOpen,
isLoading,
onClick: () => setIsOpen(!isOpen),
onClose: () => setIsOpen(false),
onLoading: setIsLoading,
onRuleChanged: () => Promise.resolve(),
snoozeRule: () => Promise.resolve(),
unsnoozeRule: () => Promise.resolve(),
})}
</div>
);
};

View file

@ -11,6 +11,7 @@ import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox';
import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox';
import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox';
import { RuleEventLogListSandbox } from './rule_event_log_list_sandbox';
import { RulesListNotifyBadgeSandbox } from './rules_list_notify_badge_sandbox';
import { RulesListSandbox } from './rules_list_sandbox';
export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
@ -22,6 +23,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
<RuleTagBadgeSandbox />
<RulesListSandbox />
<RuleEventLogListSandbox />
<RulesListNotifyBadgeSandbox />
</>
);
};

View file

@ -47,3 +47,6 @@ export const RuleEventLogList = suspendedComponentWithProps(
export const RulesList = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rules_list'))
);
export const RulesListNotifyBadge = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rules_list_notify_badge'))
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import * as React from 'react';
import moment from 'moment';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { CollapsedItemActions } from './collapsed_item_actions';
import { act } from 'react-dom/test-utils';
@ -19,9 +19,10 @@ const onEditRule = jest.fn();
const setRulesToDelete = jest.fn();
const disableRule = jest.fn();
const enableRule = jest.fn();
const unmuteRule = jest.fn();
const muteRule = jest.fn();
const onUpdateAPIKey = jest.fn();
const snoozeRule = jest.fn();
const unsnoozeRule = jest.fn();
const onLoading = jest.fn();
export const tick = (ms = 0) =>
new Promise((resolve) => {
@ -90,9 +91,10 @@ describe('CollapsedItemActions', () => {
setRulesToDelete,
disableRule,
enableRule,
unmuteRule,
muteRule,
onUpdateAPIKey,
snoozeRule,
unsnoozeRule,
onLoading,
};
};
@ -116,7 +118,7 @@ describe('CollapsedItemActions', () => {
expect(wrapper.find('[data-test-subj="selectActionButton"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="snoozeButton"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeFalsy();
@ -129,7 +131,7 @@ describe('CollapsedItemActions', () => {
});
expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="snoozeButton"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeTruthy();
@ -139,10 +141,9 @@ describe('CollapsedItemActions', () => {
wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled
).toBeFalsy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute');
expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable');
expect(wrapper.find(`[data-test-subj="snoozeButton"] button`).text()).toEqual('Snooze');
expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule');
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy();
@ -150,22 +151,6 @@ describe('CollapsedItemActions', () => {
expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update API key');
});
test('handles case when rule is unmuted and enabled and mute is clicked', async () => {
await setup();
const wrapper = mountWithIntl(<CollapsedItemActions {...getPropsWithRule()} />);
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
wrapper.find('button[data-test-subj="muteButton"]').simulate('click');
await act(async () => {
await tick(10);
wrapper.update();
});
expect(muteRule).toHaveBeenCalled();
});
test('handles case when rule is unmuted and enabled and disable is clicked', async () => {
await setup();
const wrapper = mountWithIntl(<CollapsedItemActions {...getPropsWithRule()} />);
@ -182,24 +167,6 @@ describe('CollapsedItemActions', () => {
expect(disableRule).toHaveBeenCalled();
});
test('handles case when rule is muted and enabled and unmute is clicked', async () => {
await setup();
const wrapper = mountWithIntl(
<CollapsedItemActions {...getPropsWithRule({ muteAll: true })} />
);
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
wrapper.find('button[data-test-subj="muteButton"]').simulate('click');
await act(async () => {
await tick(10);
wrapper.update();
});
expect(unmuteRule).toHaveBeenCalled();
});
test('handles case when rule is unmuted and disabled and enable is clicked', async () => {
await setup();
const wrapper = mountWithIntl(
@ -261,8 +228,7 @@ describe('CollapsedItemActions', () => {
wrapper.update();
});
expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute');
expect(wrapper.find(`[data-test-subj="snoozeButton"] button`).exists()).toBeFalsy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Enable');
expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy();
@ -298,8 +264,7 @@ describe('CollapsedItemActions', () => {
wrapper.update();
});
expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute');
expect(wrapper.find(`[data-test-subj="snoozeButton"] button`).prop('disabled')).toBeTruthy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeTruthy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable');
expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy();
@ -319,8 +284,9 @@ describe('CollapsedItemActions', () => {
wrapper.update();
});
expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Unmute');
expect(wrapper.find('[data-test-subj="snoozeButton"] button').text()).toEqual(
'Snoozed indefinitely'
);
expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable');
expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeFalsy();
@ -338,8 +304,6 @@ describe('CollapsedItemActions', () => {
wrapper.update();
});
expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute');
expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable');
expect(wrapper.find(`[data-test-subj="editRule"] button`).prop('disabled')).toBeTruthy();
@ -347,4 +311,34 @@ describe('CollapsedItemActions', () => {
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy();
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule');
});
test('renders snooze text correctly if the rule is snoozed', async () => {
jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
await setup();
const wrapper = mountWithIntl(
<CollapsedItemActions
{...getPropsWithRule({ isSnoozedUntil: moment('1990-02-01').format() })}
/>
);
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
await act(async () => {
jest.runOnlyPendingTimers();
});
expect(wrapper.find('[data-test-subj="snoozeButton"] button').text()).toEqual(
'Snoozed until Feb 1'
);
});
test('snooze is disabled for SIEM rules', async () => {
await setup();
const wrapper = mountWithIntl(
<CollapsedItemActions
{...getPropsWithRule({
consumer: 'siem',
})}
/>
);
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
expect(wrapper.find('[data-test-subj="snoozeButton"]').exists()).toBeFalsy();
});
});

View file

@ -6,9 +6,19 @@
*/
import { i18n } from '@kbn/i18n';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { asyncScheduler } from 'rxjs';
import React, { useEffect, useState } from 'react';
import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiPopover,
EuiContextMenu,
EuiPanel,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { useKibana } from '../../../../common/lib/kibana';
import { RuleTableItem } from '../../../../types';
@ -16,36 +26,53 @@ import {
ComponentOpts as BulkOperationsComponentOpts,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { RulesListSnoozePanel } from './rules_list_snooze_panel';
import { isRuleSnoozed } from './rule_status_dropdown';
import './collapsed_item_actions.scss';
export type ComponentOpts = {
item: RuleTableItem;
onRuleChanged: () => void;
onRuleChanged: () => Promise<void>;
onLoading: (isLoading: boolean) => void;
setRulesToDelete: React.Dispatch<React.SetStateAction<string[]>>;
onEditRule: (item: RuleTableItem) => void;
onUpdateAPIKey: (id: string[]) => void;
} & Pick<BulkOperationsComponentOpts, 'disableRule' | 'enableRule' | 'unmuteRule' | 'muteRule'>;
} & Pick<BulkOperationsComponentOpts, 'disableRule' | 'enableRule' | 'snoozeRule' | 'unsnoozeRule'>;
export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
item,
onLoading,
onRuleChanged,
disableRule,
enableRule,
unmuteRule,
muteRule,
setRulesToDelete,
onEditRule,
onUpdateAPIKey,
snoozeRule,
unsnoozeRule,
}: ComponentOpts) => {
const { ruleTypeRegistry } = useKibana().services;
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [isDisabled, setIsDisabled] = useState<boolean>(!item.enabled);
const [isMuted, setIsMuted] = useState<boolean>(item.muteAll);
useEffect(() => {
setIsDisabled(!item.enabled);
setIsMuted(item.muteAll);
}, [item.enabled, item.muteAll]);
}, [item.enabled]);
const snoozeRuleInternal = useCallback(
async (snoozeEndTime: string | -1, interval: string | null) => {
await snoozeRule(item, snoozeEndTime);
},
[snoozeRule, item]
);
const unsnoozeRuleInternal = useCallback(async () => {
await unsnoozeRule(item);
}, [unsnoozeRule, item]);
const onClose = useCallback(() => {
setIsPopoverOpen(false);
}, [setIsPopoverOpen]);
const isRuleTypeEditableInContext = ruleTypeRegistry.has(item.ruleTypeId)
? !ruleTypeRegistry.get(item.ruleTypeId).requiresAppContext
@ -65,36 +92,62 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
/>
);
const isSnoozed = useMemo(() => {
return isRuleSnoozed(item);
}, [item]);
const snoozedButtonText = useMemo(() => {
if (item.muteAll) {
return i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.snoozedIndefinitely',
{
defaultMessage: 'Snoozed indefinitely',
}
);
}
if (isSnoozed) {
return i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.snoozedUntil',
{
defaultMessage: 'Snoozed until {snoozeTime}',
values: {
snoozeTime: moment(item.isSnoozedUntil).format('MMM D'),
},
}
);
}
return i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.snooze',
{
defaultMessage: 'Snooze',
}
);
}, [isSnoozed, item]);
const snoozePanelItem = useMemo(() => {
if (isDisabled || item.consumer === AlertConsumers.SIEM) {
return [];
}
return [
{
disabled: !item.isEditable || !item.enabledInLicense,
'data-test-subj': 'snoozeButton',
icon: 'bellSlash',
name: snoozedButtonText,
panel: 1,
},
];
}, [isDisabled, item, snoozedButtonText]);
const panels = [
{
id: 0,
hasFocus: false,
items: [
...snoozePanelItem,
{
disabled: !(item.isEditable && !isDisabled) || !item.enabledInLicense,
'data-test-subj': 'muteButton',
onClick: async () => {
const muteAll = isMuted;
asyncScheduler.schedule(async () => {
if (muteAll) {
await unmuteRule({ ...item, muteAll });
} else {
await muteRule({ ...item, muteAll });
}
onRuleChanged();
}, 10);
setIsMuted(!isMuted);
setIsPopoverOpen(!isPopoverOpen);
},
name: isMuted
? i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.unmuteTitle',
{ defaultMessage: 'Unmute' }
)
: i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.muteTitle',
{ defaultMessage: 'Mute' }
),
isSeparator: true as const,
},
{
disabled: !item.isEditable || !item.enabledInLicense,
@ -161,6 +214,35 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
},
],
},
{
id: 1,
title: (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="bellSlash" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.snoozeActions',
{ defaultMessage: 'Snooze actions' }
)}
</EuiFlexItem>
</EuiFlexGroup>
),
width: 500,
content: (
<EuiPanel>
<RulesListSnoozePanel
rule={item}
onClose={onClose}
onLoading={onLoading}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRuleInternal}
unsnoozeRule={unsnoozeRuleInternal}
/>
</EuiPanel>
),
},
];
return (

View file

@ -998,7 +998,7 @@ describe('rules_list component with items', () => {
const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities');
hasExecuteActionsCapability.mockReturnValue(false);
await setup();
expect(wrapper.find('.euiButtonIcon-isDisabled').length).toEqual(5);
expect(wrapper.find('.euiButtonIcon-isDisabled').length).toEqual(8);
hasExecuteActionsCapability.mockReturnValue(true);
});
});
@ -1156,6 +1156,7 @@ describe('rules_list with show only capability', () => {
expect(wrapper.find('EuiBasicTable')).toHaveLength(1);
expect(wrapper.find('EuiTableRow')).toHaveLength(2);
expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0);
hasAllPrivilege.mockReturnValue(true);
});
it('renders table of rules with actions menu collapsedItemActions', async () => {
@ -1286,7 +1287,6 @@ describe('rules_list with disabled items', () => {
it('clicking the notify badge shows the snooze panel', async () => {
await setup();
expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeFalsy();
wrapper
@ -1296,7 +1296,7 @@ describe('rules_list with disabled items', () => {
expect(wrapper.find('[data-test-subj="rulesListNotifyBadge"]').exists()).toBeTruthy();
wrapper.find('[data-test-subj="rulesListNotifyBadge"]').first().simulate('click');
wrapper.find('[data-test-subj="rulesListNotifyBadge-unsnoozed"]').first().simulate('click');
expect(wrapper.find('[data-test-subj="snoozePanel"]').exists()).toBeTruthy();
});

View file

@ -541,7 +541,7 @@ export const RulesList: React.FunctionComponent = () => {
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiFlexGroup alignItems="baseline" gutterSize="none">
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued" data-test-subj="totalRulesCount">
<FormattedMessage
@ -681,10 +681,11 @@ export const RulesList: React.FunctionComponent = () => {
onEnableRule={onEnableRule}
onSnoozeRule={onSnoozeRule}
onUnsnoozeRule={onUnsnoozeRule}
renderCollapsedItemActions={(rule) => (
renderCollapsedItemActions={(rule, onLoading) => (
<CollapsedItemActions
key={rule.id}
item={rule}
onLoading={onLoading}
onRuleChanged={() => loadData()}
setRulesToDelete={setRulesToDelete}
onEditRule={() => onRuleEdit(rule)}

View file

@ -0,0 +1,178 @@
/*
* 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 React from 'react';
import moment from 'moment';
import { EuiButtonIcon, EuiButton } from '@elastic/eui';
import { RuleTableItem } from '../../../../types';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { RulesListNotifyBadge } from './rules_list_notify_badge';
import { RulesListSnoozePanel } from './rules_list_snooze_panel';
import { act } from 'react-dom/test-utils';
jest.mock('../../../../common/lib/kibana');
const onClick = jest.fn();
const onClose = jest.fn();
const onLoading = jest.fn();
const onRuleChanged = jest.fn();
const snoozeRule = jest.fn();
const unsnoozeRule = jest.fn();
const getRule = (overrides = {}): RuleTableItem => ({
id: '1',
enabled: true,
name: 'test rule',
tags: ['tag1'],
ruleTypeId: 'test_rule_type',
consumer: 'rules',
schedule: { interval: '5d' },
actions: [
{ id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } },
],
params: { name: 'test rule type name' },
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: '1m',
notifyWhen: 'onActiveAlert',
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'active',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
actionsCount: 1,
index: 0,
ruleType: 'Test Rule Type',
isEditable: true,
enabledInLicense: true,
...overrides,
});
describe('RulesListNotifyBadge', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders the notify badge correctly', async () => {
jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
isSnoozedUntil: null,
muteAll: false,
})}
isLoading={false}
isOpen={false}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Rule without snooze
const badge = wrapper.find(EuiButtonIcon);
expect(badge.first().props().iconType).toEqual('bell');
// Rule with snooze
wrapper.setProps({
rule: getRule({
isSnoozedUntil: moment('1990-02-01').format(),
}),
});
const snoozeBadge = wrapper.find(EuiButton);
expect(snoozeBadge.first().props().iconType).toEqual('bellSlash');
expect(snoozeBadge.text()).toEqual('Feb 1');
// Rule with indefinite snooze
wrapper.setProps({
rule: getRule({
isSnoozedUntil: moment('1990-02-01').format(),
muteAll: true,
}),
});
const indefiniteSnoozeBadge = wrapper.find(EuiButtonIcon);
expect(indefiniteSnoozeBadge.first().props().iconType).toEqual('bellSlash');
expect(indefiniteSnoozeBadge.text()).toEqual('');
});
it('should allow the user to snooze rules', async () => {
jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
isSnoozedUntil: null,
muteAll: false,
})}
isLoading={false}
isOpen={true}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Snooze for 1 hour
wrapper
.find(RulesListSnoozePanel)
.find({ children: '1 hour' })
.find('button')
.first()
.simulate('click');
expect(onLoading).toHaveBeenCalledWith(true);
expect(onClose).toHaveBeenCalled();
expect(snoozeRule).toHaveBeenCalledWith(expect.stringContaining('1990'), '1h');
await act(async () => {
jest.runOnlyPendingTimers();
});
expect(onRuleChanged).toHaveBeenCalled();
expect(onLoading).toHaveBeenCalledWith(false);
});
it('should allow the user to unsnooze rules', async () => {
jest.useFakeTimers('modern').setSystemTime(moment('1990-01-01').toDate());
const wrapper = mountWithIntl(
<RulesListNotifyBadge
rule={getRule({
muteAll: true,
})}
isLoading={false}
isOpen={true}
onLoading={onLoading}
onClick={onClick}
onClose={onClose}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
);
// Unsnooze
wrapper.find('[data-test-subj="ruleSnoozeCancel"] button').simulate('click');
expect(onLoading).toHaveBeenCalledWith(true);
expect(onClose).toHaveBeenCalled();
await act(async () => {
jest.runOnlyPendingTimers();
});
expect(unsnoozeRule).toHaveBeenCalled();
expect(onLoading).toHaveBeenCalledWith(false);
});
});

View file

@ -5,26 +5,33 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import moment from 'moment';
import { EuiButton, EuiButtonIcon, EuiPopover, EuiText, EuiToolTip } from '@elastic/eui';
import {
EuiButton,
EuiButtonIcon,
EuiPopover,
EuiText,
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopoverTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isRuleSnoozed } from './rule_status_dropdown';
import { RuleTableItem } from '../../../../types';
import {
SnoozePanel,
futureTimeToInterval,
usePreviousSnoozeInterval,
SnoozeUnit,
} from './rule_status_dropdown';
import { RulesListSnoozePanel } from './rules_list_snooze_panel';
export interface RulesListNotifyBadgeProps {
rule: RuleTableItem;
isOpen: boolean;
isLoading: boolean;
previousSnoozeInterval?: string | null;
onClick: React.MouseEventHandler<HTMLButtonElement>;
onClose: () => void;
onRuleChanged: () => void;
onLoading: (isLoading: boolean) => void;
onRuleChanged: () => Promise<void>;
snoozeRule: (snoozeEndTime: string | -1, interval: string | null) => Promise<void>;
unsnoozeRule: () => Promise<void>;
}
@ -36,23 +43,19 @@ const openSnoozePanelAriaLabel = i18n.translate(
export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeProps> = (props) => {
const {
isLoading = false,
rule,
isOpen,
previousSnoozeInterval: propsPreviousSnoozeInterval,
previousSnoozeInterval,
onClick,
onClose,
onLoading,
onRuleChanged,
snoozeRule,
unsnoozeRule,
} = props;
const { isSnoozedUntil, muteAll } = rule;
const [previousSnoozeInterval, setPreviousSnoozeInterval] = usePreviousSnoozeInterval(
propsPreviousSnoozeInterval
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { isSnoozedUntil, muteAll, isEditable } = rule;
const isSnoozedIndefinitely = muteAll;
@ -100,7 +103,11 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
const snoozedButton = useMemo(() => {
return (
<EuiButton
data-test-subj="rulesListNotifyBadge"
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
data-test-subj="rulesListNotifyBadge-snoozed"
aria-label={openSnoozePanelAriaLabel}
minWidth={85}
iconType="bellSlash"
color="accent"
@ -109,49 +116,58 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
<EuiText size="xs">{formattedSnoozeText}</EuiText>
</EuiButton>
);
}, [formattedSnoozeText, onClick]);
}, [formattedSnoozeText, isLoading, isEditable, onClick]);
const scheduledSnoozeButton = useMemo(() => {
// TODO: Implement scheduled snooze button
return (
<EuiButton
data-test-subj="rulesListNotifyBadge"
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
data-test-subj="rulesListNotifyBadge-scheduled"
minWidth={85}
iconType="calendar"
color="text"
aria-label={openSnoozePanelAriaLabel}
onClick={onClick}
>
<EuiText size="xs">{formattedSnoozeText}</EuiText>
</EuiButton>
);
}, [formattedSnoozeText, onClick]);
}, [formattedSnoozeText, isLoading, isEditable, onClick]);
const unsnoozedButton = useMemo(() => {
return (
<EuiButtonIcon
size="m"
data-test-subj="rulesListNotifyBadge"
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
display={isLoading ? 'base' : 'empty'}
data-test-subj="rulesListNotifyBadge-unsnoozed"
aria-label={openSnoozePanelAriaLabel}
className={isOpen ? '' : 'ruleSidebarItem__action'}
color="accent"
iconType="bellSlash"
className={isOpen || isLoading ? '' : 'ruleSidebarItem__action'}
iconType="bell"
onClick={onClick}
/>
);
}, [isOpen, onClick]);
}, [isOpen, isLoading, isEditable, onClick]);
const indefiniteSnoozeButton = useMemo(() => {
return (
<EuiButtonIcon
size="s"
isLoading={isLoading}
disabled={isLoading || !isEditable}
display="base"
size="m"
data-test-subj="rulesListNotifyBadge"
data-test-subj="rulesListNotifyBadge-snoozedIndefinitely"
aria-label={openSnoozePanelAriaLabel}
iconType="bellSlash"
color="accent"
onClick={onClick}
/>
);
}, [onClick]);
}, [isLoading, isEditable, onClick]);
const button = useMemo(() => {
if (isScheduled) {
@ -181,44 +197,38 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
return <EuiToolTip content={snoozeTooltipText}>{button}</EuiToolTip>;
}, [isOpen, button, snoozeTooltipText]);
const snoozeRuleAndStoreInterval = useCallback(
(newSnoozeEndTime: string | -1, interval: string | null) => {
if (interval) {
setPreviousSnoozeInterval(interval);
}
return snoozeRule(newSnoozeEndTime, interval);
},
[setPreviousSnoozeInterval, snoozeRule]
);
const onChangeSnooze = useCallback(
async (value: number, unit?: SnoozeUnit) => {
setIsLoading(true);
try {
if (value === -1) {
await snoozeRuleAndStoreInterval(-1, null);
} else if (value !== 0) {
const newSnoozeEndTime = moment().add(value, unit).toISOString();
await snoozeRuleAndStoreInterval(newSnoozeEndTime, `${value}${unit}`);
} else await unsnoozeRule();
onRuleChanged();
} finally {
onClose();
setIsLoading(false);
}
},
[onRuleChanged, onClose, snoozeRuleAndStoreInterval, unsnoozeRule, setIsLoading]
);
return (
<EuiPopover isOpen={isOpen} closePopover={onClose} button={buttonWithToolTip}>
<SnoozePanel
isLoading={isLoading}
applySnooze={onChangeSnooze}
interval={futureTimeToInterval(isSnoozedUntil)}
showCancel={isSnoozed}
<EuiPopover
data-test-subj="rulesListNotifyBadge"
isOpen={isOpen}
closePopover={onClose}
button={buttonWithToolTip}
>
<EuiPopoverTitle>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="bellSlash" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.snoozeActions',
{ defaultMessage: 'Snooze actions' }
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
<RulesListSnoozePanel
rule={rule}
onClose={onClose}
onLoading={onLoading}
previousSnoozeInterval={previousSnoozeInterval}
onRuleChanged={onRuleChanged}
snoozeRule={snoozeRule}
unsnoozeRule={unsnoozeRule}
/>
</EuiPopover>
);
};
// eslint-disable-next-line import/no-default-export
export { RulesListNotifyBadge as default };

View file

@ -0,0 +1,123 @@
/*
* 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 React, { useCallback, useMemo } from 'react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { RuleTableItem } from '../../../../types';
import { useKibana } from '../../../../common/lib/kibana';
import {
SnoozePanel,
futureTimeToInterval,
usePreviousSnoozeInterval,
SnoozeUnit,
isRuleSnoozed,
} from './rule_status_dropdown';
export const SNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess',
{
defaultMessage: 'Rule successfully snoozed',
}
);
export const UNSNOOZE_SUCCESS_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess',
{
defaultMessage: 'Rule successfully unsnoozed',
}
);
export const SNOOZE_FAILED_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed',
{
defaultMessage: 'Unabled to change rule snooze settings',
}
);
const EMPTY_HANDLER = () => {};
export interface RulesListSnoozePanelProps {
rule: RuleTableItem;
previousSnoozeInterval?: string | null;
onLoading?: (isLoading: boolean) => void;
onRuleChanged: () => Promise<void>;
onClose: () => void;
snoozeRule: (snoozeEndTime: string | -1, interval: string | null) => Promise<void>;
unsnoozeRule: () => Promise<void>;
}
export const RulesListSnoozePanel = (props: RulesListSnoozePanelProps) => {
const {
rule,
previousSnoozeInterval: propsPreviousSnoozeInterval,
onRuleChanged,
onClose,
snoozeRule,
unsnoozeRule,
onLoading = EMPTY_HANDLER,
} = props;
const {
notifications: { toasts },
} = useKibana().services;
const { isSnoozedUntil } = rule;
const [previousSnoozeInterval, setPreviousSnoozeInterval] = usePreviousSnoozeInterval(
propsPreviousSnoozeInterval
);
const isSnoozed = useMemo(() => {
return isRuleSnoozed(rule);
}, [rule]);
const snoozeRuleAndStoreInterval = useCallback(
(newSnoozeEndTime: string | -1, interval: string | null) => {
if (interval) {
setPreviousSnoozeInterval(interval);
}
return snoozeRule(newSnoozeEndTime, interval);
},
[setPreviousSnoozeInterval, snoozeRule]
);
const onChangeSnooze = useCallback(
async (value: number, unit?: SnoozeUnit) => {
onLoading(true);
onClose();
try {
if (value === -1) {
await snoozeRuleAndStoreInterval(-1, null);
toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE);
} else if (value !== 0) {
const newSnoozeEndTime = moment().add(value, unit).toISOString();
await snoozeRuleAndStoreInterval(newSnoozeEndTime, `${value}${unit}`);
toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE);
} else {
await unsnoozeRule();
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
}
} catch (e) {
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
} finally {
await onRuleChanged();
onLoading(false);
}
},
[toasts, onRuleChanged, snoozeRuleAndStoreInterval, unsnoozeRule, onLoading, onClose]
);
return (
<SnoozePanel
applySnooze={onChangeSnooze}
interval={futureTimeToInterval(isSnoozedUntil)}
showCancel={isSnoozed}
previousSnoozeInterval={previousSnoozeInterval}
/>
);
};

View file

@ -7,6 +7,7 @@
import React, { useMemo, useState } from 'react';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiBasicTable,
@ -112,12 +113,15 @@ export interface RulesListTableProps {
onTagClose?: (rule: RuleTableItem) => void;
onSelectionChange?: (updatedSelectedItemsList: RuleTableItem[]) => void;
onPercentileOptionsChange?: (options: EuiSelectableOption[]) => void;
onRuleChanged: () => void;
onRuleChanged: () => Promise<void>;
onEnableRule: (rule: RuleTableItem) => Promise<void>;
onDisableRule: (rule: RuleTableItem) => Promise<void>;
onSnoozeRule: (rule: RuleTableItem, snoozeEndTime: string | -1) => Promise<void>;
onUnsnoozeRule: (rule: RuleTableItem) => Promise<void>;
renderCollapsedItemActions?: (rule: RuleTableItem) => React.ReactNode;
renderCollapsedItemActions?: (
rule: RuleTableItem,
onLoading: (isLoading: boolean) => void
) => React.ReactNode;
renderRuleError?: (rule: RuleTableItem) => React.ReactNode;
}
@ -168,7 +172,7 @@ export const RulesListTable = (props: RulesListTableProps) => {
onManageLicenseClick = EMPTY_HANDLER,
onSelectionChange = EMPTY_HANDLER,
onPercentileOptionsChange = EMPTY_HANDLER,
onRuleChanged = EMPTY_HANDLER,
onRuleChanged,
onEnableRule = EMPTY_HANDLER,
onDisableRule = EMPTY_HANDLER,
onSnoozeRule = EMPTY_HANDLER,
@ -180,6 +184,8 @@ export const RulesListTable = (props: RulesListTableProps) => {
const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState<number>(-1);
const [currentlyOpenNotify, setCurrentlyOpenNotify] = useState<string>();
const [isLoadingMap, setIsLoadingMap] = useState<Record<string, boolean>>({});
const selectedPercentile = useMemo(() => {
const selectedOption = percentileOptions.find((option) => option.checked === 'on');
if (selectedOption) {
@ -187,6 +193,13 @@ export const RulesListTable = (props: RulesListTableProps) => {
}
}, [percentileOptions]);
const onLoading = (id: string, newIsLoading: boolean) => {
setIsLoadingMap((prevState) => ({
...prevState,
[id]: newIsLoading,
}));
};
const renderPercentileColumnName = () => {
return (
<span data-test-subj={`rulesTable-${selectedPercentile}ColumnName`}>
@ -399,13 +412,39 @@ export const RulesListTable = (props: RulesListTableProps) => {
},
},
{
name: 'Notify',
width: '16%',
name: (
<EuiToolTip
data-test-subj="rulesTableCell-notifyTooltip"
content={i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.notifyTooltip',
{
defaultMessage: 'Snooze notifications for a rule.',
}
)}
>
<span>
{i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.notifyTitle',
{
defaultMessage: 'Notify',
}
)}
&nbsp;
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
width: '14%',
'data-test-subj': 'rulesTableCell-rulesListNotify',
render: (rule: RuleTableItem) => {
if (rule.consumer === AlertConsumers.SIEM || !rule.enabled) {
return null;
}
return (
<RulesListNotifyBadge
rule={rule}
isLoading={!!isLoadingMap[rule.id]}
onLoading={(newIsLoading) => onLoading(rule.id, newIsLoading)}
isOpen={currentlyOpenNotify === rule.id}
onClick={() => setCurrentlyOpenNotify(rule.id)}
onClose={() => setCurrentlyOpenNotify('')}
@ -644,7 +683,11 @@ export const RulesListTable = (props: RulesListTableProps) => {
) : null}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>{renderCollapsedItemActions(rule)}</EuiFlexItem>
<EuiFlexItem grow={false}>
{renderCollapsedItemActions(rule, (newIsLoading) =>
onLoading(rule.id, newIsLoading)
)}
</EuiFlexItem>
</EuiFlexGroup>
);
},

View file

@ -0,0 +1,14 @@
/*
* 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 React from 'react';
import { RulesListNotifyBadge } from '../application/sections';
import type { RulesListNotifyBadgeProps } from '../application/sections/rules_list/components/rules_list_notify_badge';
export const getRulesListNotifyBadgeLazy = (props: RulesListNotifyBadgeProps) => {
return <RulesListNotifyBadge {...props} />;
};

View file

@ -32,6 +32,7 @@ import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
import { getRulesListLazy } from './common/get_rules_list';
import { getAlertsTableStateLazy } from './common/get_alerts_table_state';
import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge';
import { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state';
function createStartMock(): TriggersAndActionsUIPublicPluginStart {
@ -86,6 +87,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleEventLogList: (props) => {
return getRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props) => {
return getRulesListNotifyBadgeLazy(props);
},
getRulesList: () => {
return getRulesListLazy();
},

View file

@ -35,6 +35,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge';
import { getRulesListLazy } from './common/get_rules_list';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import {
@ -55,6 +56,7 @@ import type {
RuleStatusFilterProps,
RuleTagBadgeProps,
RuleEventLogListProps,
RulesListNotifyBadgeProps,
AlertsTableConfigurationRegistry,
} from './types';
import { TriggersActionsUiConfigType } from '../common/types';
@ -92,6 +94,9 @@ export interface TriggersAndActionsUIPublicPluginStart {
getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement<RuleStatusFilterProps>;
getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement<RuleTagBadgeProps>;
getRuleEventLogList: (props: RuleEventLogListProps) => ReactElement<RuleEventLogListProps>;
getRulesListNotifyBadge: (
props: RulesListNotifyBadgeProps
) => ReactElement<RulesListNotifyBadgeProps>;
getRulesList: () => ReactElement;
}
@ -281,6 +286,9 @@ export class Plugin
getRuleEventLogList: (props: RuleEventLogListProps) => {
return getRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props: RulesListNotifyBadgeProps) => {
return getRulesListNotifyBadgeLazy(props);
},
getRulesList: () => {
return getRulesListLazy();
},

View file

@ -50,6 +50,7 @@ import type { RuleTagFilterProps } from './application/sections/rules_list/compo
import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter';
import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge';
import type { RuleEventLogListProps } from './application/sections/rule_details/components/rule_event_log_list';
import type { RulesListNotifyBadgeProps } from './application/sections/rules_list/components/rules_list_notify_badge';
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
// so the `Params` is a black-box of Record<string, unknown>
@ -86,6 +87,7 @@ export type {
RuleStatusFilterProps,
RuleTagBadgeProps,
RuleEventLogListProps,
RulesListNotifyBadgeProps,
};
export type { ActionType, AsApiContract };
export {

View file

@ -752,5 +752,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(disabled).to.equal(null);
});
});
it('should allow rules to be snoozed using the right side dropdown', async () => {
const createdAlert = await createAlert({
supertest,
objectRemover,
});
await refreshAlertsList();
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
await testSubjects.click('collapsedItemActions');
await testSubjects.click('snoozeButton');
await testSubjects.click('ruleSnoozeApply');
await retry.try(async () => {
await testSubjects.missingOrFail('rulesListNotifyBadge-snoozed');
});
await refreshAlertsList();
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
await testSubjects.click('collapsedItemActions');
await testSubjects.click('snoozeButton');
await testSubjects.click('ruleSnoozeIndefiniteApply');
await retry.try(async () => {
await testSubjects.missingOrFail('rulesListNotifyBadge-snoozedIndefinitely');
});
await refreshAlertsList();
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
await testSubjects.click('collapsedItemActions');
await testSubjects.click('snoozeButton');
await testSubjects.click('ruleSnoozeCancel');
await retry.try(async () => {
await testSubjects.missingOrFail('rulesListNotifyBadge-unsnoozed');
});
});
});
};