[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:
Jiawei Wu 2022-07-25 12:08:31 -07:00 committed by GitHub
parent 8d88c7851c
commit b2825cb9a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 927 additions and 239 deletions

View file

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

View file

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

View file

@ -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}を表示しています",

View file

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

View file

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

View file

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

View file

@ -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\\"}}]",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,7 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel"
defaultMessage="Status"
defaultMessage="Response"
/>
</EuiFilterButton>
}