[RAM] Add error logs in rule details page (#128925) (#129278)

* add error log on details page + snooze

* refresh table in details page

* fix type

* add + fix jest test

* add test for error log

* remove console

* update functional test + delete flaky jest test + fix i18n

* review I

* remove skip

* review II

* remove disable panel

* clean up design

* fix check

* fix types

* Design tweaks to header, status dropdown, mobile

* remove dead code

* jest test

* fix actions layout

* review III

Co-authored-by: Ryan Keairns <contactryank@gmail.com>
(cherry picked from commit d6b8e4bbc5)
This commit is contained in:
Xavier Mouligneau 2022-04-03 10:06:00 -04:00 committed by GitHub
parent 8374636625
commit 2aa076a33a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1066 additions and 655 deletions

View file

@ -36,7 +36,21 @@ export interface IExecutionLog {
timed_out: boolean;
}
export interface IExecutionErrors {
id: string;
timestamp: string;
type: string;
message: string;
}
export interface IExecutionErrorsResult {
totalErrors: number;
errors: IExecutionErrors[];
}
export interface IExecutionLogResult {
total: number;
data: IExecutionLog[];
}
export type IExecutionLogWithErrorsResult = IExecutionLogResult & IExecutionErrorsResult;

View file

@ -7,6 +7,7 @@
import { get } from 'lodash';
import { QueryEventsBySavedObjectResult, IValidatedEvent } from '../../../event_log/server';
import { IExecutionErrors, IExecutionErrorsResult } from '../../common';
const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid';
const TIMESTAMP_FIELD = '@timestamp';
@ -14,18 +15,6 @@ const PROVIDER_FIELD = 'event.provider';
const MESSAGE_FIELD = 'message';
const ERROR_MESSAGE_FIELD = 'error.message';
export interface IExecutionErrors {
id: string;
timestamp: string;
type: string;
message: string;
}
export interface IExecutionErrorsResult {
totalErrors: number;
errors: IExecutionErrors[];
}
export const EMPTY_EXECUTION_ERRORS_RESULT = {
totalErrors: 0,
errors: [],

View file

@ -11,7 +11,7 @@ import { licenseStateMock } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { SavedObjectsErrorHelpers } from 'src/core/server';
import { rulesClientMock } from '../rules_client.mock';
import { IExecutionLogWithErrorsResult } from '../rules_client';
import { IExecutionLogWithErrorsResult } from '../../common';
const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access.ts', () => ({

View file

@ -89,13 +89,10 @@ import {
formatExecutionLogResult,
getExecutionLogAggregation,
} from '../lib/get_execution_log_aggregation';
import { IExecutionLogResult } from '../../common';
import { IExecutionLogWithErrorsResult } from '../../common';
import { validateSnoozeDate } from '../lib/validate_snooze_date';
import { RuleMutedError } from '../lib/errors/rule_muted';
import {
formatExecutionErrorsResult,
IExecutionErrorsResult,
} from '../lib/format_execution_log_errors';
import { formatExecutionErrorsResult } from '../lib/format_execution_log_errors';
export interface RegistryAlertTypeWithAuth extends RegistryRuleType {
authorizedConsumers: string[];
@ -263,7 +260,6 @@ export interface GetExecutionLogByIdParams {
sort: estypes.Sort;
}
export type IExecutionLogWithErrorsResult = IExecutionLogResult & IExecutionErrorsResult;
interface ScheduleRuleOptions {
id: string;
consumer: string;

View file

@ -27777,13 +27777,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText": "ルールを編集",
"xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle": "このルールに関連付けられたコネクターの1つで問題が発生しています。",
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
"xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRule": "このルールは無効になっていて再表示できません。",
"xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRuleTitle": "無効なルール",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableLoadingTitle": "有効にする",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableTitle": "有効にする",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteLoadingTitle": "ミュート",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteTitle": "ミュート",
"xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle": "閉じる",
"xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "編集",
"xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "ライセンスの管理",
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "ルール",

View file

@ -27806,13 +27806,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText": "编辑规则",
"xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle": "与此规则关联的连接器之一出现问题。",
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
"xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRule": "此规则已禁用,无法显示。",
"xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRuleTitle": "已禁用规则",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableLoadingTitle": "启用",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableTitle": "启用",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteLoadingTitle": "静音",
"xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteTitle": "静音",
"xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle": "关闭",
"xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel": "编辑",
"xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "管理许可证",
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "规则",

View file

@ -12,9 +12,9 @@ import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import {
IExecutionLogResult,
IExecutionLog,
ExecutionLogSortFields,
IExecutionLogWithErrorsResult,
} from '../../../../../alerting/common';
import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common';
@ -36,9 +36,12 @@ const getRenamedLog = (data: IExecutionLog) => {
};
};
const rewriteBodyRes: RewriteRequestCase<IExecutionLogResult> = ({ data, total }: any) => ({
const rewriteBodyRes: RewriteRequestCase<IExecutionLogWithErrorsResult> = ({
data,
...rest
}: any) => ({
data: data.map((log: IExecutionLog) => getRenamedLog(log)),
total,
...rest,
});
const getFilter = (filter: string[] | undefined) => {
@ -77,7 +80,7 @@ export const loadExecutionLogAggregations = async ({
}: LoadExecutionLogAggregationsProps & { http: HttpSetup }) => {
const sortField: any[] = sort;
const result = await http.get<AsApiContract<IExecutionLogResult>>(
const result = await http.get<AsApiContract<IExecutionLogWithErrorsResult>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_log`,
{
query: {

View file

@ -35,8 +35,10 @@ import {
resolveRule,
loadExecutionLogAggregations,
LoadExecutionLogAggregationsProps,
snoozeRule,
unsnoozeRule,
} from '../../../lib/rule_api';
import { IExecutionLogResult } from '../../../../../../alerting/common';
import { IExecutionLogWithErrorsResult } from '../../../../../../alerting/common';
import { useKibana } from '../../../../common/lib/kibana';
export interface ComponentOpts {
@ -64,9 +66,11 @@ export interface ComponentOpts {
loadRuleTypes: () => Promise<RuleType[]>;
loadExecutionLogAggregations: (
props: LoadExecutionLogAggregationsProps
) => Promise<IExecutionLogResult>;
) => Promise<IExecutionLogWithErrorsResult>;
getHealth: () => Promise<AlertingFrameworkHealth>;
resolveRule: (id: Rule['id']) => Promise<ResolvedRule>;
snoozeRule: (rule: Rule, snoozeEndTime: string | -1) => Promise<void>;
unsnoozeRule: (rule: Rule) => Promise<void>;
}
export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>;
@ -145,6 +149,12 @@ export function withBulkRuleOperations<T>(
}
resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })}
getHealth={async () => alertingFrameworkHealth({ http })}
snoozeRule={async (rule: Rule, snoozeEndTime: string | -1) => {
return await snoozeRule({ http, id: rule.id, snoozeEndTime });
}}
unsnoozeRule={async (rule: Rule) => {
return await unsnoozeRule({ http, id: rule.id });
}}
/>
);
};

View file

@ -12,14 +12,16 @@ import {
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiStat,
EuiIconTip,
EuiTabbedContent,
EuiText,
} from '@elastic/eui';
// @ts-ignore
import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services';
import { FormattedMessage } from '@kbn/i18n-react';
import moment from 'moment';
import {
ActionGroup,
AlertExecutionStatusErrorReasons,
@ -47,6 +49,7 @@ import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experime
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list'));
const RuleErrorLogWithApi = lazy(() => import('./rule_error_log'));
const RuleAlertList = lazy(() => import('./rule_alert_list'));
@ -56,6 +59,7 @@ type RuleProps = {
readOnly: boolean;
ruleSummary: RuleSummary;
requestRefresh: () => Promise<void>;
refreshToken?: number;
numberOfExecutions: number;
onChangeDuration: (length: number) => void;
durationEpoch?: number;
@ -64,6 +68,7 @@ type RuleProps = {
const EVENT_LOG_LIST_TAB = 'rule_event_log_list';
const ALERT_LIST_TAB = 'rule_alert_list';
const EVENT_ERROR_LOG_TAB = 'rule_error_log_list';
export function RuleComponent({
rule,
@ -73,6 +78,7 @@ export function RuleComponent({
muteAlertInstance,
unmuteAlertInstance,
requestRefresh,
refreshToken,
numberOfExecutions,
onChangeDuration,
durationEpoch = Date.now(),
@ -116,10 +122,13 @@ export function RuleComponent({
{
id: EVENT_LOG_LIST_TAB,
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
defaultMessage: 'Execution History',
defaultMessage: 'Execution history',
}),
'data-test-subj': 'eventLogListTab',
content: suspendedComponentWithProps(RuleEventLogListWithApi, 'xl')({ rule }),
content: suspendedComponentWithProps(
RuleEventLogListWithApi,
'xl'
)({ requestRefresh, rule, refreshToken }),
},
{
id: ALERT_LIST_TAB,
@ -129,6 +138,17 @@ export function RuleComponent({
'data-test-subj': 'ruleAlertListTab',
content: renderRuleAlertList(),
},
{
id: EVENT_ERROR_LOG_TAB,
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.errorLogTabText', {
defaultMessage: 'Error log',
}),
'data-test-subj': 'errorLogTab',
content: suspendedComponentWithProps(
RuleErrorLogWithApi,
'xl'
)({ requestRefresh, rule, refreshToken }),
},
];
const renderTabs = () => {
@ -141,29 +161,51 @@ export function RuleComponent({
return (
<>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiPanel color="subdued" hasBorder={false}>
<EuiStat
data-test-subj={`ruleStatus-${rule.executionStatus.status}`}
titleSize="xs"
title={
<EuiHealth
<EuiFlexGroup
gutterSize="none"
direction="column"
justifyContent="spaceBetween"
responsive={false}
style={{ height: '100%' }}
>
<EuiFlexItem>
<EuiStat
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`,
}
)}
/>
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>
</EuiFlexItem>
<EuiFlexItem grow={1}>
@ -217,6 +259,7 @@ export function RuleComponent({
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
<input
type="hidden"

View file

@ -12,14 +12,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from '@testing-library/react';
import { RuleDetails } from './rule_details';
import { Rule, ActionType, RuleTypeModel, RuleType } from '../../../../types';
import {
EuiBadge,
EuiFlexItem,
EuiSwitch,
EuiButtonEmpty,
EuiText,
EuiPageHeaderProps,
} from '@elastic/eui';
import { EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiPageHeaderProps } from '@elastic/eui';
import {
ActionGroup,
AlertExecutionStatusErrorReasons,
@ -29,6 +22,8 @@ import {
import { useKibana } from '../../../../common/lib/kibana';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
export const DATE_9999 = '9999-12-31T12:34:56.789Z';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/lib/config_api', () => ({
@ -64,6 +59,8 @@ const mockRuleApis = {
disableRule: jest.fn(),
requestRefresh: jest.fn(),
refreshToken: Date.now(),
snoozeRule: jest.fn(),
unsnoozeRule: jest.fn(),
};
const authorizedConsumers = {
@ -103,30 +100,29 @@ describe('rule_details', () => {
).toBeTruthy();
});
it('renders the rule error banner with error message, when rule status is an error', () => {
it('renders the rule error banner with error message, when rule has a license error', () => {
const rule = mockRule({
enabled: true,
executionStatus: {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
reason: AlertExecutionStatusErrorReasons.Unknown,
reason: AlertExecutionStatusErrorReasons.License,
message: 'test',
},
},
});
expect(
shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
).containsMatchingElement(
<EuiText size="s" color="danger" data-test-subj="ruleErrorMessageText">
{'test'}
</EuiText>
)
).toBeTruthy();
const wrapper = shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
);
expect(wrapper.find('[data-test-subj="ruleErrorBanner"]').first().text()).toMatchInlineSnapshot(
`"<EuiIcon /> Cannot run rule, test <FormattedMessage />"`
);
});
it('renders the rule warning banner with warning message, when rule status is a warning', () => {
const rule = mockRule({
enabled: true,
executionStatus: {
status: 'warning',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
@ -136,15 +132,12 @@ describe('rule_details', () => {
},
},
});
const wrapper = shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
);
expect(
shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
).containsMatchingElement(
<EuiText size="s" color="warning" data-test-subj="ruleWarningMessageText">
{'warning message'}
</EuiText>
)
).toBeTruthy();
wrapper.find('[data-test-subj="ruleWarningBanner"]').first().text()
).toMatchInlineSnapshot(`"<EuiIcon /> Action limit exceeded warning message"`);
});
it('displays a toast message when interval is less than configured minimum', async () => {
@ -190,7 +183,7 @@ describe('rule_details', () => {
];
expect(
shallow(
mountWithIntl(
<RuleDetails
rule={rule}
ruleType={ruleType}
@ -241,7 +234,7 @@ describe('rule_details', () => {
},
];
const details = shallow(
const details = mountWithIntl(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={actionTypes} {...mockRuleApis} />
);
@ -302,63 +295,37 @@ describe('rule_details', () => {
});
});
describe('disable button', () => {
it('should render a disable button when rule is enabled', () => {
describe('disable/enable functionality', () => {
it('should show that the rule is enabled', () => {
const rule = mockRule({
enabled: true,
});
const enableButton = shallow(
const wrapper = mountWithIntl(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
)
.find(EuiSwitch)
.find('[name="enable"]')
.first();
);
const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first();
expect(enableButton.props()).toMatchObject({
checked: true,
disabled: false,
});
expect(actionsElem.text()).toEqual('Enabled');
});
it('should render a enable button and empty state when rule is disabled', async () => {
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();
await act(async () => {
await nextTick();
wrapper.update();
});
const enableButton = wrapper.find(EuiSwitch).find('[name="enable"]').first();
const disabledEmptyPrompt = wrapper.find('[data-test-subj="disabledEmptyPrompt"]');
const disabledEmptyPromptAction = wrapper.find('[data-test-subj="disabledEmptyPromptAction"]');
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: false,
});
expect(disabledEmptyPrompt.exists()).toBeTruthy();
expect(disabledEmptyPromptAction.exists()).toBeTruthy();
disabledEmptyPromptAction.first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(mockRuleApis.enableRule).toHaveBeenCalledTimes(1);
expect(actionsElem.text()).toEqual('Disabled');
});
it('should disable the rule when rule is enabled and button is clicked', () => {
it('should disable the rule when picking disable in the dropdown', async () => {
const rule = mockRule({
enabled: true,
});
const disableRule = jest.fn();
const enableButton = shallow(
const wrapper = mountWithIntl(
<RuleDetails
rule={rule}
ruleType={ruleType}
@ -366,60 +333,32 @@ describe('disable button', () => {
{...mockRuleApis}
disableRule={disableRule}
/>
)
.find(EuiSwitch)
.find('[name="enable"]')
);
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();
});
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
expect(disableRule).toHaveBeenCalledTimes(0);
handler!({} as React.FormEvent);
expect(disableRule).toHaveBeenCalledTimes(1);
});
it('should enable the rule when rule is disabled and button is clicked', () => {
it('if rule is already disable should do nothing when picking disable in the dropdown', async () => {
const rule = mockRule({
enabled: false,
});
const enableRule = jest.fn();
const enableButton = shallow(
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={[]}
{...mockRuleApis}
enableRule={enableRule}
/>
)
.find(EuiSwitch)
.find('[name="enable"]')
.first();
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
expect(enableRule).toHaveBeenCalledTimes(0);
handler!({} as React.FormEvent);
expect(enableRule).toHaveBeenCalledTimes(1);
});
it('should reset error banner dismissal after re-enabling the rule', async () => {
const rule = mockRule({
enabled: true,
executionStatus: {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
reason: AlertExecutionStatusErrorReasons.Execute,
message: 'Fail',
},
},
});
const disableRule = jest.fn();
const enableRule = jest.fn();
const wrapper = mountWithIntl(
<RuleDetails
rule={rule}
@ -427,55 +366,99 @@ describe('disable button', () => {
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();
});
// Dismiss the error banner
await act(async () => {
wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click');
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
actionsMenuItemElem.at(0).simulate('click');
await nextTick();
});
// Disable the rule
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().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,
});
expect(disableRule).toHaveBeenCalled();
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();
});
// Enable the rule
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]');
const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem');
actionsMenuItemElem.at(0).simulate('click');
await nextTick();
});
expect(enableRule).toHaveBeenCalled();
// Ensure error banner is back
expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0);
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,
executionStatus: {
status: 'error',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
error: {
reason: AlertExecutionStatusErrorReasons.Execute,
message: 'Fail',
},
},
});
const disableRule = jest.fn(async () => {
@ -493,139 +476,53 @@ describe('disable button', () => {
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
// Dismiss the error banner
await act(async () => {
wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click');
await nextTick();
});
// Disable the rule
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
await nextTick();
});
expect(disableRule).toHaveBeenCalled();
const actionsElem = wrapper
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
.first();
actionsElem.simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
// Enable the rule
await act(async () => {
expect(wrapper.find('[data-test-subj="enableSpinner"]').length).toBeGreaterThan(0);
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('mute button', () => {
it('should render an mute button when rule is enabled', () => {
const rule = mockRule({
enabled: true,
muteAll: false,
});
const enableButton = shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: false,
});
});
it('should render an muted button when rule is muted', () => {
describe('snooze functionality', () => {
it('should render "Snooze Indefinitely" when rule is enabled and mute all', () => {
const rule = mockRule({
enabled: true,
muteAll: true,
});
const enableButton = shallow(
const wrapper = mountWithIntl(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
);
const actionsElem = wrapper
.find('[data-test-subj="statusDropdown"] .euiBadge__childButton')
.first();
expect(enableButton.props()).toMatchObject({
checked: true,
disabled: false,
});
});
it('should mute the rule when rule is unmuted and button is clicked', () => {
const rule = mockRule({
enabled: true,
muteAll: false,
});
const muteRule = jest.fn();
const enableButton = shallow(
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={[]}
{...mockRuleApis}
muteRule={muteRule}
/>
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
expect(muteRule).toHaveBeenCalledTimes(0);
handler!({} as React.FormEvent);
expect(muteRule).toHaveBeenCalledTimes(1);
});
it('should unmute the rule when rule is muted and button is clicked', () => {
const rule = mockRule({
enabled: true,
muteAll: true,
});
const unmuteRule = jest.fn();
const enableButton = shallow(
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={[]}
{...mockRuleApis}
unmuteRule={unmuteRule}
/>
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
enableButton.simulate('click');
const handler = enableButton.prop('onChange');
expect(typeof handler).toEqual('function');
expect(unmuteRule).toHaveBeenCalledTimes(0);
handler!({} as React.FormEvent);
expect(unmuteRule).toHaveBeenCalledTimes(1);
});
it('should disabled mute button when rule is disabled', () => {
const rule = mockRule({
enabled: false,
muteAll: false,
});
const enableButton = shallow(
<RuleDetails rule={rule} ruleType={ruleType} actionTypes={[]} {...mockRuleApis} />
)
.find(EuiSwitch)
.find('[name="mute"]')
.first();
expect(enableButton.props()).toMatchObject({
checked: false,
disabled: true,
});
expect(actionsElem.text()).toEqual('Snoozed');
expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toEqual(
'Indefinitely'
);
});
});

View file

@ -16,15 +16,13 @@ import {
EuiFlexItem,
EuiBadge,
EuiPageContentBody,
EuiSwitch,
EuiCallOut,
EuiSpacer,
EuiButtonEmpty,
EuiButton,
EuiLoadingSpinner,
EuiIconTip,
EuiEmptyPrompt,
EuiPageTemplate,
EuiIcon,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
@ -38,6 +36,7 @@ import {
ActionType,
ActionConnector,
TriggersActionsUiConfig,
RuleTableItem,
} from '../../../../types';
import {
ComponentOpts as BulkOperationsComponentOpts,
@ -55,6 +54,7 @@ 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;
@ -62,7 +62,7 @@ export type RuleDetailsProps = {
actionTypes: ActionType[];
requestRefresh: () => Promise<void>;
refreshToken?: number;
} & Pick<BulkOperationsComponentOpts, 'disableRule' | 'enableRule' | 'unmuteRule' | 'muteRule'>;
} & Pick<BulkOperationsComponentOpts, 'disableRule' | 'enableRule' | 'snoozeRule' | 'unsnoozeRule'>;
export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
rule,
@ -70,8 +70,8 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
actionTypes,
disableRule,
enableRule,
unmuteRule,
muteRule,
snoozeRule,
unsnoozeRule,
requestRefresh,
refreshToken,
}) => {
@ -150,13 +150,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
const ruleActions = rule.actions;
const uniqueActions = Array.from(new Set(ruleActions.map((item: any) => item.actionTypeId)));
const [isEnabled, setIsEnabled] = useState<boolean>(rule.enabled);
const [isEnabledUpdating, setIsEnabledUpdating] = useState<boolean>(false);
const [isMutedUpdating, setIsMutedUpdating] = useState<boolean>(false);
const [isMuted, setIsMuted] = useState<boolean>(rule.muteAll);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const [dismissRuleErrors, setDismissRuleErrors] = useState<boolean>(false);
const [dismissRuleWarning, setDismissRuleWarning] = useState<boolean>(false);
// Check whether interval is below configured minium
useEffect(() => {
@ -269,6 +263,95 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
values={{ ruleName: rule.name }}
/>
}
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.triggerActionsTitle"
defaultMessage="Trigger actions"
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<RuleStatusDropdown
disableRule={async () => await disableRule(rule)}
enableRule={async () => await enableRule(rule)}
snoozeRule={async (snoozeEndTime: string | -1) =>
await snoozeRule(rule, snoozeEndTime)
}
unsnoozeRule={async () => await unsnoozeRule(rule)}
item={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}>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleTypeTitle"
defaultMessage="Type"
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge data-test-subj="ruleTypeLabel">{ruleType.name}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{uniqueActions && uniqueActions.length ? (
<EuiFlexGroup responsive={false} gutterSize="xs">
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex"
defaultMessage="Actions"
/>{' '}
{hasActionsWithBrokenConnector && (
<EuiIconTip
data-test-subj="actionWithBrokenConnector"
type="alert"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip',
{
defaultMessage:
'Unable to load one of the connectors associated with this rule. Edit the rule to select a new connector.',
}
)}
position="right"
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
{uniqueActions.map((action, index) => (
<EuiFlexItem key={index} grow={false}>
<EuiBadge color="hollow" data-test-subj="actionTypeLabel">
{actionTypesByTypeId[action].name ?? action}
</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
}
rightSideItems={[
<ViewInApp rule={rule} />,
<EuiButtonEmpty
@ -288,227 +371,49 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
/>
<EuiSpacer size="l" />
<EuiPageContentBody>
<EuiFlexGroup wrap responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleTypeTitle"
defaultMessage="Type"
/>
</p>
</EuiText>
<EuiSpacer size="xs" />
<EuiBadge data-test-subj="ruleTypeLabel">{ruleType.name}</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={1}>
{uniqueActions && uniqueActions.length ? (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex"
defaultMessage="Actions"
/>{' '}
{hasActionsWithBrokenConnector && (
<EuiIconTip
data-test-subj="actionWithBrokenConnector"
type="rule"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip',
{
defaultMessage:
'Unable to load one of the connectors associated with this rule. Edit the rule to select a new connector.',
}
)}
position="right"
/>
)}
</EuiText>
<EuiSpacer size="xs" />
<EuiFlexGroup wrap gutterSize="s">
{uniqueActions.map((action, index) => (
<EuiFlexItem key={index} grow={false}>
<EuiBadge color="hollow" data-test-subj="actionTypeLabel">
{actionTypesByTypeId[action].name ?? action}
</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
</>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd" wrap responsive={false} gutterSize="m">
<EuiFlexItem grow={false}>
{isEnabledUpdating ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiLoadingSpinner data-test-subj="enableSpinner" size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableLoadingTitle"
defaultMessage="Enable"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiSwitch
name="enable"
disabled={!canSaveRule || !ruleType.enabledInLicense}
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
setIsEnabledUpdating(true);
if (isEnabled) {
setIsEnabled(false);
await disableRule(rule);
// Reset dismiss if previously clicked
setDismissRuleErrors(false);
} else {
setIsEnabled(true);
await enableRule(rule);
}
requestRefresh();
setIsEnabledUpdating(false);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.enableTitle"
defaultMessage="Enable"
/>
}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{isMutedUpdating ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteLoadingTitle"
defaultMessage="Mute"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiSwitch
name="mute"
checked={isMuted}
disabled={!canSaveRule || !isEnabled || !ruleType.enabledInLicense}
data-test-subj="muteSwitch"
onChange={async () => {
setIsMutedUpdating(true);
if (isMuted) {
setIsMuted(false);
await unmuteRule(rule);
} else {
setIsMuted(true);
await muteRule(rule);
}
requestRefresh();
setIsMutedUpdating(false);
}}
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.collapsedItemActons.muteTitle"
defaultMessage="Mute"
/>
}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{rule.enabled && !dismissRuleErrors && rule.executionStatus.status === 'error' ? (
{rule.enabled &&
rule.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
color="danger"
data-test-subj="ruleErrorBanner"
size="s"
title={getRuleStatusErrorReasonText()}
iconType="rule"
>
<EuiText size="s" color="danger" data-test-subj="ruleErrorMessageText">
<EuiCallOut color="danger" data-test-subj="ruleErrorBanner" size="s" iconType="rule">
<p>
<EuiIcon color="danger" type="alert" />
&nbsp;
<b>{getRuleStatusErrorReasonText()}</b>&#44;&nbsp;
{rule.executionStatus.error?.message}
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" wrap={true}>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="dismiss-execution-error"
color="danger"
onClick={() => setDismissRuleErrors(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle"
defaultMessage="Dismiss"
/>
</EuiButton>
</EuiFlexItem>
{rule.executionStatus.error?.reason ===
AlertExecutionStatusErrorReasons.License && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={`${http.basePath.get()}/app/management/stack/license_management`}
color="danger"
target="_blank"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle"
defaultMessage="Manage license"
/>
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
&nbsp;
<EuiLink
href={`${http.basePath.get()}/app/management/stack/license_management`}
color="primary"
target="_blank"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle"
defaultMessage="Manage license"
/>
</EuiLink>
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
{rule.enabled && !dismissRuleWarning && rule.executionStatus.status === 'warning' ? (
{rule.enabled && rule.executionStatus.status === 'warning' ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
color="warning"
data-test-subj="ruleWarningBanner"
size="s"
title={getRuleStatusWarningReasonText()}
iconType="alert"
>
<EuiText size="s" color="warning" data-test-subj="ruleWarningMessageText">
<p>
<EuiIcon color="warning" type="alert" />
&nbsp;
{getRuleStatusWarningReasonText()}
&nbsp;
{rule.executionStatus.warning?.message}
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" wrap={true}>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="dismiss-execution-warning"
color="warning"
onClick={() => setDismissRuleWarning(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.dismissButtonTitle"
defaultMessage="Dismiss"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
@ -521,89 +426,41 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
color="warning"
data-test-subj="actionWithBrokenConnectorWarningBanner"
size="s"
title={i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle',
{
defaultMessage:
'There is an issue with one of the connectors associated with this rule.',
}
)}
>
{hasEditButton && (
<EuiFlexGroup gutterSize="s" wrap={true}>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="actionWithBrokenConnectorWarningBannerEdit"
color="warning"
onClick={() => setEditFlyoutVisibility(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText"
defaultMessage="Edit rule"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
<p>
<EuiIcon color="warning" type="alert" />
&nbsp;
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle"
defaultMessage="There is an issue with one of the connectors associated with this rule."
/>
&nbsp;
{hasEditButton && (
<EuiLink
data-test-subj="actionWithBrokenConnectorWarningBannerEdit"
color="primary"
onClick={() => setEditFlyoutVisibility(true)}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerEditText"
defaultMessage="Edit rule"
/>
</EuiLink>
)}
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup>
<EuiFlexItem>
{rule.enabled ? (
<RuleRouteWithApi
requestRefresh={requestRefresh}
refreshToken={refreshToken}
rule={rule}
ruleType={ruleType}
readOnly={!canSaveRule}
/>
) : (
<>
<EuiSpacer />
<EuiPageTemplate template="empty">
<EuiEmptyPrompt
data-test-subj="disabledEmptyPrompt"
title={
<h2>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRuleTitle"
defaultMessage="Disabled Rule"
/>
</h2>
}
body={
<>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.alertInstances.disabledRule"
defaultMessage="This rule is disabled and cannot be displayed."
/>
</p>
</>
}
actions={[
<EuiButton
data-test-subj="disabledEmptyPromptAction"
color="primary"
fill
disabled={isEnabledUpdating}
onClick={async () => {
setIsEnabledUpdating(true);
setIsEnabled(true);
await enableRule(rule);
requestRefresh();
setIsEnabledUpdating(false);
}}
>
Enable
</EuiButton>,
]}
/>
</EuiPageTemplate>
</>
)}
<RuleRouteWithApi
requestRefresh={requestRefresh}
refreshToken={refreshToken}
rule={rule}
ruleType={ruleType}
readOnly={!canSaveRule}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>

View file

@ -0,0 +1,312 @@
/*
* 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 { useKibana } from '../../../../common/lib/kibana';
import { EuiSuperDatePicker } from '@elastic/eui';
import { Rule } from '../../../../types';
import { RuleErrorLog } from './rule_error_log';
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../../../common/lib/kibana');
const mockLogResponse: any = {
total: 8,
data: [],
totalErrors: 12,
errors: [
{
id: '66b9c04a-d5d3-4ed4-aa7c-94ddaca3ac1d',
timestamp: '2022-03-31T18:03:33.133Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '14fcfe1c-5403-458f-8549-fa8ef59cdea3',
timestamp: '2022-03-31T18:02:30.119Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: 'd53a401e-2a3a-4abe-8913-26e08a5039fd',
timestamp: '2022-03-31T18:01:27.112Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '9cfeae08-24b4-4d5c-b870-a303418f14d6',
timestamp: '2022-03-31T18:00:24.113Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '66b9c04a-d5d3-4ed4-aa7c-94ddaca3ac23',
timestamp: '2022-03-31T18:03:21.133Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '14fcfe1c-5403-458f-8549-fa8ef59cde18',
timestamp: '2022-03-31T18:02:18.119Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: 'd53a401e-2a3a-4abe-8913-26e08a503915',
timestamp: '2022-03-31T18:01:15.112Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '9cfeae08-24b4-4d5c-b870-a303418f1412',
timestamp: '2022-03-31T18:00:12.113Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '66b9c04a-d5d3-4ed4-aa7c-94ddaca3ac09',
timestamp: '2022-03-31T18:03:09.133Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '14fcfe1c-5403-458f-8549-fa8ef59cde06',
timestamp: '2022-03-31T18:02:06.119Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: 'd53a401e-2a3a-4abe-8913-26e08a503903',
timestamp: '2022-03-31T18:01:03.112Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
{
id: '9cfeae08-24b4-4d5c-b870-a303418f1400',
timestamp: '2022-03-31T18:00:00.113Z',
type: 'alerting',
message:
"rule execution failure: .es-query:d87fcbd0-b11b-11ec-88f6-293354dba871: 'Mine' - x_content_parse_exception: [parsing_exception] Reason: unknown query [match_allxxxx] did you mean [match_all]?",
},
],
};
const mockRule: Rule = {
id: '56b61397-13d7-43d0-a583-0fa8c704a46f',
enabled: true,
name: 'rule-56b61397-13d7-43d0-a583-0fa8c704a46f',
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'),
},
};
const loadExecutionLogAggregationsMock = jest.fn();
describe('rule_error_log', () => {
beforeEach(() => {
jest.clearAllMocks();
useKibanaMock().services.uiSettings.get = jest.fn().mockImplementation((value: string) => {
if (value === 'timepicker:quickRanges') {
return [
{
from: 'now-15m',
to: 'now',
display: 'Last 15 minutes',
},
];
}
});
loadExecutionLogAggregationsMock.mockResolvedValue(mockLogResponse);
});
it('renders correctly', async () => {
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
const wrapper = mountWithIntl(
<RuleErrorLog
rule={mockRule}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
// No data initially
expect(wrapper.find('.euiTableRow .euiTableCellContent__text').first().text()).toEqual(
'No items found'
);
// Run the initial load fetch call
expect(loadExecutionLogAggregationsMock).toHaveBeenCalledTimes(1);
expect(loadExecutionLogAggregationsMock).toHaveBeenCalledWith(
expect.objectContaining({
dateEnd: '1969-12-31T19:00:00-05:00',
dateStart: '1969-12-30T19:00:00-05:00',
id: '56b61397-13d7-43d0-a583-0fa8c704a46f',
page: 0,
perPage: 1,
sort: { timestamp: { order: 'desc' } },
})
);
// Loading
expect(wrapper.find(EuiSuperDatePicker).props().isLoading).toBeTruthy();
expect(wrapper.find('[data-test-subj="tableHeaderCell_timestamp_0"]').exists()).toBeTruthy();
// Let the load resolve
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(EuiSuperDatePicker).props().isLoading).toBeFalsy();
expect(wrapper.find('.euiTableRow').length).toEqual(10);
nowMock.mockRestore();
});
it('can sort on timestamp columns', async () => {
const wrapper = mountWithIntl(
<RuleErrorLog
rule={mockRule}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('.euiTableRow').first().find('.euiTableCellContent').first().text()
).toEqual('Mar 31, 2022 @ 14:03:33.133');
wrapper.find('button[data-test-subj="tableHeaderSortButton"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('.euiTableRow').first().find('.euiTableCellContent').first().text()
).toEqual('Mar 31, 2022 @ 14:00:00.113');
});
it('can paginate', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 100,
});
const wrapper = mountWithIntl(
<RuleErrorLog
rule={mockRule}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('.euiPagination').exists()).toBeTruthy();
// Paginate to the next page
wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('.euiTableRow').length).toEqual(2);
});
it('can filter by start and end date', async () => {
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
const wrapper = mountWithIntl(
<RuleErrorLog
rule={mockRule}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
dateEnd: '1969-12-31T19:00:00-05:00',
dateStart: '1969-12-30T19:00:00-05:00',
id: '56b61397-13d7-43d0-a583-0fa8c704a46f',
page: 0,
perPage: 1,
sort: { timestamp: { order: 'desc' } },
})
);
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"] button')
.simulate('click');
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_Last_15 minutes"] button')
.simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
dateStart: '1969-12-31T18:45:00-05:00',
dateEnd: '1969-12-31T19:00:00-05:00',
id: '56b61397-13d7-43d0-a583-0fa8c704a46f',
page: 0,
perPage: 1,
sort: { timestamp: { order: 'desc' } },
})
);
nowMock.mockRestore();
});
});

View file

@ -0,0 +1,266 @@
/*
* 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, useEffect, useState, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import datemath from '@elastic/datemath';
import {
EuiFlexItem,
EuiFlexGroup,
EuiProgress,
EuiSpacer,
Pagination,
EuiSuperDatePicker,
OnTimeChangeProps,
EuiBasicTable,
EuiTableSortingType,
EuiBasicTableColumn,
} from '@elastic/eui';
import { useKibana } from '../../../../common/lib/kibana';
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
import { Rule } from '../../../../types';
import { IExecutionErrors } from '../../../../../../alerting/common';
import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { RuleEventLogListCellRenderer } from './rule_event_log_list_cell_renderer';
const getParsedDate = (date: string) => {
if (date.includes('now')) {
return datemath.parse(date)?.format() || date;
}
return date;
};
const API_FAILED_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.errorLogColumn.apiError',
{
defaultMessage: 'Failed to fetch error log',
}
);
const updateButtonProps = {
iconOnly: true,
fill: false,
};
const sortErrorLog = (
a: IExecutionErrors,
b: IExecutionErrors,
direction: 'desc' | 'asc' = 'desc'
) =>
direction === 'desc'
? new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
: new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
export type RuleErrorLogProps = {
rule: Rule;
refreshToken?: number;
requestRefresh?: () => Promise<void>;
} & Pick<RuleApis, 'loadExecutionLogAggregations'>;
export const RuleErrorLog = (props: RuleErrorLogProps) => {
const { rule, loadExecutionLogAggregations, refreshToken } = props;
const { uiSettings, notifications } = useKibana().services;
// Data grid states
const [logs, setLogs] = useState<IExecutionErrors[]>([]);
const [pagination, setPagination] = useState<Pagination>({
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
});
const [sort, setSort] = useState<EuiTableSortingType<IExecutionErrors>['sort']>({
field: 'timestamp',
direction: 'desc',
});
// Date related states
const [isLoading, setIsLoading] = useState<boolean>(false);
const [dateStart, setDateStart] = useState<string>('now-24h');
const [dateEnd, setDateEnd] = useState<string>('now');
const [dateFormat] = useState(() => uiSettings?.get('dateFormat'));
const [commonlyUsedRanges] = useState(() => {
return (
uiSettings
?.get('timepicker:quickRanges')
?.map(({ from, to, display }: { from: string; to: string; display: string }) => ({
start: from,
end: to,
label: display,
})) || []
);
});
const isInitialized = useRef(false);
const loadEventLogs = async () => {
setIsLoading(true);
try {
const result = await loadExecutionLogAggregations({
id: rule.id,
sort: {
[sort?.field || 'timestamp']: { order: sort?.direction || 'desc' },
} as unknown as LoadExecutionLogAggregationsProps['sort'],
dateStart: getParsedDate(dateStart),
dateEnd: getParsedDate(dateEnd),
page: 0,
perPage: 1,
});
setLogs(result.errors);
setPagination({
...pagination,
totalItemCount: result.totalErrors,
});
} catch (e) {
notifications.toasts.addDanger({
title: API_FAILED_MESSAGE,
text: e.body.message,
});
}
setIsLoading(false);
};
const onTimeChange = useCallback(
({ start, end, isInvalid }: OnTimeChangeProps) => {
if (isInvalid) {
return;
}
setDateStart(start);
setDateEnd(end);
},
[setDateStart, setDateEnd]
);
const onRefresh = () => {
loadEventLogs();
};
const columns: Array<EuiBasicTableColumn<IExecutionErrors>> = useMemo(
() => [
{
field: 'timestamp',
name: i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.errorLogColumn.timestamp',
{
defaultMessage: 'Timestamp',
}
),
render: (date: string) => (
<RuleEventLogListCellRenderer columnId="timestamp" value={date} dateFormat={dateFormat} />
),
sortable: true,
width: '250px',
},
{
field: 'type',
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.errorLogColumn.type', {
defaultMessage: 'Type',
}),
sortable: false,
width: '100px',
},
{
field: 'message',
name: i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.errorLogColumn.message',
{
defaultMessage: 'Message',
}
),
sortable: false,
},
],
[dateFormat]
);
const logList = useMemo(() => {
const start = pagination.pageIndex * pagination.pageSize;
const logsSortDesc = logs.sort((a, b) => sortErrorLog(a, b, sort?.direction));
return logsSortDesc.slice(start, start + pagination.pageSize);
}, [logs, pagination.pageIndex, pagination.pageSize, sort?.direction]);
useEffect(() => {
loadEventLogs();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateStart, dateEnd]);
useEffect(() => {
if (isInitialized.current) {
loadEventLogs();
}
isInitialized.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshToken]);
return (
<div>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
data-test-subj="ruleEventLogListDatePicker"
width="auto"
isLoading={isLoading}
start={dateStart}
end={dateEnd}
onTimeChange={onTimeChange}
onRefresh={onRefresh}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={updateButtonProps}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{isLoading && (
<EuiProgress size="xs" color="accent" data-test-subj="ruleEventLogListProgressBar" />
)}
<EuiBasicTable
data-test-subj="RuleErrorLog"
loading={isLoading}
items={logList ?? []}
columns={columns}
sorting={{ sort }}
pagination={pagination}
onChange={({ page: changedPage, sort: changedSort }) => {
if (changedPage) {
setPagination((prevPagination) => {
if (
prevPagination.pageIndex !== changedPage.index ||
prevPagination.pageSize !== changedPage.size
) {
return {
...prevPagination,
pageIndex: changedPage.index,
pageSize: changedPage.size,
};
}
return prevPagination;
});
}
if (changedSort) {
setSort((prevSort) => {
if (prevSort?.direction !== changedSort.direction) {
return changedSort;
}
return prevSort;
});
}
}}
/>
</div>
);
};
export const RuleErrorLogWithApi = withBulkRuleOperations(RuleErrorLog);
// eslint-disable-next-line import/no-default-export
export { RuleErrorLogWithApi as default };

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import datemath from '@elastic/datemath';
import {
@ -233,6 +233,8 @@ const updateButtonProps = {
export type RuleEventLogListProps = {
rule: Rule;
localStorageKey?: string;
refreshToken?: number;
requestRefresh?: () => Promise<void>;
} & Pick<RuleApis, 'loadExecutionLogAggregations'>;
export const RuleEventLogList = (props: RuleEventLogListProps) => {
@ -240,6 +242,7 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
rule,
localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY,
loadExecutionLogAggregations,
refreshToken,
} = props;
const { uiSettings, notifications } = useKibana().services;
@ -277,6 +280,8 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
);
});
const isInitialized = useRef(false);
// Main cell renderer, renders durations, statuses, etc.
const renderCell = ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => {
const { pageIndex, pageSize } = pagination;
@ -406,6 +411,14 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortingColumns, dateStart, dateEnd, filter, pagination.pageIndex, pagination.pageSize]);
useEffect(() => {
if (isInitialized.current) {
loadEventLogs();
}
isInitialized.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshToken]);
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns));
}, [localStorageKey, visibleColumns]);

View file

@ -77,6 +77,7 @@ export const RuleRoute: React.FunctionComponent<WithRuleSummaryProps> = ({
return ruleSummary ? (
<Rules
requestRefresh={requestRefresh}
refreshToken={refreshToken}
rule={rule}
ruleType={ruleType}
readOnly={readOnly}

View file

@ -43,6 +43,7 @@ export interface ComponentOpts {
snoozeRule: (snoozeEndTime: string | -1) => Promise<void>;
unsnoozeRule: () => Promise<void>;
isEditable: boolean;
direction?: 'column' | 'row';
}
export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
@ -53,6 +54,7 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
snoozeRule,
unsnoozeRule,
isEditable,
direction = 'column',
}: ComponentOpts) => {
const [isEnabled, setIsEnabled] = useState<boolean>(item.enabled);
const [isSnoozed, setIsSnoozed] = useState<boolean>(isItemSnoozed(item));
@ -70,6 +72,9 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
const onChangeEnabledStatus = useCallback(
async (enable: boolean) => {
if (item.enabled === enable) {
return;
}
setIsUpdating(true);
if (enable) {
await enableRule();
@ -80,7 +85,7 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
onRuleChanged();
setIsUpdating(false);
},
[setIsUpdating, isEnabled, setIsEnabled, onRuleChanged, enableRule, disableRule]
[item.enabled, isEnabled, onRuleChanged, enableRule, disableRule]
);
const onChangeSnooze = useCallback(
async (value: number, unit?: SnoozeUnit) => {
@ -136,10 +141,11 @@ export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
return (
<EuiFlexGroup
direction="column"
alignItems="flexStart"
direction={direction}
alignItems={direction === 'row' ? 'center' : 'flexStart'}
justifyContent="flexStart"
gutterSize="s"
gutterSize={direction === 'row' ? 's' : 'xs'}
responsive={false}
>
<EuiFlexItem grow={false}>
{isEditable ? (
@ -259,7 +265,7 @@ const RuleStatusMenu: React.FunctionComponent<RuleStatusMenuProps> = ({
},
];
return <EuiContextMenu initialPanelId={0} panels={panels} />;
return <EuiContextMenu data-test-subj="ruleStatusMenu" initialPanelId={0} panels={panels} />;
};
interface SnoozePanelProps {
@ -329,7 +335,7 @@ const SnoozePanel: React.FunctionComponent<SnoozePanelProps> = ({
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onClickApplyButton}>
<EuiButton onClick={onClickApplyButton} data-test-subj="ruleSnoozeApply">
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.applySnooze', {
defaultMessage: 'Apply',
})}
@ -380,7 +386,7 @@ const SnoozePanel: React.FunctionComponent<SnoozePanelProps> = ({
<EuiHorizontalRule margin="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiLink onClick={onApplyIndefinite}>
<EuiLink onClick={onApplyIndefinite} data-test-subj="ruleSnoozeIndefiniteApply">
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeIndefinitely', {
defaultMessage: 'Snooze indefinitely',
})}
@ -392,7 +398,7 @@ const SnoozePanel: React.FunctionComponent<SnoozePanelProps> = ({
<EuiHorizontalRule margin="s" />
<EuiFlexGroup>
<EuiFlexItem grow>
<EuiButton color="danger" onClick={onCancelSnooze}>
<EuiButton color="danger" onClick={onCancelSnooze} data-test-subj="ruleSnoozeCancel">
Cancel snooze
</EuiButton>
</EuiFlexItem>

View file

@ -831,7 +831,10 @@ export const RulesList: React.FunctionComponent = () => {
},
{
field: 'enabled',
name: '',
name: i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.triggerActionsTitle',
{ defaultMessage: 'Trigger actions' }
),
sortable: true,
truncateText: false,
width: '10%',

View file

@ -180,75 +180,90 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should disable the rule', async () => {
const enableSwitch = await testSubjects.find('enableSwitch');
const actionsDropdown = await testSubjects.find('statusDropdown');
const isChecked = await enableSwitch.getAttribute('aria-checked');
expect(isChecked).to.eql('true');
expect(await actionsDropdown.getVisibleText()).to.eql('Enabled');
await enableSwitch.click();
await actionsDropdown.click();
const actionsMenuElem = await testSubjects.find('ruleStatusMenu');
const actionsMenuItemElem = await actionsMenuElem.findAllByClassName('euiContextMenuItem');
const disableSwitchAfterDisabling = await testSubjects.find('enableSwitch');
const isCheckedAfterDisabling = await disableSwitchAfterDisabling.getAttribute(
'aria-checked'
);
expect(isCheckedAfterDisabling).to.eql('false');
await actionsMenuItemElem.at(1)?.click();
await retry.try(async () => {
expect(await actionsDropdown.getVisibleText()).to.eql('Disabled');
});
});
it('shouldnt allow you to mute a disabled rule', async () => {
const disabledEnableSwitch = await testSubjects.find('enableSwitch');
expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false');
it('shouldnt allow you to snooze a disabled rule', async () => {
const actionsDropdown = await testSubjects.find('statusDropdown');
const muteSwitch = await testSubjects.find('muteSwitch');
expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false');
expect(await actionsDropdown.getVisibleText()).to.eql('Disabled');
await muteSwitch.click();
await actionsDropdown.click();
const actionsMenuElem = await testSubjects.find('ruleStatusMenu');
const actionsMenuItemElem = await actionsMenuElem.findAllByClassName('euiContextMenuItem');
const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch');
const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute(
'aria-checked'
);
expect(isDisabledMuteAfterDisabling).to.eql('false');
expect(await actionsMenuItemElem.at(2)?.getVisibleText()).to.eql('Snooze');
expect(await actionsMenuItemElem.at(2)?.getAttribute('disabled')).to.eql('true');
// close the dropdown
await actionsDropdown.click();
});
it('should reenable a disabled the rule', async () => {
const enableSwitch = await testSubjects.find('enableSwitch');
const actionsDropdown = await testSubjects.find('statusDropdown');
const isChecked = await enableSwitch.getAttribute('aria-checked');
expect(isChecked).to.eql('false');
expect(await actionsDropdown.getVisibleText()).to.eql('Disabled');
await enableSwitch.click();
await actionsDropdown.click();
const actionsMenuElem = await testSubjects.find('ruleStatusMenu');
const actionsMenuItemElem = await actionsMenuElem.findAllByClassName('euiContextMenuItem');
const disableSwitchAfterReenabling = await testSubjects.find('enableSwitch');
const isCheckedAfterDisabling = await disableSwitchAfterReenabling.getAttribute(
'aria-checked'
);
expect(isCheckedAfterDisabling).to.eql('true');
await actionsMenuItemElem.at(0)?.click();
await retry.try(async () => {
expect(await actionsDropdown.getVisibleText()).to.eql('Enabled');
});
});
it('should mute the rule', async () => {
const muteSwitch = await testSubjects.find('muteSwitch');
it('should snooze the rule', async () => {
const actionsDropdown = await testSubjects.find('statusDropdown');
const isChecked = await muteSwitch.getAttribute('aria-checked');
expect(isChecked).to.eql('false');
expect(await actionsDropdown.getVisibleText()).to.eql('Enabled');
await muteSwitch.click();
await actionsDropdown.click();
const actionsMenuElem = await testSubjects.find('ruleStatusMenu');
const actionsMenuItemElem = await actionsMenuElem.findAllByClassName('euiContextMenuItem');
const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch');
const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked');
expect(isCheckedAfterDisabling).to.eql('true');
await actionsMenuItemElem.at(2)?.click();
const snoozeIndefinite = await testSubjects.find('ruleSnoozeIndefiniteApply');
await snoozeIndefinite.click();
await retry.try(async () => {
expect(await actionsDropdown.getVisibleText()).to.eql('Snoozed');
const remainingSnoozeTime = await testSubjects.find('remainingSnoozeTime');
expect(await remainingSnoozeTime.getVisibleText()).to.eql('Indefinitely');
});
});
it('should unmute the rule', async () => {
const muteSwitch = await testSubjects.find('muteSwitch');
it('should unsnooze the rule', async () => {
const actionsDropdown = await testSubjects.find('statusDropdown');
const isChecked = await muteSwitch.getAttribute('aria-checked');
expect(isChecked).to.eql('true');
expect(await actionsDropdown.getVisibleText()).to.eql('Snoozed');
await muteSwitch.click();
await actionsDropdown.click();
const actionsMenuElem = await testSubjects.find('ruleStatusMenu');
const actionsMenuItemElem = await actionsMenuElem.findAllByClassName('euiContextMenuItem');
const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch');
const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked');
expect(isCheckedAfterDisabling).to.eql('false');
await actionsMenuItemElem.at(2)?.click();
const snoozeCancel = await testSubjects.find('ruleSnoozeCancel');
await snoozeCancel.click();
await retry.try(async () => {
expect(await actionsDropdown.getVisibleText()).to.eql('Enabled');
});
});
});