mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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>
(cherry picked from commit 363c813e49
)
Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
This commit is contained in:
parent
2095b6a113
commit
d131787716
19 changed files with 724 additions and 166 deletions
|
@ -29060,9 +29060,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é",
|
||||
|
|
|
@ -29225,9 +29225,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": "有効",
|
||||
|
|
|
@ -29258,9 +29258,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": "已启用",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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'))
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
)}
|
||||
|
||||
<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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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} />;
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue