[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:
Zacqary Adam Xeper 2022-06-29 12:15:52 -05:00 committed by GitHub
parent 66b161a3e0
commit 46d8e11f29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 562 additions and 350 deletions

View file

@ -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',

View file

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

View file

@ -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[] = [
{

View file

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

View file

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

View file

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

View file

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

View file

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