mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM] Add snooze state UI to Rule Details page (#135146)
* [RAM] Add snooze state UI to Rule Details page * Remove disalbe/enable test * Move disabled/enabled tests to rule and rulestatuspanel * Remove test for snoozed dropdown display
This commit is contained in:
parent
66b161a3e0
commit
46d8e11f29
8 changed files with 562 additions and 350 deletions
|
@ -6,15 +6,15 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { RuleComponent, alertToListItem } from './rule';
|
||||
import { AlertListItem } from './types';
|
||||
import { RuleAlertList } from './rule_alert_list';
|
||||
import { Rule, RuleSummary, AlertStatus, RuleType } from '../../../../types';
|
||||
import { RuleSummary, AlertStatus, RuleType } from '../../../../types';
|
||||
import { ExecutionDurationChart } from '../../common/components/execution_duration_chart';
|
||||
import { mockRule } from './test_helpers';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
|
@ -403,6 +403,46 @@ describe('execution duration overview', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('disable/enable functionality', () => {
|
||||
it('should show that the rule is enabled', () => {
|
||||
const rule = mockRule();
|
||||
const ruleType = mockRuleType();
|
||||
const ruleSummary = mockRuleSummary();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleComponent
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={ruleSummary}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first();
|
||||
|
||||
expect(actionsElem.text()).toEqual('Enabled');
|
||||
});
|
||||
|
||||
it('should show that the rule is disabled', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: false,
|
||||
});
|
||||
const ruleType = mockRuleType();
|
||||
const ruleSummary = mockRuleSummary();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleComponent
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
ruleSummary={ruleSummary}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first();
|
||||
|
||||
expect(actionsElem.text()).toEqual('Disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabbed content', () => {
|
||||
it('tabbed content renders when the event log experiment is on', async () => {
|
||||
// Enable the event log experiment
|
||||
|
@ -461,34 +501,6 @@ describe('tabbed content', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function mockRule(overloads: Partial<Rule> = {}): Rule {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `rule-${uuid.v4()}`,
|
||||
tags: [],
|
||||
ruleTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
...overloads,
|
||||
};
|
||||
}
|
||||
|
||||
function mockRuleType(overloads: Partial<RuleType> = {}): RuleType {
|
||||
return {
|
||||
id: 'test.testRuleType',
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiHealth,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -16,10 +15,7 @@ import {
|
|||
EuiStat,
|
||||
EuiIconTip,
|
||||
EuiTabbedContent,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ActionGroup,
|
||||
RuleExecutionStatusErrorReasons,
|
||||
|
@ -45,6 +41,7 @@ import { ExecutionDurationChart } from '../../common/components/execution_durati
|
|||
import { AlertListItem } from './types';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
|
||||
import { RuleStatusPanelWithApi } from './rule_status_panel';
|
||||
|
||||
const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list'));
|
||||
const RuleErrorLogWithApi = lazy(() => import('./rule_error_log'));
|
||||
|
@ -120,7 +117,7 @@ export function RuleComponent({
|
|||
{
|
||||
id: EVENT_LOG_LIST_TAB,
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
|
||||
defaultMessage: 'Run history',
|
||||
defaultMessage: 'History',
|
||||
}),
|
||||
'data-test-subj': 'eventLogListTab',
|
||||
content: suspendedComponentWithProps(
|
||||
|
@ -161,50 +158,13 @@ export function RuleComponent({
|
|||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiPanel color="subdued" hasBorder={false}>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
justifyContent="spaceBetween"
|
||||
responsive={false}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj={`ruleStatus-${rule.executionStatus.status}`}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<EuiHealth
|
||||
data-test-subj={`ruleStatus-${rule.executionStatus.status}`}
|
||||
textSize="inherit"
|
||||
color={healthColor}
|
||||
>
|
||||
{statusMessage}
|
||||
</EuiHealth>
|
||||
}
|
||||
description={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription',
|
||||
{
|
||||
defaultMessage: `Last response`,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<p>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleLastExecutionUpdatedAt"
|
||||
defaultMessage="Updated"
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{moment(rule.executionStatus.lastExecutionDate).fromNow()}
|
||||
</EuiText>
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<RuleStatusPanelWithApi
|
||||
rule={rule}
|
||||
isEditable={!readOnly}
|
||||
healthColor={healthColor}
|
||||
statusMessage={statusMessage}
|
||||
requestRefresh={requestRefresh}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiPanel
|
||||
|
@ -248,7 +208,7 @@ export function RuleComponent({
|
|||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<EuiFlexItem grow={2}>
|
||||
<ExecutionDurationChart
|
||||
executionDuration={ruleSummary.executionDuration}
|
||||
numberOfExecutions={numberOfExecutions}
|
||||
|
|
|
@ -328,238 +328,6 @@ describe('rule_details', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('disable/enable functionality', () => {
|
||||
it('should show that the rule is enabled', () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
|
||||
);
|
||||
const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first();
|
||||
|
||||
expect(actionsElem.text()).toEqual('Enabled');
|
||||
});
|
||||
|
||||
it('should show that the rule is disabled', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: false,
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
|
||||
);
|
||||
const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first();
|
||||
|
||||
expect(actionsElem.text()).toEqual('Disabled');
|
||||
});
|
||||
|
||||
it('should disable the rule when picking disable in the dropdown', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
});
|
||||
const disableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
actionTypes={[]}
|
||||
{...mockRuleApis}
|
||||
disableRule={disableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(disableRule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if rule is already disable should do nothing when picking disable in the dropdown', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: false,
|
||||
});
|
||||
const disableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
actionTypes={[]}
|
||||
{...mockRuleApis}
|
||||
disableRule={disableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(disableRule).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should enable the rule when picking enable in the dropdown', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: false,
|
||||
});
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
actionTypes={[]}
|
||||
{...mockRuleApis}
|
||||
enableRule={enableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(0).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(enableRule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if rule is already enable should do nothing when picking enable in the dropdown', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
});
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
actionTypes={[]}
|
||||
{...mockRuleApis}
|
||||
enableRule={enableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(0).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(enableRule).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const disableRule = jest.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
});
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
actionTypes={[]}
|
||||
{...mockRuleApis}
|
||||
disableRule={disableRule}
|
||||
enableRule={enableRule}
|
||||
/>
|
||||
);
|
||||
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(disableRule).toHaveBeenCalled();
|
||||
expect(
|
||||
wrapper.find(
|
||||
'[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner'
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snooze functionality', () => {
|
||||
it('should render "Snooze Indefinitely" when rule is enabled and mute all', () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
muteAll: true,
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
expect(actionsElem.text()).toEqual('Snoozed');
|
||||
expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toEqual(
|
||||
'Indefinitely'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit button', () => {
|
||||
const actionTypes: ActionType[] = [
|
||||
{
|
||||
|
|
|
@ -44,7 +44,6 @@ import {
|
|||
ActionType,
|
||||
ActionConnector,
|
||||
TriggersActionsUiConfig,
|
||||
RuleTableItem,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
ComponentOpts as BulkOperationsComponentOpts,
|
||||
|
@ -62,7 +61,6 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { ruleReducer } from '../../rule_form/rule_reducer';
|
||||
import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api';
|
||||
import { triggersActionsUiConfig } from '../../../../common/lib/config_api';
|
||||
import { RuleStatusDropdown } from '../../rules_list/components/rule_status_dropdown';
|
||||
|
||||
export type RuleDetailsProps = {
|
||||
rule: Rule;
|
||||
|
@ -311,34 +309,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
}
|
||||
description={
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.stateTitle"
|
||||
defaultMessage="State"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<RuleStatusDropdown
|
||||
disableRule={async () => await disableRule(rule)}
|
||||
enableRule={async () => await enableRule(rule)}
|
||||
snoozeRule={async (snoozeSchedule) => {
|
||||
await snoozeRule(rule, snoozeSchedule);
|
||||
}}
|
||||
unsnoozeRule={async (scheduleIds) => await unsnoozeRule(rule, scheduleIds)}
|
||||
rule={rule as RuleTableItem}
|
||||
onRuleChanged={requestRefresh}
|
||||
direction="row"
|
||||
isEditable={hasEditButton}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { RuleStatusPanelWithApi, RuleStatusPanel } from './rule_status_panel';
|
||||
import { mockRule } from './test_helpers';
|
||||
|
||||
jest.mock('../../../lib/rule_api/load_execution_log_aggregations', () => ({
|
||||
loadExecutionLogAggregations: () => ({ total: 400 }),
|
||||
}));
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
notifications: {
|
||||
toasts: {
|
||||
addSuccess: jest.fn(),
|
||||
addDanger: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockAPIs = {
|
||||
enableRule: jest.fn(),
|
||||
disableRule: jest.fn(),
|
||||
snoozeRule: jest.fn(),
|
||||
unsnoozeRule: jest.fn(),
|
||||
loadExecutionLogAggregations: jest.fn(),
|
||||
};
|
||||
const requestRefresh = jest.fn();
|
||||
|
||||
describe('rule status panel', () => {
|
||||
it('fetches and renders the number of executions in the last 24 hours', async () => {
|
||||
const rule = mockRule();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanelWithApi
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
/>
|
||||
);
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
const ruleExecutionsDescription = wrapper.find(
|
||||
'[data-test-subj="ruleStatus-numberOfExecutions"]'
|
||||
);
|
||||
expect(ruleExecutionsDescription.first().text()).toBe('400 executions in the last 24 hr');
|
||||
});
|
||||
it('should disable the rule when picking disable in the dropdown', async () => {
|
||||
const rule = mockRule({ enabled: true });
|
||||
const disableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanel
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
disableRule={disableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(disableRule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if rule is already disabled should do nothing when picking disable in the dropdown', async () => {
|
||||
const rule = mockRule({ enabled: false });
|
||||
const disableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanel
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
disableRule={disableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(disableRule).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should enable the rule when picking enable in the dropdown', async () => {
|
||||
const rule = mockRule({ enabled: false });
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanel
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
enableRule={enableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(0).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(enableRule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('if rule is already enabled should do nothing when picking enable in the dropdown', async () => {
|
||||
const rule = mockRule({ enabled: true });
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanel
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
enableRule={enableRule}
|
||||
/>
|
||||
);
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(0).simulate('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
expect(enableRule).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const disableRule = jest.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
});
|
||||
const enableRule = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusPanel
|
||||
{...mockAPIs}
|
||||
rule={rule}
|
||||
isEditable
|
||||
healthColor="primary"
|
||||
statusMessage="Ok"
|
||||
requestRefresh={requestRefresh}
|
||||
enableRule={enableRule}
|
||||
disableRule={disableRule}
|
||||
/>
|
||||
);
|
||||
|
||||
const actionsElem = wrapper
|
||||
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
|
||||
.first();
|
||||
actionsElem.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
|
||||
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
|
||||
actionsMenuItemElem.at(1).simulate('click');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
expect(disableRule).toHaveBeenCalled();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner')
|
||||
.length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import moment from 'moment';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiHealth,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiStat,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { RuleStatusDropdown, RulesListNotifyBadge } from '../..';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
|
||||
type ComponentOpts = Pick<
|
||||
RuleApis,
|
||||
'disableRule' | 'enableRule' | 'snoozeRule' | 'unsnoozeRule' | 'loadExecutionLogAggregations'
|
||||
> & {
|
||||
rule: any;
|
||||
isEditable: boolean;
|
||||
requestRefresh: () => void;
|
||||
healthColor: string;
|
||||
statusMessage: string;
|
||||
};
|
||||
|
||||
export const RuleStatusPanel: React.FC<ComponentOpts> = ({
|
||||
rule,
|
||||
disableRule,
|
||||
enableRule,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
requestRefresh,
|
||||
isEditable,
|
||||
healthColor,
|
||||
statusMessage,
|
||||
loadExecutionLogAggregations,
|
||||
}) => {
|
||||
const [isSnoozeLoading, setIsSnoozeLoading] = useState(false);
|
||||
const [isSnoozeOpen, setIsSnoozeOpen] = useState(false);
|
||||
const [lastNumberOfExecutions, setLastNumberOfExecutions] = useState<number | null>(null);
|
||||
|
||||
const openSnooze = useCallback(() => setIsSnoozeOpen(true), [setIsSnoozeOpen]);
|
||||
const closeSnooze = useCallback(() => setIsSnoozeOpen(false), [setIsSnoozeOpen]);
|
||||
const onSnoozeRule = useCallback(
|
||||
(snoozeSchedule) => snoozeRule(rule, snoozeSchedule),
|
||||
[rule, snoozeRule]
|
||||
);
|
||||
const onUnsnoozeRule = useCallback(
|
||||
(scheduleIds) => unsnoozeRule(rule, scheduleIds),
|
||||
[rule, unsnoozeRule]
|
||||
);
|
||||
|
||||
const getLastNumberOfExecutions = useCallback(async () => {
|
||||
try {
|
||||
const result = await loadExecutionLogAggregations({
|
||||
id: rule.id,
|
||||
dateStart: datemath.parse('now-24h')!.format(),
|
||||
dateEnd: datemath.parse('now')!.format(),
|
||||
page: 0,
|
||||
perPage: 10,
|
||||
});
|
||||
setLastNumberOfExecutions(result.total);
|
||||
} catch (e) {
|
||||
// Do nothing if executions fail to fetch
|
||||
}
|
||||
}, [loadExecutionLogAggregations, setLastNumberOfExecutions, rule]);
|
||||
|
||||
useEffect(() => {
|
||||
getLastNumberOfExecutions();
|
||||
}, [getLastNumberOfExecutions]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder paddingSize="none">
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlexGroup justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.ruleIsEnabledDisabledTitle"
|
||||
defaultMessage="Rule is"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<RuleStatusDropdown
|
||||
disableRule={async () => await disableRule(rule)}
|
||||
enableRule={async () => await enableRule(rule)}
|
||||
snoozeRule={async () => {}}
|
||||
unsnoozeRule={async () => {}}
|
||||
rule={rule}
|
||||
onRuleChanged={requestRefresh}
|
||||
direction="row"
|
||||
isEditable={isEditable}
|
||||
hideSnoozeOption
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" color="subdued" data-test-subj="ruleStatus-numberOfExecutions">
|
||||
{lastNumberOfExecutions !== null && (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.totalExecutions"
|
||||
defaultMessage="{executions, plural, one {# execution} other {# executions}} in the last 24 hr"
|
||||
values={{ executions: lastNumberOfExecutions }}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlexGroup gutterSize="none" direction="row" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj={`ruleStatus-${rule.executionStatus.status}`}
|
||||
titleSize="m"
|
||||
descriptionElement="strong"
|
||||
titleElement="h5"
|
||||
title={
|
||||
<EuiHealth
|
||||
data-test-subj={`ruleStatus-${rule.executionStatus.status}`}
|
||||
textSize="m"
|
||||
color={healthColor}
|
||||
style={{ fontWeight: 400 }}
|
||||
>
|
||||
{statusMessage}
|
||||
</EuiHealth>
|
||||
}
|
||||
description={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription',
|
||||
{
|
||||
defaultMessage: `Last response`,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText color="subdued" size="xs">
|
||||
{moment(rule.executionStatus.lastExecutionDate).fromNow()}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiPanel hasShadow={false}>
|
||||
<RulesListNotifyBadge
|
||||
rule={{ ...rule, isEditable }}
|
||||
isOpen={isSnoozeOpen}
|
||||
isLoading={isSnoozeLoading}
|
||||
onLoading={setIsSnoozeLoading}
|
||||
onClick={openSnooze}
|
||||
onClose={closeSnooze}
|
||||
onRuleChanged={requestRefresh}
|
||||
snoozeRule={onSnoozeRule}
|
||||
unsnoozeRule={onUnsnoozeRule}
|
||||
showTooltipInline
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleStatusPanelWithApi = withBulkRuleOperations(RuleStatusPanel);
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
import { Rule } from '../../../../types';
|
||||
|
||||
export function mockRule(overloads: Partial<Rule> = {}): Rule {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `rule-${uuid.v4()}`,
|
||||
tags: [],
|
||||
ruleTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
...overloads,
|
||||
};
|
||||
}
|
|
@ -7,7 +7,15 @@
|
|||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import { EuiButton, EuiButtonIcon, EuiPopover, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleSnooze, RuleSnoozeSchedule } from '@kbn/alerting-plugin/common';
|
||||
import { i18nAbbrMonthDayDate, i18nMonthDayDate } from '../../../lib/i18n_month_day_date';
|
||||
|
@ -32,7 +40,7 @@ export const UNSNOOZE_SUCCESS_MESSAGE = i18n.translate(
|
|||
export const SNOOZE_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed',
|
||||
{
|
||||
defaultMessage: 'Unabled to change rule snooze settings',
|
||||
defaultMessage: 'Unable to change rule snooze settings',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -47,6 +55,7 @@ export interface RulesListNotifyBadgeProps {
|
|||
onRuleChanged: () => void;
|
||||
snoozeRule: (schedule: SnoozeSchedule, muteAll?: boolean) => Promise<void>;
|
||||
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
|
||||
showTooltipInline?: boolean;
|
||||
}
|
||||
|
||||
const openSnoozePanelAriaLabel = i18n.translate(
|
||||
|
@ -81,6 +90,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
onRuleChanged,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
showTooltipInline = false,
|
||||
} = props;
|
||||
|
||||
const { isSnoozedUntil, muteAll, isEditable } = rule;
|
||||
|
@ -139,8 +149,23 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
}
|
||||
);
|
||||
}
|
||||
if (showTooltipInline) {
|
||||
return i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.noSnoozeAppliedTooltip',
|
||||
{
|
||||
defaultMessage: 'Notify when alerts generated',
|
||||
}
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}, [isSnoozedIndefinitely, isScheduled, isSnoozed, isSnoozedUntil, nextScheduledSnooze]);
|
||||
}, [
|
||||
isSnoozedIndefinitely,
|
||||
isScheduled,
|
||||
isSnoozed,
|
||||
isSnoozedUntil,
|
||||
nextScheduledSnooze,
|
||||
showTooltipInline,
|
||||
]);
|
||||
|
||||
const snoozedButton = useMemo(() => {
|
||||
return (
|
||||
|
@ -233,11 +258,11 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
]);
|
||||
|
||||
const buttonWithToolTip = useMemo(() => {
|
||||
if (isOpen) {
|
||||
if (isOpen || showTooltipInline) {
|
||||
return button;
|
||||
}
|
||||
return <EuiToolTip content={snoozeTooltipText}>{button}</EuiToolTip>;
|
||||
}, [isOpen, button, snoozeTooltipText]);
|
||||
}, [isOpen, button, snoozeTooltipText, showTooltipInline]);
|
||||
|
||||
const onClosePopover = useCallback(() => {
|
||||
onClose();
|
||||
|
@ -249,6 +274,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
async (schedule: SnoozeSchedule) => {
|
||||
try {
|
||||
onLoading(true);
|
||||
onClosePopover();
|
||||
await snoozeRule(schedule);
|
||||
onRuleChanged();
|
||||
toasts.addSuccess(SNOOZE_SUCCESS_MESSAGE);
|
||||
|
@ -256,7 +282,6 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
|
||||
} finally {
|
||||
onLoading(false);
|
||||
onClosePopover();
|
||||
}
|
||||
},
|
||||
[onLoading, snoozeRule, onRuleChanged, toasts, onClosePopover]
|
||||
|
@ -266,6 +291,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
async (scheduleIds?: string[]) => {
|
||||
try {
|
||||
onLoading(true);
|
||||
onClosePopover();
|
||||
await unsnoozeRule(scheduleIds);
|
||||
onRuleChanged();
|
||||
toasts.addSuccess(UNSNOOZE_SUCCESS_MESSAGE);
|
||||
|
@ -273,13 +299,12 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
toasts.addDanger(SNOOZE_FAILED_MESSAGE);
|
||||
} finally {
|
||||
onLoading(false);
|
||||
onClosePopover();
|
||||
}
|
||||
},
|
||||
[onLoading, unsnoozeRule, onRuleChanged, toasts, onClosePopover]
|
||||
);
|
||||
|
||||
return (
|
||||
const popover = (
|
||||
<EuiPopover
|
||||
data-test-subj="rulesListNotifyBadge"
|
||||
isOpen={isOpen}
|
||||
|
@ -296,6 +321,19 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
if (showTooltipInline) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>{popover}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{snoozeTooltipText}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return popover;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue