mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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:
parent
8374636625
commit
2aa076a33a
18 changed files with 1066 additions and 655 deletions
|
@ -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;
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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', () => ({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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": "ルール",
|
||||
|
|
|
@ -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": "规则",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<b>{getRuleStatusErrorReasonText()}</b>,
|
||||
{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>
|
||||
|
||||
<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" />
|
||||
|
||||
{getRuleStatusWarningReasonText()}
|
||||
|
||||
{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" />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.actionWithBrokenConnectorWarningBannerTitle"
|
||||
defaultMessage="There is an issue with one of the connectors associated with this rule."
|
||||
/>
|
||||
|
||||
{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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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]);
|
||||
|
|
|
@ -77,6 +77,7 @@ export const RuleRoute: React.FunctionComponent<WithRuleSummaryProps> = ({
|
|||
return ruleSummary ? (
|
||||
<Rules
|
||||
requestRefresh={requestRefresh}
|
||||
refreshToken={refreshToken}
|
||||
rule={rule}
|
||||
ruleType={ruleType}
|
||||
readOnly={readOnly}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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%',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue