mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Consolidate event logs UI (#136589)
* Move error action log endpoint to a new endpoint * consolidate run log and error log * Add new tests and fix existing tests * Fix lint * Fix lint * Add API integration tests * Remove invalid sorting columns * Addressed comments * Fix tests * Add utility to convert ES sort to event log sort, addressed comments * Fix jest tests * Fix type error * Address design feedback * Revert testing code to action executor * Address comments and add tests * remove unused translations * Address design feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8d88c7851c
commit
b2825cb9a8
20 changed files with 927 additions and 239 deletions
|
@ -877,7 +877,6 @@ export class RulesClient {
|
|||
const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId;
|
||||
|
||||
try {
|
||||
// Make sure user has access to this rule
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: rule.alertTypeId,
|
||||
consumer: rule.consumer,
|
||||
|
|
|
@ -30673,7 +30673,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "Alertes récupérées",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "Actions générées",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "Retard sur la planification",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.status": "Statut",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "Actions ayant réussi",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "Expiré",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "Horodatage",
|
||||
|
@ -30685,7 +30684,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "règle",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "Revenir en haut de la page.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "Alertes",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.errorLogTabText": "Log d'erreurs",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "Exécuter l'historique",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "Affichage de {range} sur {total, number} {type}",
|
||||
|
|
|
@ -30752,7 +30752,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "回復されたアラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "生成されたアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "遅延をスケジュール",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.status": "ステータス",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "成功したアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "タイムアウトしました",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "タイムスタンプ",
|
||||
|
@ -30764,7 +30763,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "ルール",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "最上部へ戻る。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "アラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.errorLogTabText": "エラーログ",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "実行履歴",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "{total, number} {type}件中{range}を表示しています",
|
||||
|
|
|
@ -30781,7 +30781,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "已恢复告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "生成的操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "计划延迟",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.status": "状态",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "成功的操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "已超时",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "时间戳",
|
||||
|
@ -30793,7 +30792,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "规则",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "返回顶部。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.errorLogTabText": "错误日志",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "运行历史记录",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "正在显示第 {range} 个(共 {total, number} 个){type}",
|
||||
|
|
|
@ -72,9 +72,13 @@ export const RULE_EXECUTION_LOG_ALERT_COUNT_COLUMNS = [
|
|||
'num_recovered_alerts',
|
||||
];
|
||||
|
||||
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [
|
||||
export const LOCKED_COLUMNS = [
|
||||
'timestamp',
|
||||
'execution_duration',
|
||||
'status',
|
||||
'message',
|
||||
'num_active_alerts',
|
||||
'num_errored_actions',
|
||||
];
|
||||
|
||||
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [...LOCKED_COLUMNS];
|
||||
|
|
|
@ -58,13 +58,13 @@ const getFilter = ({ runId, message }: { runId?: string; message?: string }) =>
|
|||
}
|
||||
|
||||
if (message) {
|
||||
filter.push(`message: "${message}"`);
|
||||
filter.push(`message: "${message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&')}"`);
|
||||
}
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
export const loadActionErrorLog = async ({
|
||||
export const loadActionErrorLog = ({
|
||||
id,
|
||||
http,
|
||||
dateStart,
|
||||
|
|
|
@ -84,7 +84,7 @@ describe('loadExecutionLogAggregations', () => {
|
|||
"query": Object {
|
||||
"date_end": "2022-03-23T16:17:53.482Z",
|
||||
"date_start": "2022-03-23T16:17:53.482Z",
|
||||
"filter": "event.outcome: success or unknown",
|
||||
"filter": "event.provider: alerting AND event.outcome: success or unknown",
|
||||
"page": 1,
|
||||
"per_page": 10,
|
||||
"sort": "[{\\"timestamp\\":{\\"order\\":\\"asc\\"}}]",
|
||||
|
|
|
@ -45,11 +45,11 @@ const getFilter = ({ outcomeFilter, message }: { outcomeFilter?: string[]; messa
|
|||
const filter: string[] = [];
|
||||
|
||||
if (outcomeFilter && outcomeFilter.length) {
|
||||
filter.push(`event.outcome: ${outcomeFilter.join(' or ')}`);
|
||||
filter.push(`event.provider: alerting AND event.outcome: ${outcomeFilter.join(' or ')}`);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
filter.push(`message: "${message}"`);
|
||||
filter.push(`message: "${message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&')}"`);
|
||||
}
|
||||
|
||||
return filter;
|
||||
|
|
|
@ -486,11 +486,6 @@ describe('tabbed content', () => {
|
|||
tabbedContent.update();
|
||||
});
|
||||
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeTruthy();
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeFalsy();
|
||||
|
||||
tabbedContent.find('[data-test-subj="ruleAlertListTab"]').simulate('click');
|
||||
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeFalsy();
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeTruthy();
|
||||
|
||||
|
@ -498,6 +493,11 @@ describe('tabbed content', () => {
|
|||
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeTruthy();
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeFalsy();
|
||||
|
||||
tabbedContent.find('[data-test-subj="ruleAlertListTab"]').simulate('click');
|
||||
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeFalsy();
|
||||
expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ import { suspendedComponentWithProps } from '../../../lib/suspended_component_wi
|
|||
import RuleStatusPanelWithApi from './rule_status_panel';
|
||||
|
||||
const RuleEventLogListWithApi = lazy(() => import('./rule_event_log_list'));
|
||||
const RuleErrorLogWithApi = lazy(() => import('./rule_error_log'));
|
||||
const RuleAlertList = lazy(() => import('./rule_alert_list'));
|
||||
|
||||
type RuleProps = {
|
||||
|
@ -62,7 +61,6 @@ 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,
|
||||
|
@ -113,17 +111,6 @@ export function RuleComponent({
|
|||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: EVENT_LOG_LIST_TAB,
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
|
||||
defaultMessage: 'History',
|
||||
}),
|
||||
'data-test-subj': 'eventLogListTab',
|
||||
content: suspendedComponentWithProps(
|
||||
RuleEventLogListWithApi,
|
||||
'xl'
|
||||
)({ requestRefresh, rule, refreshToken }),
|
||||
},
|
||||
{
|
||||
id: ALERT_LIST_TAB,
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', {
|
||||
|
@ -133,13 +120,13 @@ export function RuleComponent({
|
|||
content: renderRuleAlertList(),
|
||||
},
|
||||
{
|
||||
id: EVENT_ERROR_LOG_TAB,
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.errorLogTabText', {
|
||||
defaultMessage: 'Error log',
|
||||
id: EVENT_LOG_LIST_TAB,
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', {
|
||||
defaultMessage: 'History',
|
||||
}),
|
||||
'data-test-subj': 'errorLogTab',
|
||||
'data-test-subj': 'eventLogListTab',
|
||||
content: suspendedComponentWithProps(
|
||||
RuleErrorLogWithApi,
|
||||
RuleEventLogListWithApi,
|
||||
'xl'
|
||||
)({ requestRefresh, rule, refreshToken }),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiText } from '@elastic/eui';
|
||||
|
||||
export interface RuleActionErrorBadge {
|
||||
totalErrors: number;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export const RuleActionErrorBadge = (props: RuleActionErrorBadge) => {
|
||||
const { totalErrors, showIcon = false } = props;
|
||||
|
||||
return (
|
||||
<EuiBadge
|
||||
iconType={showIcon ? 'alert' : undefined}
|
||||
color={totalErrors ? 'danger' : 'hollow'}
|
||||
data-test-subj="ruleActionErrorBadge"
|
||||
>
|
||||
<EuiText size="s">{totalErrors}</EuiText>
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
|
||||
import { Rule } from '../../../../types';
|
||||
|
||||
jest.mock('../../../lib/rule_api/load_action_error_log', () => ({
|
||||
loadActionErrorLog: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseIsWithinBreakpoints = jest.fn();
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
...original,
|
||||
useIsWithinBreakpoints: () => mockUseIsWithinBreakpoints(),
|
||||
};
|
||||
});
|
||||
|
||||
const loadActionErrorLogMock = loadActionErrorLog as unknown as jest.MockedFunction<
|
||||
typeof loadActionErrorLog
|
||||
>;
|
||||
|
||||
const mockErrorLogResponse = {
|
||||
totalErrors: 1,
|
||||
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]?",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockRule: Rule = {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `rule-${uuid.v4()}`,
|
||||
tags: [],
|
||||
ruleTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
};
|
||||
|
||||
const mockExecution: any = {
|
||||
id: uuid.v4(),
|
||||
timestamp: '2022-03-20T07:40:44-07:00',
|
||||
duration: 5000000,
|
||||
status: 'success',
|
||||
message: 'rule execution #1',
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 2,
|
||||
num_new_alerts: 4,
|
||||
num_recovered_alerts: 3,
|
||||
num_triggered_actions: 10,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 4,
|
||||
total_search_duration: 1000000,
|
||||
es_search_duration: 1400000,
|
||||
schedule_delay: 2000000,
|
||||
timed_out: false,
|
||||
};
|
||||
|
||||
const mockClose = jest.fn();
|
||||
|
||||
describe('rule_action_error_log_flyout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
loadActionErrorLogMock.mockResolvedValue(mockErrorLogResponse);
|
||||
mockUseIsWithinBreakpoints.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="RuleErrorLog"]').exists()).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="ruleActionErrorLogFlyoutMessageText"]').first().text()
|
||||
).toEqual(mockExecution.message);
|
||||
expect(wrapper.find('[data-test-subj="ruleActionErrorBadge"]').first().text()).toEqual('4');
|
||||
});
|
||||
|
||||
it('can close the flyout', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
wrapper.find('[data-test-subj="ruleActionErrorLogFlyoutCloseButton"] button').simulate('click');
|
||||
|
||||
expect(mockClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('switches between push and overlay flyout depending on the size of the screen', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleActionErrorLogFlyout rule={mockRule} runLog={mockExecution} onClose={mockClose} />
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
mockUseIsWithinBreakpoints.mockReturnValue(false);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="ruleActionErrorLogFlyout"]').first().props().type
|
||||
).toEqual('push');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useIsWithinBreakpoints,
|
||||
EuiHorizontalRule,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { Rule } from '../../../../types';
|
||||
import { RuleErrorLogWithApi } from './rule_error_log';
|
||||
import { RuleActionErrorBadge } from './rule_action_error_badge';
|
||||
|
||||
export interface RuleActionErrorLogFlyoutProps {
|
||||
rule: Rule;
|
||||
runLog: IExecutionLog;
|
||||
refreshToken?: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const RuleActionErrorLogFlyout = (props: RuleActionErrorLogFlyoutProps) => {
|
||||
const { rule, runLog, refreshToken, onClose } = props;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { id, message, num_errored_actions: totalErrors } = runLog;
|
||||
|
||||
const isFlyoutPush = useIsWithinBreakpoints(['xl']);
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
type={isFlyoutPush ? 'push' : 'overlay'}
|
||||
onClose={onClose}
|
||||
size={isFlyoutPush ? 'm' : 'l'}
|
||||
data-test-subj="ruleActionErrorLogFlyout"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrors"
|
||||
defaultMessage="Errored Actions"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiText
|
||||
size="xs"
|
||||
style={{
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.message"
|
||||
defaultMessage="Message"
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText data-test-subj="ruleActionErrorLogFlyoutMessageText">{message}</EuiText>
|
||||
<EuiHorizontalRule size="full" />
|
||||
<div>
|
||||
<RuleActionErrorBadge totalErrors={totalErrors} />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrorsPlural"
|
||||
defaultMessage="{value, plural, one {errored action} other {errored actions}}"
|
||||
values={{
|
||||
value: totalErrors,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RuleErrorLogWithApi rule={rule} runId={id} refreshToken={refreshToken} />
|
||||
<EuiSpacer />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButton data-test-subj="ruleActionErrorLogFlyoutCloseButton" onClick={onClose}>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.close"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react'
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiProgress,
|
||||
|
@ -45,6 +46,13 @@ const API_FAILED_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.errorLogColumn.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search error log message',
|
||||
}
|
||||
);
|
||||
|
||||
const updateButtonProps = {
|
||||
iconOnly: true,
|
||||
fill: false,
|
||||
|
@ -54,12 +62,13 @@ const MAX_RESULTS = 1000;
|
|||
|
||||
export type RuleErrorLogProps = {
|
||||
rule: Rule;
|
||||
runId?: string;
|
||||
refreshToken?: number;
|
||||
requestRefresh?: () => Promise<void>;
|
||||
} & Pick<RuleApis, 'loadActionErrorLog'>;
|
||||
|
||||
export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
||||
const { rule, loadActionErrorLog, refreshToken } = props;
|
||||
const { rule, runId, loadActionErrorLog, refreshToken } = props;
|
||||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
|
@ -75,6 +84,9 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
direction: 'desc',
|
||||
});
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
|
||||
// Date related states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [dateStart, setDateStart] = useState<string>('now-24h');
|
||||
|
@ -120,6 +132,8 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
try {
|
||||
const result = await loadActionErrorLog({
|
||||
id: rule.id,
|
||||
runId,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
page: pagination.pageIndex,
|
||||
|
@ -152,6 +166,25 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
[setDateStart, setDateEnd]
|
||||
);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e) => {
|
||||
if (e.target.value === '') {
|
||||
setSearchText('');
|
||||
}
|
||||
setSearch(e.target.value);
|
||||
},
|
||||
[setSearchText, setSearch]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchText(search);
|
||||
}
|
||||
},
|
||||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
loadEventLogs();
|
||||
};
|
||||
|
@ -197,7 +230,15 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
useEffect(() => {
|
||||
loadEventLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dateStart, dateEnd, formattedSort, pagination.pageIndex, pagination.pageSize]);
|
||||
}, [
|
||||
dateStart,
|
||||
dateEnd,
|
||||
formattedSort,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
searchText,
|
||||
runId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
|
@ -211,6 +252,18 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
<div>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
{runId && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isClearable
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperDatePicker
|
||||
data-test-subj="ruleEventLogListDatePicker"
|
||||
|
|
|
@ -5,14 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridStyle,
|
||||
Pagination,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiBadge,
|
||||
EuiDataGridCellPopoverElementProps,
|
||||
useEuiTheme,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
IExecutionLog,
|
||||
|
@ -21,184 +31,25 @@ import {
|
|||
} from '@kbn/alerting-plugin/common';
|
||||
import { RuleEventLogListCellRenderer, ColumnId } from './rule_event_log_list_cell_renderer';
|
||||
import { RuleEventLogPaginationStatus } from './rule_event_log_pagination_status';
|
||||
import { RuleActionErrorBadge } from './rule_action_error_badge';
|
||||
import './rule_event_log_list.scss';
|
||||
|
||||
const getIsColumnSortable = (columnId: string) => {
|
||||
return executionLogSortableColumns.includes(columnId as ExecutionLogSortFields);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id',
|
||||
{
|
||||
defaultMessage: 'Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('id'),
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timestamp'),
|
||||
initialWidth: 250,
|
||||
},
|
||||
{
|
||||
id: 'execution_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration',
|
||||
{
|
||||
defaultMessage: 'Duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('execution_duration'),
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.status',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('status'),
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('message'),
|
||||
},
|
||||
{
|
||||
id: 'num_active_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts',
|
||||
{
|
||||
defaultMessage: 'Active alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_active_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_new_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts',
|
||||
{
|
||||
defaultMessage: 'New alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_new_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_recovered_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts',
|
||||
{
|
||||
defaultMessage: 'Recovered alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_recovered_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_triggered_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions',
|
||||
{
|
||||
defaultMessage: 'Triggered actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_triggered_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_generated_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions',
|
||||
{
|
||||
defaultMessage: 'Generated actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_generated_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_succeeded_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions',
|
||||
{
|
||||
defaultMessage: 'Succeeded actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_succeeded_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_errored_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions',
|
||||
{
|
||||
defaultMessage: 'Errored actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_errored_actions'),
|
||||
},
|
||||
{
|
||||
id: 'total_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration',
|
||||
{
|
||||
defaultMessage: 'Total search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('total_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'es_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration',
|
||||
{
|
||||
defaultMessage: 'ES search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('es_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'schedule_delay',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay',
|
||||
{
|
||||
defaultMessage: 'Schedule delay',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('schedule_delay'),
|
||||
},
|
||||
{
|
||||
id: 'timed_out',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut',
|
||||
{
|
||||
defaultMessage: 'Timed out',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
];
|
||||
const getErroredActionsTranslation = (errors: number) => {
|
||||
return i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsTooltip',
|
||||
{
|
||||
defaultMessage: '{value, plural, one {# errored action} other {# errored actions}}',
|
||||
values: { value: errors },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 50, 100];
|
||||
|
||||
const gridStyles: EuiDataGridStyle = {
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
};
|
||||
|
||||
export interface RuleEventLogDataGrid {
|
||||
logs: IExecutionLog[];
|
||||
pagination: Pagination;
|
||||
|
@ -206,8 +57,11 @@ export interface RuleEventLogDataGrid {
|
|||
visibleColumns: string[];
|
||||
dateFormat: string;
|
||||
pageSizeOptions?: number[];
|
||||
selectedRunLog?: IExecutionLog;
|
||||
onChangeItemsPerPage: (pageSize: number) => void;
|
||||
onChangePage: (pageIndex: number) => void;
|
||||
onFilterChange: (filter: string[]) => void;
|
||||
onFlyoutOpen: (runLog: IExecutionLog) => void;
|
||||
setVisibleColumns: (visibleColumns: string[]) => void;
|
||||
setSortingColumns: (sortingColumns: EuiDataGridSorting['columns']) => void;
|
||||
}
|
||||
|
@ -220,12 +74,260 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
pagination,
|
||||
dateFormat,
|
||||
visibleColumns,
|
||||
selectedRunLog,
|
||||
setVisibleColumns,
|
||||
setSortingColumns,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
onFilterChange,
|
||||
onFlyoutOpen,
|
||||
} = props;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const getPaginatedRowIndex = useCallback(
|
||||
(rowIndex: number) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return rowIndex - pageIndex * pageSize;
|
||||
},
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id',
|
||||
{
|
||||
defaultMessage: 'Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('id'),
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timestamp'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 250,
|
||||
},
|
||||
{
|
||||
id: 'execution_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration',
|
||||
{
|
||||
defaultMessage: 'Duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('execution_duration'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response',
|
||||
{
|
||||
defaultMessage: 'Response',
|
||||
}
|
||||
),
|
||||
actions: {
|
||||
showHide: false,
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
additional: [
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures',
|
||||
{
|
||||
defaultMessage: 'Show only failures',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange(['failure']),
|
||||
size: 'xs',
|
||||
},
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll',
|
||||
{
|
||||
defaultMessage: 'Show all',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange([]),
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
},
|
||||
isSortable: getIsColumnSortable('status'),
|
||||
isResizable: false,
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('message'),
|
||||
cellActions: [
|
||||
({ rowIndex, Component }) => {
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const runLog = logs[pagedRowIndex];
|
||||
const actionErrors = runLog?.num_errored_actions as number;
|
||||
if (actionErrors) {
|
||||
return (
|
||||
<Component onClick={() => onFlyoutOpen(runLog)} iconType="alert">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors"
|
||||
defaultMessage="View action errors"
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'num_active_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts',
|
||||
{
|
||||
defaultMessage: 'Active alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_active_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_new_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts',
|
||||
{
|
||||
defaultMessage: 'New alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_new_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_recovered_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts',
|
||||
{
|
||||
defaultMessage: 'Recovered alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_recovered_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_triggered_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions',
|
||||
{
|
||||
defaultMessage: 'Triggered actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_triggered_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_generated_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions',
|
||||
{
|
||||
defaultMessage: 'Generated actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_generated_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_succeeded_actions',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions',
|
||||
{
|
||||
defaultMessage: 'Succeeded actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_succeeded_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_errored_actions',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions',
|
||||
{
|
||||
defaultMessage: 'Errored actions',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_errored_actions'),
|
||||
},
|
||||
{
|
||||
id: 'total_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration',
|
||||
{
|
||||
defaultMessage: 'Total search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('total_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'es_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration',
|
||||
{
|
||||
defaultMessage: 'ES search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('es_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'schedule_delay',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay',
|
||||
{
|
||||
defaultMessage: 'Schedule delay',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('schedule_delay'),
|
||||
},
|
||||
{
|
||||
id: 'timed_out',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut',
|
||||
{
|
||||
defaultMessage: 'Timed out',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
],
|
||||
[getPaginatedRowIndex, onFlyoutOpen, onFilterChange, logs]
|
||||
);
|
||||
|
||||
const columnVisibilityProps = useMemo(
|
||||
() => ({
|
||||
visibleColumns,
|
||||
|
@ -252,20 +354,141 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
[pagination, pageSizeOptions, onChangeItemsPerPage, onChangePage]
|
||||
);
|
||||
|
||||
const rowClasses = useMemo(() => {
|
||||
if (!selectedRunLog) {
|
||||
return {};
|
||||
}
|
||||
const index = logs.findIndex((log) => log.id === selectedRunLog.id);
|
||||
return {
|
||||
[index]: 'ruleEventLogDataGrid--rowClassSelected',
|
||||
};
|
||||
}, [selectedRunLog, logs]);
|
||||
|
||||
const gridStyles: EuiDataGridStyle = useMemo(() => {
|
||||
return {
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
rowClasses,
|
||||
};
|
||||
}, [rowClasses]);
|
||||
|
||||
const renderMessageWithActionError = (
|
||||
columnId: string,
|
||||
errors: number,
|
||||
showTooltip: boolean = false
|
||||
) => {
|
||||
if (columnId !== 'message') {
|
||||
return null;
|
||||
}
|
||||
if (!errors) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
{showTooltip ? (
|
||||
<EuiToolTip content={getErroredActionsTranslation(errors)}>
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders the cell popover for runs with errored actions
|
||||
const renderCellPopover = (cellPopoverProps: EuiDataGridCellPopoverElementProps) => {
|
||||
const { columnId, rowIndex, cellActions, DefaultCellPopover } = cellPopoverProps;
|
||||
|
||||
if (columnId !== 'message') {
|
||||
return <DefaultCellPopover {...cellPopoverProps} />;
|
||||
}
|
||||
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const runLog = logs[pagedRowIndex];
|
||||
|
||||
if (!runLog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = runLog[columnId as keyof IExecutionLog] as string;
|
||||
const actionErrors = runLog.num_errored_actions || (0 as number);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<EuiSpacer size="s" />
|
||||
<div>
|
||||
<EuiText size="m">{value}</EuiText>
|
||||
</div>
|
||||
<EuiSpacer size="s" />
|
||||
{actionErrors > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{renderMessageWithActionError(columnId, actionErrors)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsCellPopover"
|
||||
defaultMessage="{value, plural, one {errored action} other {errored actions}}"
|
||||
values={{
|
||||
value: actionErrors,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{cellActions}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main cell renderer, renders durations, statuses, etc.
|
||||
const renderCell = ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
const pagedRowIndex = rowIndex - pageIndex * pageSize;
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
|
||||
const runLog = logs[pagedRowIndex];
|
||||
const value = logs[pagedRowIndex]?.[columnId as keyof IExecutionLog] as string;
|
||||
const actionErrors = logs[pagedRowIndex]?.num_errored_actions || (0 as number);
|
||||
const version = logs?.[pagedRowIndex]?.version;
|
||||
|
||||
if (columnId === 'num_errored_actions' && runLog) {
|
||||
return (
|
||||
<EuiBadge
|
||||
data-test-subj="ruleEventLogDataGridErroredActionBadge"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
color="hollow"
|
||||
onClick={() => onFlyoutOpen(runLog)}
|
||||
onClickAriaLabel={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.openActionErrorsFlyout',
|
||||
{
|
||||
defaultMessage: 'Open action errors flyout',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RuleEventLogListCellRenderer
|
||||
columnId={columnId as ColumnId}
|
||||
value={value}
|
||||
version={version}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{renderMessageWithActionError(columnId, actionErrors, true)}
|
||||
<EuiFlexItem>
|
||||
<RuleEventLogListCellRenderer
|
||||
columnId={columnId as ColumnId}
|
||||
value={value}
|
||||
version={version}
|
||||
dateFormat={dateFormat}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -286,6 +509,7 @@ export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
|||
sorting={sortingProps}
|
||||
pagination={paginationProps}
|
||||
gridStyle={gridStyles}
|
||||
renderCellPopover={renderCellPopover}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
.ruleEventLogDataGrid--rowClassSelected {
|
||||
background-color: $euiColorHighlight;
|
||||
}
|
|
@ -17,9 +17,17 @@ import { RuleEventLogList } from './rule_event_log_list';
|
|||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS } from '../../../constants';
|
||||
import { Rule } from '../../../../types';
|
||||
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../lib/rule_api/load_action_error_log', () => ({
|
||||
loadActionErrorLog: jest.fn(),
|
||||
}));
|
||||
|
||||
const loadActionErrorLogMock = loadActionErrorLog as unknown as jest.MockedFunction<
|
||||
typeof loadActionErrorLog
|
||||
>;
|
||||
|
||||
const mockLogResponse: any = {
|
||||
data: [
|
||||
|
@ -124,6 +132,19 @@ const mockRule: Rule = {
|
|||
},
|
||||
};
|
||||
|
||||
const mockErrorLogResponse = {
|
||||
totalErrors: 1,
|
||||
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]?",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const loadExecutionLogAggregationsMock = jest.fn();
|
||||
|
||||
describe('rule_event_log_list', () => {
|
||||
|
@ -140,6 +161,7 @@ describe('rule_event_log_list', () => {
|
|||
];
|
||||
}
|
||||
});
|
||||
loadActionErrorLogMock.mockResolvedValue(mockErrorLogResponse);
|
||||
loadExecutionLogAggregationsMock.mockResolvedValue(mockLogResponse);
|
||||
});
|
||||
|
||||
|
@ -217,16 +239,11 @@ describe('rule_event_log_list', () => {
|
|||
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
id: mockRule.id,
|
||||
sort: [
|
||||
{
|
||||
timestamp: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
message: '',
|
||||
outcomeFilter: [],
|
||||
page: 0,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -246,13 +263,7 @@ describe('rule_event_log_list', () => {
|
|||
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
id: mockRule.id,
|
||||
sort: [
|
||||
{
|
||||
timestamp: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
outcomeFilter: [],
|
||||
page: 0,
|
||||
perPage: 10,
|
||||
|
@ -287,7 +298,7 @@ describe('rule_event_log_list', () => {
|
|||
timestamp: { order: 'desc' },
|
||||
},
|
||||
{
|
||||
execution_duration: { order: 'asc' },
|
||||
execution_duration: { order: 'desc' },
|
||||
},
|
||||
],
|
||||
outcomeFilter: [],
|
||||
|
@ -501,7 +512,7 @@ describe('rule_event_log_list', () => {
|
|||
JSON.parse(
|
||||
localStorage.getItem('xpack.triggersActionsUI.ruleEventLogList.initialColumns') ?? 'null'
|
||||
)
|
||||
).toEqual([...RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, 'num_active_alerts']);
|
||||
).toEqual(['timestamp', 'execution_duration', 'status', 'message', 'num_errored_actions']);
|
||||
});
|
||||
|
||||
it('does not show the refine search prompt normally', async () => {
|
||||
|
@ -656,4 +667,51 @@ describe('rule_event_log_list', () => {
|
|||
'Showing 81 - 85 of 85 log entries'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders errored action badges in message rows', async () => {
|
||||
loadExecutionLogAggregationsMock.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: uuid.v4(),
|
||||
timestamp: '2022-03-20T07:40:44-07:00',
|
||||
duration: 5000000,
|
||||
status: 'success',
|
||||
message: 'rule execution #1',
|
||||
version: '8.2.0',
|
||||
num_active_alerts: 2,
|
||||
num_new_alerts: 4,
|
||||
num_recovered_alerts: 3,
|
||||
num_triggered_actions: 10,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 4,
|
||||
total_search_duration: 1000000,
|
||||
es_search_duration: 1400000,
|
||||
schedule_delay: 2000000,
|
||||
timed_out: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogList
|
||||
rule={mockRule}
|
||||
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleActionErrorBadge"]').first().text()).toEqual('4');
|
||||
|
||||
// Click to open flyout
|
||||
wrapper
|
||||
.find('[data-test-subj="ruleEventLogDataGridErroredActionBadge"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="ruleActionErrorLogFlyout"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react'
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiProgress,
|
||||
|
@ -20,10 +21,11 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS } from '../../../constants';
|
||||
import { RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS, LOCKED_COLUMNS } from '../../../constants';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { RuleEventLogDataGrid } from './rule_event_log_data_grid';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
|
||||
|
@ -47,8 +49,20 @@ const API_FAILED_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search event log message',
|
||||
}
|
||||
);
|
||||
|
||||
const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns';
|
||||
|
||||
const getDefaultColumns = (columns: string[]) => {
|
||||
const columnsWithoutLockedColumn = columns.filter((column) => !LOCKED_COLUMNS.includes(column));
|
||||
return [...LOCKED_COLUMNS, ...columnsWithoutLockedColumn];
|
||||
};
|
||||
|
||||
const updateButtonProps = {
|
||||
iconOnly: true,
|
||||
fill: false,
|
||||
|
@ -74,12 +88,17 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
|
||||
const [selectedRunLog, setSelectedRunLog] = useState<IExecutionLog | undefined>();
|
||||
|
||||
// Data grid states
|
||||
const [logs, setLogs] = useState<IExecutionLog[]>();
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
|
||||
return (
|
||||
return getDefaultColumns(
|
||||
JSON.parse(localStorage.getItem(localStorageKey) ?? 'null') ||
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS
|
||||
);
|
||||
});
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
|
@ -131,6 +150,7 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
id: rule.id,
|
||||
sort: formattedSort as LoadExecutionLogAggregationsProps['sort'],
|
||||
outcomeFilter: filter,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
page: pagination.pageIndex,
|
||||
|
@ -198,6 +218,35 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
[setPagination, setFilter]
|
||||
);
|
||||
|
||||
const onFlyoutOpen = useCallback((runLog: IExecutionLog) => {
|
||||
setIsFlyoutOpen(true);
|
||||
setSelectedRunLog(runLog);
|
||||
}, []);
|
||||
|
||||
const onFlyoutClose = useCallback(() => {
|
||||
setIsFlyoutOpen(false);
|
||||
setSelectedRunLog(undefined);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e) => {
|
||||
if (e.target.value === '') {
|
||||
setSearchText('');
|
||||
}
|
||||
setSearch(e.target.value);
|
||||
},
|
||||
[setSearchText, setSearch]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchText(search);
|
||||
}
|
||||
},
|
||||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const renderList = () => {
|
||||
if (!logs) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
|
@ -213,8 +262,11 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
sortingColumns={sortingColumns}
|
||||
visibleColumns={visibleColumns}
|
||||
dateFormat={dateFormat}
|
||||
selectedRunLog={selectedRunLog}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
onFlyoutOpen={onFlyoutOpen}
|
||||
onFilterChange={setFilter}
|
||||
setVisibleColumns={setVisibleColumns}
|
||||
setSortingColumns={setSortingColumns}
|
||||
/>
|
||||
|
@ -225,7 +277,15 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
useEffect(() => {
|
||||
loadEventLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sortingColumns, dateStart, dateEnd, filter, pagination.pageIndex, pagination.pageSize]);
|
||||
}, [
|
||||
sortingColumns,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
searchText,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
|
@ -243,6 +303,16 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
<div style={ruleEventListContainerStyle}>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isClearable
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleEventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
</EuiFlexItem>
|
||||
|
@ -270,6 +340,14 @@ export const RuleEventLogList = (props: RuleEventLogListProps) => {
|
|||
backToTopAnchor="rule_event_log_list"
|
||||
/>
|
||||
)}
|
||||
{isFlyoutOpen && selectedRunLog && (
|
||||
<RuleActionErrorLogFlyout
|
||||
rule={rule}
|
||||
runLog={selectedRunLog}
|
||||
refreshToken={refreshToken}
|
||||
onClose={onFlyoutClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import { RuleDurationFormat } from '../../rules_list/components/rule_duration_fo
|
|||
|
||||
describe('rule_event_log_list_cell_renderer', () => {
|
||||
it('renders primitive values correctly', () => {
|
||||
const wrapper = shallow(<RuleEventLogListCellRenderer columnId="message" value="test" />);
|
||||
const wrapper = mount(<RuleEventLogListCellRenderer columnId="message" value="test" />);
|
||||
|
||||
expect(wrapper.text()).toEqual('test');
|
||||
});
|
||||
|
|
|
@ -55,7 +55,7 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel"
|
||||
defaultMessage="Status"
|
||||
defaultMessage="Response"
|
||||
/>
|
||||
</EuiFilterButton>
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue