[RAM] Make Global Event Log Shareable (#163668)

## Summary
Resolves: https://github.com/elastic/kibana/issues/161788

Makes the global event log shareable. Plus some refactors like
converting the `rule_event_log_list_table` fetch to use React Query.

Also, fixed a bug with the `rule_status_panel` where we did not refresh
if the parent component refreshed.

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2023-08-23 09:20:34 -07:00 committed by GitHub
parent 03efa64480
commit ba96a720f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1374 additions and 893 deletions

View file

@ -266,6 +266,7 @@ enabled:
- x-pack/test/functional_with_es_ssl/apps/cases/group2/config.ts
- x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/shared/config.ts
- x-pack/test/functional/apps/advanced_settings/config.ts
- x-pack/test/functional/apps/aiops/config.ts
- x-pack/test/functional/apps/api_keys/config.ts

View file

@ -13,6 +13,9 @@
"developerExamples",
"kibanaReact",
"cases"
]
],
"optionalPlugins": [
"spaces"
],
}
}

View file

@ -25,6 +25,7 @@ import { RulesListNotifyBadgeSandbox } from './components/rules_list_notify_badg
import { RuleTagBadgeSandbox } from './components/rule_tag_badge_sandbox';
import { RuleTagFilterSandbox } from './components/rule_tag_filter_sandbox';
import { RuleEventLogListSandbox } from './components/rule_event_log_list_sandbox';
import { GlobalRuleEventLogListSandbox } from './components/global_rule_event_log_list_sandbox';
import { RuleStatusDropdownSandbox } from './components/rule_status_dropdown_sandbox';
import { RuleStatusFilterSandbox } from './components/rule_status_filter_sandbox';
import { AlertsTableSandbox } from './components/alerts_table_sandbox';
@ -102,6 +103,14 @@ const TriggersActionsUiExampleApp = ({
</Page>
)}
/>
<Route
path="/global_rule_event_log_list"
render={() => (
<Page title="Global Run History List">
<GlobalRuleEventLogListSandbox triggersActionsUi={triggersActionsUi} />
</Page>
)}
/>
<Route
path="/rule_status_dropdown"
render={() => (

View file

@ -0,0 +1,24 @@
/*
* 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 { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
interface SandboxProps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const GlobalRuleEventLogListSandbox = ({ triggersActionsUi }: SandboxProps) => {
return (
<div style={{ height: '400px' }}>
{triggersActionsUi.getGlobalRuleEventLogList({
localStorageKey: 'test-local-storage-key',
filteredRuleTypes: ['apm.error_rate', 'apm.transaction_error_rate'],
})}
</div>
);
};

View file

@ -49,6 +49,11 @@ export const Sidebar = () => {
name: 'Run History List',
onClick: () => history.push(`/rule_event_log_list`),
},
{
id: 'global_rule_event_log_list',
name: 'Global Run History List',
onClick: () => history.push(`/global_rule_event_log_list`),
},
{
id: 'rule_status_dropdown',
name: 'Rule Status Dropdown',

View file

@ -95,7 +95,10 @@ export const LOCKED_COLUMNS = [
];
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [...LOCKED_COLUMNS.slice(1)];
export const GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = ['rule_name', ...LOCKED_COLUMNS];
export const GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [
'rule_name',
...LOCKED_COLUMNS.slice(1),
];
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
export const CONNECTOR_EXECUTION_LOG_COLUMN_IDS = [

View file

@ -23,7 +23,9 @@ import { useKibana } from '../common/lib/kibana';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
const RulesList = lazy(() => import('./sections/rules_list/components/rules_list'));
const LogsList = lazy(() => import('./sections/logs_list/components/logs_list'));
const LogsList = lazy(
() => import('./sections/rule_details/components/global_rule_event_log_list')
);
const AlertsPage = lazy(() => import('./sections/alerts_table/alerts_page'));
export interface MatchParams {

View file

@ -0,0 +1,70 @@
/*
* 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 { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import datemath from '@kbn/datemath';
import { useKibana } from '../../common/lib/kibana';
import {
loadExecutionLogAggregations,
loadGlobalExecutionLogAggregations,
LoadExecutionLogAggregationsProps,
LoadGlobalExecutionLogAggregationsProps,
} from '../lib/rule_api/load_execution_log_aggregations';
const getParsedDate = (date: string) => {
if (date.includes('now')) {
return datemath.parse(date)?.format() || date;
}
return date;
};
interface CommonProps {
onError?: (err: any) => void;
}
type LoadExecutionLogProps = LoadExecutionLogAggregationsProps & CommonProps;
type LoadGlobalExecutionLogProps = LoadGlobalExecutionLogAggregationsProps & CommonProps;
export type UseLoadRuleEventLogsProps = LoadExecutionLogProps | LoadGlobalExecutionLogProps;
const isGlobal = (props: UseLoadRuleEventLogsProps): props is LoadGlobalExecutionLogProps => {
return (props as LoadExecutionLogAggregationsProps).id === '*';
};
export function useLoadRuleEventLogs(props: UseLoadRuleEventLogsProps) {
const { http } = useKibana().services;
const queryFn = useCallback(() => {
if (isGlobal(props)) {
return loadGlobalExecutionLogAggregations({
http,
...props,
dateStart: getParsedDate(props.dateStart),
...(props.dateEnd ? { dateEnd: getParsedDate(props.dateEnd) } : {}),
});
}
return loadExecutionLogAggregations({
http,
...props,
dateStart: getParsedDate(props.dateStart),
...(props.dateEnd ? { dateEnd: getParsedDate(props.dateEnd) } : {}),
});
}, [props, http]);
const { data, isLoading, isFetching, refetch } = useQuery({
queryKey: ['loadRuleEventLog', props],
queryFn,
onError: props.onError,
refetchOnWindowFocus: false,
});
return {
data,
isLoading: isLoading || isFetching,
loadEventLogs: refetch,
};
}

View file

@ -34,4 +34,10 @@ describe('getFilter', () => {
test('should not return filter if outcome filter is invalid', () => {
expect(getFilter({ outcomeFilter: ['doesntexist'] })).toEqual([]);
});
test('should return ruleTypeId filter', () => {
expect(getFilter({ ruleTypeIds: ['test-1', 'test-2'] })).toEqual([
'kibana.alert.rule.rule_type_id:(test-1 or test-2)',
]);
});
});

View file

@ -10,10 +10,12 @@ export const getFilter = ({
message,
outcomeFilter,
runId,
ruleTypeIds,
}: {
message?: string;
outcomeFilter?: string[];
runId?: string;
ruleTypeIds?: string[];
}) => {
const filter: string[] = [];
@ -33,6 +35,10 @@ export const getFilter = ({
filter.push(`kibana.alert.rule.execution.uuid: ${runId}`);
}
if (ruleTypeIds?.length) {
filter.push(`kibana.alert.rule.rule_type_id:(${ruleTypeIds.join(' or ')})`);
}
return filter;
};

View file

@ -16,6 +16,7 @@ export interface LoadExecutionKPIAggregationsProps {
message?: string;
dateStart: string;
dateEnd?: string;
ruleTypeIds?: string[];
}
export const loadExecutionKPIAggregations = ({
@ -25,8 +26,9 @@ export const loadExecutionKPIAggregations = ({
message,
dateStart,
dateEnd,
ruleTypeIds,
}: LoadExecutionKPIAggregationsProps & { http: HttpSetup }) => {
const filter = getFilter({ outcomeFilter, message });
const filter = getFilter({ outcomeFilter, message, ruleTypeIds });
return http.get<IExecutionKPIResult>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_kpi`,

View file

@ -53,6 +53,7 @@ export interface LoadExecutionLogAggregationsProps {
dateStart: string;
dateEnd?: string;
outcomeFilter?: string[];
ruleTypeIds?: string[];
message?: string;
perPage?: number;
page?: number;
@ -70,13 +71,14 @@ export const loadExecutionLogAggregations = async ({
dateStart,
dateEnd,
outcomeFilter,
ruleTypeIds,
message,
perPage = 10,
page = 0,
sort = [],
}: LoadExecutionLogAggregationsProps & { http: HttpSetup }) => {
const sortField: any[] = sort;
const filter = getFilter({ outcomeFilter, message });
const filter = getFilter({ outcomeFilter, message, ruleTypeIds });
const result = await http.get<AsApiContract<IExecutionLogResult>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_log`,
@ -102,6 +104,7 @@ export const loadGlobalExecutionLogAggregations = async ({
dateStart,
dateEnd,
outcomeFilter,
ruleTypeIds,
message,
perPage = 10,
page = 0,
@ -109,7 +112,7 @@ export const loadGlobalExecutionLogAggregations = async ({
namespaces,
}: LoadGlobalExecutionLogAggregationsProps & { http: HttpSetup }) => {
const sortField: any[] = sort;
const filter = getFilter({ outcomeFilter, message });
const filter = getFilter({ outcomeFilter, message, ruleTypeIds });
const result = await http.get<AsApiContract<IExecutionLogResult>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_logs`,

View file

@ -17,6 +17,7 @@ export interface LoadGlobalExecutionKPIAggregationsProps {
dateStart: string;
dateEnd?: string;
namespaces?: Array<string | undefined>;
ruleTypeIds?: string[];
}
export const loadGlobalExecutionKPIAggregations = ({
@ -27,8 +28,9 @@ export const loadGlobalExecutionKPIAggregations = ({
dateStart,
dateEnd,
namespaces,
ruleTypeIds,
}: LoadGlobalExecutionKPIAggregationsProps & { http: HttpSetup }) => {
const filter = getFilter({ outcomeFilter, message });
const filter = getFilter({ outcomeFilter, message, ruleTypeIds });
return http.get<IExecutionKPIResult>(`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_kpi`, {
query: {

View file

@ -33,7 +33,7 @@ import { bulkActionsReducer } from './bulk_actions/reducer';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { getCasesMockMap } from './cases/index.mock';
import { getMaintenanceWindowMockMap } from './maintenance_windows/index.mock';
import { createAppMockRenderer } from '../test_utils';
import { createAppMockRenderer, getJsDomPerformanceFix } from '../test_utils';
import { createCasesServiceMock } from './index.mock';
import { useCaseViewNavigation } from './cases/use_case_view_navigation';
import { act } from 'react-dom/test-utils';
@ -196,41 +196,14 @@ const mockedUseCellActions: UseCellActions = () => {
};
};
const originalGetComputedStyle = Object.assign({}, window.getComputedStyle);
const { fix, cleanup } = getJsDomPerformanceFix();
beforeAll(() => {
// The JSDOM implementation is too slow
// Especially for dropdowns that try to position themselves
// perf issue - https://github.com/jsdom/jsdom/issues/3234
Object.defineProperty(window, 'getComputedStyle', {
value: (el: HTMLElement) => {
/**
* This is based on the jsdom implementation of getComputedStyle
* https://github.com/jsdom/jsdom/blob/9dae17bf0ad09042cfccd82e6a9d06d3a615d9f4/lib/jsdom/browser/Window.js#L779-L820
*
* It is missing global style parsing and will only return styles applied directly to an element.
* Will not return styles that are global or from emotion
*/
const declaration = new CSSStyleDeclaration();
const { style } = el;
Array.prototype.forEach.call(style, (property: string) => {
declaration.setProperty(
property,
style.getPropertyValue(property),
style.getPropertyPriority(property)
);
});
return declaration;
},
configurable: true,
writable: true,
});
fix();
});
afterAll(() => {
Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle);
cleanup();
});
describe('AlertsTable', () => {

View file

@ -46,6 +46,7 @@ export const EventLogListStatusFilter = (props: EventLogListStatusFilterProps) =
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
data-test-subj="eventLogStatusFilter"
button={
<EuiFilterButton
data-test-subj="eventLogStatusFilterButton"

View file

@ -40,7 +40,7 @@ export const RefineSearchPrompt = (props: RefineSearchFooterProps) => {
}
return (
<EuiText style={textStyles} textAlign="center" size="s">
<EuiText style={textStyles} textAlign="center" size="s" data-test-subj="refineSearchPrompt">
<FormattedMessage
id="xpack.triggersActionsUI.sections.refineSearchPrompt.prompt"
defaultMessage="These are the first {visibleDocumentSize} documents matching your search, refine your search to see others."

View file

@ -61,3 +61,6 @@ export const RuleTagBadge = suspendedComponentWithProps(
export const RuleStatusPanel = suspendedComponentWithProps(
lazy(() => import('./rule_details/components/rule_status_panel'))
);
export const GlobalRuleEventLogList = suspendedComponentWithProps(
lazy(() => import('./rule_details/components/global_rule_event_log_list'))
);

View file

@ -1,44 +0,0 @@
/*
* 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 { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
import {
RuleEventLogListTableWithApi,
RuleEventLogListCommonProps,
} from '../../rule_details/components/rule_event_log_list_table';
const GLOBAL_EVENT_LOG_LIST_STORAGE_KEY =
'xpack.triggersActionsUI.globalEventLogList.initialColumns';
export const LogsList = ({
setHeaderActions,
}: {
setHeaderActions: RuleEventLogListCommonProps['setHeaderActions'];
}) => {
return suspendedComponentWithProps(
RuleEventLogListTableWithApi,
'xl'
)({
ruleId: '*',
refreshToken: {
resolve: () => {
/* noop */
},
reject: () => {
/* noop */
},
},
initialPageSize: 50,
hasRuleNames: true,
hasAllSpaceSwitch: true,
localStorageKey: GLOBAL_EVENT_LOG_LIST_STORAGE_KEY,
setHeaderActions,
});
};
// eslint-disable-next-line import/no-default-export
export default LogsList;

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback } from 'react';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { RuleEventLogListTable, RuleEventLogListCommonProps } from './rule_event_log_list_table';
import { useKibana } from '../../../../common/lib/kibana';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export interface GlobalRuleEventLogListProps {
setHeaderActions?: RuleEventLogListCommonProps['setHeaderActions'];
localStorageKey?: RuleEventLogListCommonProps['localStorageKey'];
filteredRuleTypes?: RuleEventLogListCommonProps['filteredRuleTypes'];
}
const GLOBAL_EVENT_LOG_LIST_STORAGE_KEY =
'xpack.triggersActionsUI.globalEventLogList.initialColumns';
const REFRESH_TOKEN = {
resolve: () => {
/* noop */
},
reject: () => {
/* noop */
},
};
export const GlobalRuleEventLogList = (props: GlobalRuleEventLogListProps) => {
const { setHeaderActions, localStorageKey, filteredRuleTypes } = props;
const { spaces } = useKibana().services;
// eslint-disable-next-line react-hooks/exhaustive-deps
const SpacesContextWrapper = useCallback(
spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spaces]
);
return (
<SpacesContextWrapper feature="triggersActions">
<RuleEventLogListTable
ruleId={'*'}
refreshToken={REFRESH_TOKEN}
initialPageSize={50}
hasRuleNames={true}
hasAllSpaceSwitch={true}
localStorageKey={localStorageKey || GLOBAL_EVENT_LOG_LIST_STORAGE_KEY}
filteredRuleTypes={filteredRuleTypes}
setHeaderActions={setHeaderActions}
/>
</SpacesContextWrapper>
);
};
// eslint-disable-next-line import/no-default-export
export { GlobalRuleEventLogList as default };

View file

@ -7,15 +7,16 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import type { Capabilities } from '@kbn/core/public';
import { coreMock } from '@kbn/core/public/mocks';
import { RuleComponent, alertToListItem } from './rule';
import { RuleComponent, alertToListItem, RuleComponentProps } from './rule';
import { AlertListItem } from './types';
import { RuleAlertList } from './rule_alert_list';
import { RuleSummary, AlertStatus, RuleType, RuleTypeModel } from '../../../../types';
import { mockRule } from './test_helpers';
import { mockRule, mockLogResponse } from './test_helpers';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
import { useKibana } from '../../../../common/lib/kibana';
import { useBulkGetMaintenanceWindows } from '../../alerts_table/hooks/use_bulk_get_maintenance_windows';
@ -26,6 +27,13 @@ jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('../../alerts_table/hooks/use_bulk_get_maintenance_windows');
jest.mock('../../../lib/rule_api/load_execution_log_aggregations', () => ({
loadExecutionLogAggregations: jest.fn(),
}));
const { loadExecutionLogAggregations } = jest.requireMock(
'../../../lib/rule_api/load_execution_log_aggregations'
);
const mocks = coreMock.createSetup();
@ -85,8 +93,30 @@ beforeEach(() => {
data: maintenanceWindowsMap,
isFetching: false,
});
loadExecutionLogAggregations.mockResolvedValue(mockLogResponse);
});
afterEach(() => {
jest.clearAllMocks();
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
const RuleComponentWithProvider = (props: RuleComponentProps) => {
return (
<QueryClientProvider client={queryClient}>
<RuleComponent {...props} />
</QueryClientProvider>
);
};
describe('rules', () => {
it('render a list of rules', async () => {
const rule = mockRule();
@ -116,7 +146,7 @@ describe('rules', () => {
];
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}
@ -171,7 +201,7 @@ describe('rules', () => {
};
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}
@ -202,7 +232,7 @@ describe('rules', () => {
const ruleUsEast: AlertStatus = { status: 'OK', muted: false, flapping: false };
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}
@ -348,7 +378,7 @@ describe('execution duration overview', () => {
const ruleSummary = mockRuleSummary();
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}
@ -375,7 +405,7 @@ describe('disable/enable functionality', () => {
const ruleType = mockRuleType();
const ruleSummary = mockRuleSummary();
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}
@ -397,7 +427,7 @@ describe('disable/enable functionality', () => {
const ruleType = mockRuleType();
const ruleSummary = mockRuleSummary();
const wrapper = mountWithIntl(
<RuleComponent
<RuleComponentWithProvider
{...mockAPIs}
rule={rule}
ruleType={ruleType}

View file

@ -35,7 +35,7 @@ const RuleEventLogList = lazy(() => import('./rule_event_log_list'));
const RuleAlertList = lazy(() => import('./rule_alert_list'));
const RuleDefinition = lazy(() => import('./rule_definition'));
type RuleProps = {
export type RuleComponentProps = {
rule: Rule;
ruleType: RuleType;
readOnly: boolean;
@ -64,7 +64,7 @@ export function RuleComponent({
onChangeDuration,
durationEpoch = Date.now(),
isLoadingChart,
}: RuleProps) {
}: RuleComponentProps) {
const { ruleTypeRegistry, actionTypeRegistry } = useKibana().services;
const alerts = Object.entries(ruleSummary.alerts)
@ -152,6 +152,7 @@ export function RuleComponent({
healthColor={healthColor}
statusMessage={statusMessage}
requestRefresh={requestRefresh}
refreshToken={refreshToken}
/>
</EuiFlexItem>
{suspendedComponentWithProps(

View file

@ -6,37 +6,47 @@
*/
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { act } from 'react-dom/test-utils';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { render, screen, waitFor, cleanup } from '@testing-library/react';
import { useKibana } from '../../../../common/lib/kibana';
import { ActionGroup, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { EuiSuperDatePicker, EuiDataGrid } from '@elastic/eui';
import { EventLogListStatusFilter } from '../../common/components/event_log';
import { RuleEventLogList } from './rule_event_log_list';
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
import {
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
} from '../../../constants';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { RuleEventLogList, RuleEventLogListProps } from './rule_event_log_list';
import { mockRule, mockRuleType, mockRuleSummary, mockLogResponse } from './test_helpers';
import { RuleType } from '../../../../types';
import { loadActionErrorLog } from '../../../lib/rule_api/load_action_error_log';
import { getJsDomPerformanceFix } from '../../test_utils';
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(),
}));
jest.mock('../../../lib/rule_api/load_execution_log_aggregations', () => ({
loadExecutionLogAggregations: jest.fn(),
}));
jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('../../../hooks/use_load_rule_event_logs', () => ({
useLoadRuleEventLogs: jest.fn(),
}));
const { getIsExperimentalFeatureEnabled } = jest.requireMock(
'../../../../common/get_experimental_features'
);
const { useLoadRuleEventLogs } = jest.requireMock('../../../hooks/use_load_rule_event_logs');
const RuleEventLogListWithProvider = (props: RuleEventLogListProps<'stackManagement'>) => {
return (
<IntlProvider locale="en">
<RuleEventLogList {...props} />
</IntlProvider>
);
};
const loadActionErrorLogMock = loadActionErrorLog as unknown as jest.MockedFunction<
typeof loadActionErrorLog
>;
const loadExecutionLogAggregationsMock = jest.fn();
const onChangeDurationMock = jest.fn();
const ruleMock = mockRule();
const authorizedConsumers = {
@ -64,10 +74,22 @@ const mockErrorLogResponse = {
],
};
// FLAKY: https://github.com/elastic/kibana/issues/139062
describe.skip('rule_event_log_list', () => {
const onChangeDurationMock = jest.fn();
const { fix, cleanup: cleanupJsDomePerformanceFix } = getJsDomPerformanceFix();
beforeAll(() => {
fix();
});
afterAll(() => {
cleanupJsDomePerformanceFix();
});
const mockLoadEventLog = jest.fn();
describe('rule_event_log_list', () => {
beforeEach(() => {
jest.clearAllMocks();
getIsExperimentalFeatureEnabled.mockImplementation(() => true);
useKibanaMock().services.uiSettings.get = jest.fn().mockImplementation((value: string) => {
if (value === 'timepicker:quickRanges') {
return [
@ -80,640 +102,39 @@ describe.skip('rule_event_log_list', () => {
}
});
loadActionErrorLogMock.mockResolvedValue(mockErrorLogResponse);
loadExecutionLogAggregationsMock.mockResolvedValue(mockLogResponse);
useLoadRuleEventLogs.mockReturnValue({
data: mockLogResponse,
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
});
it('renders correctly', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
// Run the initial load fetch call
expect(loadExecutionLogAggregationsMock).toHaveBeenCalledTimes(1);
expect(loadExecutionLogAggregationsMock).toHaveBeenCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
})
);
// Loading
expect(wrapper.find(EuiSuperDatePicker).props().isLoading).toBeTruthy();
// Let the load resolve
await act(async () => {
await nextTick();
wrapper.update();
});
// Verify the initial columns are rendered
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS.forEach((column) => {
expect(wrapper.find(`[data-test-subj="dataGridHeaderCell-${column}"]`).exists()).toBeTruthy();
});
expect(wrapper.find(EuiSuperDatePicker).props().isLoading).toBeFalsy();
expect(wrapper.find(EventLogListStatusFilter).exists()).toBeTruthy();
expect(wrapper.find('[data-gridcell-column-id="timestamp"]').length).toEqual(5);
expect(wrapper.find(EuiDataGrid).props().rowCount).toEqual(mockLogResponse.total);
});
it('can sort by single and/or multiple column(s)', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
let headerCellButton = wrapper.find('[data-test-subj="dataGridHeaderCell-timestamp"] button');
headerCellButton.simulate('click');
let headerAction = wrapper.find('[data-test-subj="dataGridHeaderCellActionGroup-timestamp"]');
expect(headerAction.exists()).toBeTruthy();
// Sort by the timestamp column
headerAction.find('li').at(1).find('button').simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
message: '',
outcomeFilter: [],
page: 0,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
})
);
// Open the popover again
headerCellButton.simulate('click');
headerAction = wrapper.find('[data-test-subj="dataGridHeaderCellActionGroup-timestamp"]');
// Sort by the timestamp column, this time, in the opposite direction
headerAction.find('li').at(2).find('button').simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [{ timestamp: { order: 'desc' } }],
outcomeFilter: [],
page: 0,
perPage: 10,
})
);
// Find another column
headerCellButton = wrapper.find(
'[data-test-subj="dataGridHeaderCell-execution_duration"] button'
);
// Open the popover again
headerCellButton.simulate('click');
headerAction = wrapper.find(
'[data-test-subj="dataGridHeaderCellActionGroup-execution_duration"]'
);
// Sort
headerAction.find('li').at(1).find('button').simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [
{
timestamp: { order: 'desc' },
},
{
execution_duration: { order: 'desc' },
},
],
outcomeFilter: [],
page: 0,
perPage: 10,
})
);
});
it('can filter by execution log outcome status', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
// Filter by success
wrapper.find('[data-test-subj="eventLogStatusFilterButton"]').at(0).simulate('click');
wrapper.find('[data-test-subj="eventLogStatusFilter-success"]').at(0).simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: ['success'],
page: 0,
perPage: 10,
})
);
// Filter by failure as well
wrapper.find('[data-test-subj="eventLogStatusFilterButton"]').at(0).simulate('click');
wrapper.find('[data-test-subj="eventLogStatusFilter-failure"]').at(0).simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: ['success', 'failure'],
page: 0,
perPage: 10,
})
);
});
it('can paginate', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 100,
});
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('.euiPagination').exists()).toBeTruthy();
// Paginate to the next page
wrapper.find('.euiPagination .euiPagination__item a').at(0).simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 1,
perPage: 10,
})
);
// Change the page size
wrapper.find('[data-test-subj="tablePaginationPopoverButton"] button').simulate('click');
wrapper.find('[data-test-subj="tablePagination-50-rows"] button').simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 50,
})
);
});
it('can filter by start and end date', async () => {
const nowMock = jest.spyOn(Date, 'now').mockReturnValue(0);
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
dateStart: '1969-12-30T19:00:00-05:00',
dateEnd: '1969-12-31T19:00:00-05:00',
})
);
wrapper
.find('[data-test-subj="superDatePickerToggleQuickMenuButton"] button')
.simulate('click');
wrapper
.find('[data-test-subj="superDatePickerCommonlyUsed_Last_15 minutes"] button')
.simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadExecutionLogAggregationsMock).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
dateStart: '1969-12-31T18:45:00-05:00',
dateEnd: '1969-12-31T19:00:00-05:00',
})
);
nowMock.mockRestore();
});
it('can save display columns to localStorage', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
JSON.parse(
localStorage.getItem('xpack.triggersActionsUI.ruleEventLogList.initialColumns') ?? 'null'
)
).toEqual(GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS);
wrapper.find('[data-test-subj="dataGridColumnSelectorButton"] button').simulate('click');
wrapper
.find(
'[data-test-subj="dataGridColumnSelectorToggleColumnVisibility-num_active_alerts"] button'
)
.simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
JSON.parse(
localStorage.getItem('xpack.triggersActionsUI.ruleEventLogList.initialColumns') ?? 'null'
)
).toEqual(['timestamp', 'execution_duration', 'status', 'message', 'num_errored_actions']);
});
it('does not show the refine search prompt normally', async () => {
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(RefineSearchPrompt).exists()).toBeFalsy();
});
it('shows the refine search prompt when our queries return too much data', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
data: [],
total: 1100,
});
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
// Initially do not show the prompt
expect(wrapper.find(RefineSearchPrompt).exists()).toBeFalsy();
// Go to the last page
wrapper.find('[data-test-subj="pagination-button-99"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
// Prompt is shown
expect(wrapper.find(RefineSearchPrompt).text()).toEqual(
'These are the first 1000 documents matching your search, refine your search to see others. Back to top.'
);
// Go to the second last page
wrapper.find('[data-test-subj="pagination-button-98"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
// Prompt is not shown
expect(wrapper.find(RefineSearchPrompt).exists()).toBeFalsy();
});
it('shows the correct pagination results when results are 0', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 0,
});
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
'Showing 0 of 0 log entries'
);
});
it('shows the correct pagination result when result is 1', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 1,
});
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
'Showing 1 - 1 of 1 log entry'
);
});
it('shows the correct pagination result when paginated', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 85,
});
const wrapper = mountWithIntl(
<RuleEventLogList
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
'Showing 1 - 10 of 85 log entries'
);
wrapper.find('[data-test-subj="pagination-button-1"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
'Showing 11 - 20 of 85 log entries'
);
wrapper.find('[data-test-subj="pagination-button-8"]').first().simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
'Showing 81 - 85 of 85 log entries'
);
});
it('renders errored action badges in message rows', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
data: [
{
id: uuidv4(),
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
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
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="eventLogDataGridErroredActionBadge"]').first().simulate('click');
expect(wrapper.find('[data-test-subj="ruleActionErrorLogFlyout"]').exists()).toBeTruthy();
afterEach(() => {
jest.clearAllMocks();
cleanup();
});
it('shows rule summary and execution duration chart', async () => {
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 85,
});
const wrapper = mountWithIntl(
<RuleEventLogList
render(
<RuleEventLogListWithProvider
fetchRuleSummary={false}
ruleId={ruleMock.id}
ruleType={ruleType}
ruleSummary={mockRuleSummary({ ruleTypeId: ruleMock.ruleTypeId })}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
const avgExecutionDurationPanel = screen.getByTestId('avgExecutionDurationPanel');
await waitFor(() => {
expect(avgExecutionDurationPanel.textContent).toEqual('Average duration00:00:00.100');
expect(screen.queryByTestId('ruleDurationWarning')).not.toBeInTheDocument();
expect(screen.getByTestId('executionDurationChartPanel')).toBeInTheDocument();
expect(screen.getByTestId('avgExecutionDurationPanel')).toBeInTheDocument();
expect(screen.getByTestId('ruleEventLogListAvgDuration').textContent).toEqual('00:00:00.100');
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:00:00.100'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="executionDurationChartPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="avgExecutionDurationPanel"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="ruleEventLogListAvgDuration"]').first().text()).toEqual(
'00:00:00.100'
);
});
it('renders average execution duration', async () => {
@ -723,30 +144,23 @@ describe.skip('rule_event_log_list', () => {
ruleTypeId: ruleMock.ruleTypeId,
});
const wrapper = mountWithIntl(
<RuleEventLogList
render(
<RuleEventLogListWithProvider
fetchRuleSummary={false}
ruleId={ruleMock.id}
ruleType={ruleTypeCustom}
ruleSummary={ruleSummary}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const avgExecutionDurationPanel = screen.getByTestId('avgExecutionDurationPanel');
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('subdued');
expect(wrapper.find('EuiStat[data-test-subj="avgExecutionDurationStat"]').text()).toEqual(
'Average duration00:01:00.284'
);
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeFalsy();
await waitFor(() => {
expect(avgExecutionDurationPanel.textContent).toEqual('Average duration00:01:00.284');
expect(screen.queryByTestId('ruleDurationWarning')).not.toBeInTheDocument();
});
});
it('renders warning when average execution duration exceeds rule timeout', async () => {
@ -756,37 +170,29 @@ describe.skip('rule_event_log_list', () => {
ruleTypeId: ruleMock.ruleTypeId,
});
loadExecutionLogAggregationsMock.mockResolvedValue({
...mockLogResponse,
total: 85,
useLoadRuleEventLogs.mockReturnValue({
data: {
...mockLogResponse,
total: 85,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
const wrapper = mountWithIntl(
<RuleEventLogList
render(
<RuleEventLogListWithProvider
fetchRuleSummary={false}
ruleId={ruleMock.id}
ruleType={ruleTypeCustom}
ruleSummary={ruleSummary}
numberOfExecutions={60}
onChangeDuration={onChangeDurationMock}
loadExecutionLogAggregations={loadExecutionLogAggregationsMock}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
await waitFor(() => {
expect(screen.getByTestId('ruleEventLogListAvgDuration').textContent).toEqual('16:44:44.345');
expect(screen.getByTestId('ruleDurationWarning')).toBeInTheDocument();
});
const avgExecutionDurationPanel = wrapper.find('[data-test-subj="avgExecutionDurationPanel"]');
expect(avgExecutionDurationPanel.exists()).toBeTruthy();
expect(avgExecutionDurationPanel.first().prop('color')).toEqual('warning');
const avgExecutionDurationStat = wrapper
.find('EuiStat[data-test-subj="avgExecutionDurationStat"]')
.text()
.replaceAll('Info', '');
expect(avgExecutionDurationStat).toEqual('Average duration16:44:44.345');
expect(wrapper.find('[data-test-subj="ruleDurationWarning"]').exists()).toBeTruthy();
});
});

View file

@ -11,7 +11,7 @@ import { RuleExecutionSummaryAndChartWithApi } from './rule_execution_summary_an
import { RuleSummary, RuleType } from '../../../../types';
import { ComponentOpts as RuleApis } from '../../common/components/with_bulk_rule_api_operations';
import { RuleEventLogListTableWithApi } from './rule_event_log_list_table';
import { RuleEventLogListTable } from './rule_event_log_list_table';
import { RefreshToken } from './types';
const RULE_EVENT_LOG_LIST_STORAGE_KEY = 'xpack.triggersActionsUI.ruleEventLogList.initialColumns';
@ -55,7 +55,6 @@ export const RuleEventLogList = <T extends RuleEventLogListOptions>(
refreshToken,
requestRefresh,
fetchRuleSummary = true,
loadExecutionLogAggregations,
} = props;
const {
@ -79,11 +78,10 @@ export const RuleEventLogList = <T extends RuleEventLogListOptions>(
fetchRuleSummary={fetchRuleSummary}
/>
<EuiSpacer />
<RuleEventLogListTableWithApi
<RuleEventLogListTable
localStorageKey={localStorageKey}
ruleId={ruleId}
refreshToken={refreshToken}
overrideLoadExecutionLogAggregations={loadExecutionLogAggregations}
/>
</div>
);

View file

@ -62,6 +62,7 @@ export type RuleEventLogListKPIProps = {
message?: string;
refreshToken?: RefreshToken;
namespaces?: Array<string | undefined>;
filteredRuleTypes?: string[];
} & Pick<RuleApis, 'loadExecutionKPIAggregations' | 'loadGlobalExecutionKPIAggregations'>;
export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
@ -73,6 +74,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
message,
refreshToken,
namespaces,
filteredRuleTypes,
loadExecutionKPIAggregations,
loadGlobalExecutionKPIAggregations,
} = props;
@ -103,6 +105,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
outcomeFilter,
message,
...(namespaces ? { namespaces } : {}),
ruleTypeIds: filteredRuleTypes,
});
setKpi(newKpi);
} catch (e) {

View file

@ -0,0 +1,528 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import { fireEvent, render, screen, waitFor, cleanup } from '@testing-library/react';
import { useKibana } from '../../../../common/lib/kibana';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { RuleEventLogListTable, RuleEventLogListTableProps } from './rule_event_log_list_table';
import {
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
} from '../../../constants';
import { mockRule, mockLogResponse } from './test_helpers';
import { getJsDomPerformanceFix } from '../../test_utils';
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(),
}));
jest.mock('../../../lib/rule_api/load_execution_log_aggregations', () => ({
loadExecutionLogAggregations: jest.fn(),
}));
jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('../../../hooks/use_load_rule_event_logs', () => ({
useLoadRuleEventLogs: jest.fn(),
}));
const { getIsExperimentalFeatureEnabled } = jest.requireMock(
'../../../../common/get_experimental_features'
);
const { useLoadRuleEventLogs } = jest.requireMock('../../../hooks/use_load_rule_event_logs');
const RuleEventLogListWithProvider = (props: RuleEventLogListTableProps<'stackManagement'>) => {
return (
<IntlProvider locale="en">
<RuleEventLogListTable {...props} />
</IntlProvider>
);
};
const loadActionErrorLogMock = loadActionErrorLog as unknown as jest.MockedFunction<
typeof loadActionErrorLog
>;
const ruleMock = mockRule();
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 mockLoadEventLog = jest.fn();
const { fix, cleanup: cleanupJsDomePerformanceFix } = getJsDomPerformanceFix();
beforeAll(() => {
fix();
});
afterAll(() => {
cleanupJsDomePerformanceFix();
});
describe('rule_event_log_list_table', () => {
beforeEach(() => {
getIsExperimentalFeatureEnabled.mockImplementation(() => true);
useKibanaMock().services.uiSettings.get = jest.fn().mockImplementation((value: string) => {
if (value === 'timepicker:quickRanges') {
return [
{
from: 'now-15m',
to: 'now',
display: 'Last 15 minutes',
},
];
}
});
loadActionErrorLogMock.mockResolvedValue(mockErrorLogResponse);
useLoadRuleEventLogs.mockReturnValue({
data: mockLogResponse,
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
});
afterEach(() => {
jest.clearAllMocks();
cleanup();
});
it('renders correctly', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
})
);
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS.forEach((column) => {
expect(screen.getByTestId(`dataGridHeaderCell-${column}`)).toBeInTheDocument();
});
expect(screen.getByTestId('eventLogStatusFilter')).toBeInTheDocument();
expect(screen.getAllByText('rule execution #1').length).toEqual(4);
});
});
it('should display loading spinner if loading event logs', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: [],
isLoading: true,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
await waitFor(() => {
expect(screen.getByTestId('centerJustifiedSpinner')).toBeInTheDocument();
});
});
it('can sort by single column by ascending', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
const timeStampCell = screen.getByTestId('dataGridHeaderCell-timestamp');
fireEvent.click(timeStampCell.querySelector('button')!);
const timeStampCellPopover = screen.getByTestId('dataGridHeaderCellActionGroup-timestamp');
fireEvent.click(timeStampCellPopover.querySelectorAll('li')[0]!.querySelector('button')!);
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
message: '',
outcomeFilter: [],
page: 0,
perPage: 10,
sort: [{ timestamp: { order: 'asc' } }],
})
);
});
});
it('can sort single column by descending', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
const timeStampCell = screen.getByTestId('dataGridHeaderCell-timestamp');
fireEvent.click(timeStampCell.querySelector('button')!);
const timeStampCellPopover = screen.getByTestId('dataGridHeaderCellActionGroup-timestamp');
fireEvent.click(timeStampCellPopover.querySelectorAll('li')[1]!.querySelector('button')!);
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
message: '',
outcomeFilter: [],
page: 0,
perPage: 10,
sort: [{ timestamp: { order: 'desc' } }],
})
);
});
});
it('can filter by single execution status', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
// Filter by success
fireEvent.click(screen.getByTestId('eventLogStatusFilterButton'));
fireEvent.click(screen.getByTestId('eventLogStatusFilter-success'));
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: ['success'],
page: 0,
perPage: 10,
})
);
});
});
it('can filter by multiple execution status', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
// Filter by success
fireEvent.click(screen.getByTestId('eventLogStatusFilterButton'));
fireEvent.click(screen.getByTestId('eventLogStatusFilter-success'));
// Filter by failure as well
fireEvent.click(screen.getByTestId('eventLogStatusFilterButton'));
fireEvent.click(screen.getByTestId('eventLogStatusFilter-failure'));
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: ['success', 'failure'],
page: 0,
perPage: 10,
})
);
});
});
it('can filter by rule types', async () => {
render(
<RuleEventLogListWithProvider ruleId={ruleMock.id} filteredRuleTypes={['test-1', 'test-2']} />
);
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
page: 0,
perPage: 10,
ruleTypeIds: ['test-1', 'test-2'],
})
);
});
});
describe('pagination', () => {
beforeEach(() => {
useLoadRuleEventLogs.mockReturnValue({
data: {
...mockLogResponse,
total: 100,
},
isLoading: true,
loadEventLogs: mockLoadEventLog,
});
});
it('can paginate', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
expect(screen.getByTestId('pagination-button-previous')).toBeInTheDocument();
expect(screen.getByTestId('pagination-button-next')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('pagination-button-1'));
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 1,
perPage: 10,
})
);
});
});
it('can change the page size', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
// Change the page size
fireEvent.click(screen.getByTestId('tablePaginationPopoverButton'));
fireEvent.click(screen.getByTestId('tablePagination-50-rows'));
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 50,
})
);
});
});
it('shows the correct pagination results when results are 0', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: mockLogResponse.data,
total: 0,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
await waitFor(() => {
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 0 of 0 log entries'
);
});
});
it('shows the correct pagination result when result is 1', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: mockLogResponse.data,
total: 1,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
await waitFor(() => {
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 1 - 1 of 1 log entry'
);
});
});
it('shows the correct pagination result when paginated', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: mockLogResponse.data,
total: 85,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 1 - 10 of 85 log entries'
);
fireEvent.click(screen.getByTestId('pagination-button-1'));
await waitFor(() => {
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 11 - 20 of 85 log entries'
);
});
});
it('shows the correct pagination result when paginated to the last page', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: mockLogResponse.data,
total: 85,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 1 - 10 of 85 log entries'
);
fireEvent.click(screen.getByTestId('pagination-button-8'));
await waitFor(() => {
expect(screen.getByTestId('eventLogPaginationStatus').textContent).toEqual(
'Showing 81 - 85 of 85 log entries'
);
});
});
});
it('can filter by start and end date', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
dateStart: 'now-24h',
dateEnd: 'now',
})
);
fireEvent.click(screen.getByTestId('superDatePickerToggleQuickMenuButton'));
fireEvent.click(screen.getByTestId('superDatePickerCommonlyUsed_Last_15 minutes'));
await waitFor(() => {
expect(useLoadRuleEventLogs).toHaveBeenLastCalledWith(
expect.objectContaining({
id: ruleMock.id,
sort: [],
outcomeFilter: [],
page: 0,
perPage: 10,
dateStart: 'now-15m',
dateEnd: 'now',
})
);
});
});
it('can save display columns to localStorage', async () => {
render(
<RuleEventLogListWithProvider
ruleId={ruleMock.id}
localStorageKey={'xpack.triggersActionsUI.RuleEventLogListWithProvider.initialColumns'}
/>
);
expect(
JSON.parse(
localStorage.getItem(
'xpack.triggersActionsUI.RuleEventLogListWithProvider.initialColumns'
) ?? 'null'
)
).toEqual(GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS);
fireEvent.click(screen.getByTestId('dataGridColumnSelectorButton'));
fireEvent.click(
screen.getByTestId('dataGridColumnSelectorToggleColumnVisibility-num_active_alerts')
);
await waitFor(() => {
expect(
JSON.parse(
localStorage.getItem(
'xpack.triggersActionsUI.RuleEventLogListWithProvider.initialColumns'
) ?? 'null'
)
).toEqual(['timestamp', 'execution_duration', 'status', 'message', 'num_errored_actions']);
});
});
describe('refine search prompt', () => {
beforeEach(() => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: mockLogResponse.data,
total: 1100,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
});
it('should not show the refine search prompt normally', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
await waitFor(() => {
expect(screen.queryByTestId('refineSearchPrompt')).not.toBeInTheDocument();
});
});
it('should show the refine search prompt when our queries return too much data', async () => {
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
// Go to the last page
fireEvent.click(screen.getByTestId('pagination-button-99'));
// Prompt is shown
await waitFor(() => {
expect(screen.getByTestId('refineSearchPrompt')).toBeInTheDocument();
});
});
});
it('renders errored action badges in message rows', async () => {
useLoadRuleEventLogs.mockReturnValue({
data: {
data: [
{
id: uuidv4(),
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,
},
isLoading: false,
loadEventLogs: mockLoadEventLog,
});
render(<RuleEventLogListWithProvider ruleId={ruleMock.id} />);
expect(screen.getByTestId('ruleActionErrorBadge').textContent).toEqual('4');
// Click to open flyout
fireEvent.click(screen.getByTestId('eventLogDataGridErroredActionBadge'));
await waitFor(() => {
expect(screen.getByTestId('ruleActionErrorLogFlyout')).toBeInTheDocument();
});
});
});

View file

@ -8,7 +8,6 @@
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import datemath from '@kbn/datemath';
import {
EuiFieldSearch,
EuiFlexItem,
@ -22,7 +21,6 @@ import {
EuiDataGridColumn,
} from '@elastic/eui';
import { IExecutionLog } from '@kbn/alerting-plugin/common';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { useKibana } from '../../../../common/lib/kibana';
import {
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
@ -46,23 +44,11 @@ import { RefineSearchPrompt } from '../../common/components/refine_search_prompt
import { RulesListDocLink } from '../../rules_list/components/rules_list_doc_link';
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
import { RuleEventLogListKPIWithApi as RuleEventLogListKPI } from './rule_event_log_list_kpi';
import {
ComponentOpts as RuleApis,
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces';
import { useLoadRuleEventLogs } from '../../../hooks/use_load_rule_event_logs';
import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link';
import { RefreshToken } from './types';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
const getParsedDate = (date: string) => {
if (date.includes('now')) {
return datemath.parse(date)?.format() || date;
}
return date;
};
const API_FAILED_MESSAGE = i18n.translate(
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError',
{
@ -100,18 +86,16 @@ const MAX_RESULTS = 1000;
export type RuleEventLogListOptions = 'stackManagement' | 'default';
export type RuleEventLogListCommonProps = {
export interface RuleEventLogListCommonProps {
ruleId: string;
localStorageKey?: string;
refreshToken?: RefreshToken;
initialPageSize?: number;
// Duplicating these properties is extremely silly but it's the only way to get Jest to cooperate with the way this component is structured
overrideLoadExecutionLogAggregations?: RuleApis['loadExecutionLogAggregations'];
overrideLoadGlobalExecutionLogAggregations?: RuleApis['loadGlobalExecutionLogAggregations'];
hasRuleNames?: boolean;
hasAllSpaceSwitch?: boolean;
filteredRuleTypes?: string[];
setHeaderActions?: (components?: React.ReactNode[]) => void;
} & Pick<RuleApis, 'loadExecutionLogAggregations' | 'loadGlobalExecutionLogAggregations'>;
}
export type RuleEventLogListTableProps<T extends RuleEventLogListOptions = 'default'> =
T extends 'default'
@ -127,14 +111,11 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
ruleId,
localStorageKey = RULE_EVENT_LOG_LIST_STORAGE_KEY,
refreshToken,
loadGlobalExecutionLogAggregations,
loadExecutionLogAggregations,
overrideLoadGlobalExecutionLogAggregations,
overrideLoadExecutionLogAggregations,
initialPageSize = 10,
hasRuleNames = false,
hasAllSpaceSwitch = false,
setHeaderActions,
filteredRuleTypes,
} = props;
const { uiSettings, notifications } = useKibana().services;
@ -167,7 +148,6 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
});
// Date related states
const [isLoading, setIsLoading] = useState<boolean>(false);
const [dateStart, setDateStart] = useState<string>('now-24h');
const [dateEnd, setDateEnd] = useState<string>('now');
const [dateFormat] = useState(() => uiSettings?.get('dateFormat'));
@ -213,50 +193,41 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadLogsFn = useMemo(() => {
if (ruleId === '*') {
return overrideLoadGlobalExecutionLogAggregations ?? loadGlobalExecutionLogAggregations;
}
return overrideLoadExecutionLogAggregations ?? loadExecutionLogAggregations;
}, [
ruleId,
overrideLoadExecutionLogAggregations,
overrideLoadGlobalExecutionLogAggregations,
loadExecutionLogAggregations,
loadGlobalExecutionLogAggregations,
]);
const loadEventLogs = async () => {
if (!loadLogsFn) {
return;
}
setIsLoading(true);
try {
const result = await loadLogsFn({
id: ruleId,
sort: formattedSort as LoadExecutionLogAggregationsProps['sort'],
outcomeFilter: filter,
message: searchText,
dateStart: getParsedDate(dateStart),
dateEnd: getParsedDate(dateEnd),
page: pagination.pageIndex,
perPage: pagination.pageSize,
namespaces,
});
setLogs(result.data);
setPagination({
...pagination,
totalItemCount: Math.min(result.total, MAX_RESULTS),
});
setActualTotalItemCount(result.total);
} catch (e) {
const onError = useCallback(
(e) => {
notifications.toasts.addDanger({
title: API_FAILED_MESSAGE,
text: e.body?.message ?? e,
});
},
[notifications]
);
const { data, isLoading, loadEventLogs } = useLoadRuleEventLogs({
id: ruleId,
sort: formattedSort as LoadExecutionLogAggregationsProps['sort'],
outcomeFilter: filter,
message: searchText,
dateStart,
dateEnd,
page: pagination.pageIndex,
perPage: pagination.pageSize,
namespaces,
ruleTypeIds: filteredRuleTypes,
onError,
});
useEffect(() => {
if (!data) {
return;
}
setIsLoading(false);
};
setPagination((prevPagination) => ({
...prevPagination,
totalItemCount: Math.min(data.total, MAX_RESULTS),
}));
setLogs(data.data);
setActualTotalItemCount(data.total);
}, [data]);
const getPaginatedRowIndex = useCallback(
(rowIndex: number) => {
@ -694,7 +665,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
}, [refreshToken]);
return (
<EuiFlexGroup gutterSize="none" direction="column">
<EuiFlexGroup gutterSize="none" direction="column" data-test-subj="ruleEventLogListTable">
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
@ -745,6 +716,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
message={searchText}
refreshToken={internalRefreshToken}
namespaces={namespaces}
filteredRuleTypes={filteredRuleTypes}
/>
<EuiSpacer />
</EuiFlexItem>
@ -769,23 +741,5 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
</EuiFlexGroup>
);
};
const RuleEventLogListTableWithSpaces: React.FC<RuleEventLogListTableProps> = (props) => {
const { spaces } = useKibana().services;
// eslint-disable-next-line react-hooks/exhaustive-deps
const SpacesContextWrapper = useCallback(
spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spaces]
);
return (
<SpacesContextWrapper feature="triggersActions">
<RuleEventLogListTable {...props} />
</SpacesContextWrapper>
);
};
export const RuleEventLogListTableWithApi = withBulkRuleOperations(RuleEventLogListTableWithSpaces);
// eslint-disable-next-line import/no-default-export
export { RuleEventLogListTableWithApi as default };
export { RuleEventLogListTable as default };

View file

@ -179,9 +179,8 @@ export const RuleExecutionSummaryAndChart = (props: RuleExecutionSummaryAndChart
</EuiText>
<EuiFlexGroup gutterSize="xs" style={{ alignItems: 'center' }}>
{showDurationWarning && (
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} data-test-subj="ruleDurationWarning">
<EuiIconTip
data-test-subj="ruleDurationWarning"
anchorClassName="ruleDurationWarningIcon"
type="warning"
color="warning"

View file

@ -7,13 +7,36 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import RuleStatusPanelWithApi, { RuleStatusPanel } from './rule_status_panel';
import { RuleStatusPanel, RuleStatusPanelWithApiProps } from './rule_status_panel';
import { mockRule } from './test_helpers';
jest.mock('../../../lib/rule_api/load_execution_log_aggregations', () => ({
loadExecutionLogAggregations: () => ({ total: 400 }),
loadExecutionLogAggregations: jest.fn(),
}));
const { loadExecutionLogAggregations } = jest.requireMock(
'../../../lib/rule_api/load_execution_log_aggregations'
);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
const RuleStatusPanelWithProvider = (props: RuleStatusPanelWithApiProps) => {
return (
<QueryClientProvider client={queryClient}>
<RuleStatusPanel {...props} />
</QueryClientProvider>
);
};
jest.mock('../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
@ -32,15 +55,26 @@ const mockAPIs = {
bulkDisableRules: jest.fn(),
snoozeRule: jest.fn(),
unsnoozeRule: jest.fn(),
loadExecutionLogAggregations: jest.fn(),
};
const requestRefresh = jest.fn();
describe('rule status panel', () => {
beforeEach(() => {
loadExecutionLogAggregations.mockResolvedValue({
total: 400,
data: [],
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('fetches and renders the number of executions in the last 24 hours', async () => {
const rule = mockRule();
const wrapper = mountWithIntl(
<RuleStatusPanelWithApi
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable
healthColor="primary"
@ -48,13 +82,21 @@ describe('rule status panel', () => {
requestRefresh={requestRefresh}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
const ruleExecutionsDescription = wrapper.find(
'[data-test-subj="ruleStatus-numberOfExecutions"]'
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(ruleExecutionsDescription.first().text()).toBe('400 executions in the last 24 hr');
});
@ -62,7 +104,7 @@ describe('rule status panel', () => {
const rule = mockRule({ enabled: true });
const bulkDisableRules = jest.fn();
const wrapper = mountWithIntl(
<RuleStatusPanel
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable
@ -96,7 +138,7 @@ describe('rule status panel', () => {
const rule = mockRule({ enabled: false });
const bulkDisableRules = jest.fn();
const wrapper = mountWithIntl(
<RuleStatusPanel
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable
@ -130,7 +172,7 @@ describe('rule status panel', () => {
const rule = mockRule({ enabled: false });
const bulkEnableRules = jest.fn();
const wrapper = mountWithIntl(
<RuleStatusPanel
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable
@ -164,7 +206,7 @@ describe('rule status panel', () => {
const rule = mockRule({ enabled: true });
const bulkEnableRules = jest.fn();
const wrapper = mountWithIntl(
<RuleStatusPanel
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable
@ -204,7 +246,7 @@ describe('rule status panel', () => {
}) as any;
const wrapper = mountWithIntl(
<RuleStatusPanel
<RuleStatusPanelWithProvider
{...mockAPIs}
rule={rule}
isEditable

View file

@ -6,8 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import datemath from '@kbn/datemath';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@ -27,6 +26,8 @@ import {
withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations';
import { RulesListNotifyBadge } from '../../rules_list/components/notify_badge';
import { useLoadRuleEventLogs } from '../../../hooks/use_load_rule_event_logs';
import { RefreshToken } from './types';
export interface RuleStatusPanelProps {
rule: any;
@ -34,19 +35,16 @@ export interface RuleStatusPanelProps {
requestRefresh: () => void;
healthColor: string;
statusMessage?: string | null;
refreshToken?: RefreshToken;
}
type ComponentOpts = Pick<
export type RuleStatusPanelWithApiProps = Pick<
RuleApis,
| 'bulkDisableRules'
| 'bulkEnableRules'
| 'snoozeRule'
| 'unsnoozeRule'
| 'loadExecutionLogAggregations'
'bulkDisableRules' | 'bulkEnableRules' | 'snoozeRule' | 'unsnoozeRule'
> &
RuleStatusPanelProps;
export const RuleStatusPanel: React.FC<ComponentOpts> = ({
export const RuleStatusPanel: React.FC<RuleStatusPanelWithApiProps> = ({
rule,
bulkEnableRules,
bulkDisableRules,
@ -56,9 +54,10 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
isEditable,
healthColor,
statusMessage,
loadExecutionLogAggregations,
refreshToken,
}) => {
const [lastNumberOfExecutions, setLastNumberOfExecutions] = useState<number | null>(null);
const isInitialized = useRef(false);
const onSnoozeRule = useCallback(
(snoozeSchedule) => snoozeRule(rule, snoozeSchedule),
@ -83,24 +82,33 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
return statusMessage;
}, [rule, statusMessage]);
const getLastNumberOfExecutions = useCallback(async () => {
try {
const result = await loadExecutionLogAggregations({
id: rule.id,
dateStart: datemath.parse('now-24h')!.format(),
dateEnd: datemath.parse('now')!.format(),
page: 0,
perPage: 10,
});
setLastNumberOfExecutions(result.total);
} catch (e) {
// Do nothing if executions fail to fetch
}
}, [loadExecutionLogAggregations, setLastNumberOfExecutions, rule]);
const { data, loadEventLogs } = useLoadRuleEventLogs({
id: rule.id,
dateStart: 'now-24h',
dateEnd: 'now',
page: 0,
perPage: 10,
});
useEffect(() => {
getLastNumberOfExecutions();
}, [getLastNumberOfExecutions]);
if (!data) {
return;
}
setLastNumberOfExecutions(data.total);
}, [data]);
const requestRefreshInternal = useCallback(() => {
loadEventLogs();
requestRefresh();
}, [requestRefresh, loadEventLogs]);
useEffect(() => {
if (isInitialized.current) {
loadEventLogs();
}
isInitialized.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshToken]);
return (
<EuiPanel data-test-subj="ruleStatusPanel" hasBorder paddingSize="none">
@ -127,7 +135,7 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
snoozeRule={async () => {}}
unsnoozeRule={async () => {}}
rule={rule}
onRuleChanged={requestRefresh}
onRuleChanged={requestRefreshInternal}
direction="row"
isEditable={isEditable}
hideSnoozeOption
@ -186,7 +194,7 @@ export const RuleStatusPanel: React.FC<ComponentOpts> = ({
snoozeSettings={rule}
loading={!rule}
disabled={!isEditable}
onRuleChanged={requestRefresh}
onRuleChanged={requestRefreshInternal}
snoozeRule={onSnoozeRule}
unsnoozeRule={onUnsnoozeRule}
showTooltipInline

View file

@ -74,3 +74,43 @@ export const createAppMockRenderer = (): AppMockRenderer => {
AppWrapper,
};
};
export const getJsDomPerformanceFix = () => {
const originalGetComputedStyle = Object.assign({}, window.getComputedStyle);
return {
fix: () => {
// The JSDOM implementation is too slow
// Especially for dropdowns that try to position themselves
// perf issue - https://github.com/jsdom/jsdom/issues/3234
Object.defineProperty(window, 'getComputedStyle', {
value: (el: HTMLElement) => {
/**
* This is based on the jsdom implementation of getComputedStyle
* https://github.com/jsdom/jsdom/blob/9dae17bf0ad09042cfccd82e6a9d06d3a615d9f4/lib/jsdom/browser/Window.js#L779-L820
*
* It is missing global style parsing and will only return styles applied directly to an element.
* Will not return styles that are global or from emotion
*/
const declaration = new CSSStyleDeclaration();
const { style } = el;
Array.prototype.forEach.call(style, (property: string) => {
declaration.setProperty(
property,
style.getPropertyValue(property),
style.getPropertyPriority(property)
);
});
return declaration;
},
configurable: true,
writable: true,
});
},
cleanup: () => {
Object.defineProperty(window, 'getComputedStyle', originalGetComputedStyle);
},
};
};

View file

@ -0,0 +1,21 @@
/*
* 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GlobalRuleEventLogList } from '../application/sections';
import type { GlobalRuleEventLogListProps } from '../application/sections/rule_details/components/global_rule_event_log_list';
const queryClient = new QueryClient();
export const getGlobalRuleEventLogListLazy = (props: GlobalRuleEventLogListProps) => {
return (
<QueryClientProvider client={queryClient}>
<GlobalRuleEventLogList {...props} />;
</QueryClientProvider>
);
};

View file

@ -6,14 +6,21 @@
*/
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RuleEventLogList } from '../application/sections';
import type {
RuleEventLogListProps,
RuleEventLogListOptions,
} from '../application/sections/rule_details/components/rule_event_log_list';
const queryClient = new QueryClient();
export const getRuleEventLogListLazy = <T extends RuleEventLogListOptions = 'default'>(
props: RuleEventLogListProps<T>
) => {
return <RuleEventLogList {...props} />;
return (
<QueryClientProvider client={queryClient}>
<RuleEventLogList {...props} />;
</QueryClientProvider>
);
};

View file

@ -31,6 +31,7 @@ import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
import { getRulesListLazy } from './common/get_rules_list';
import { getAlertsTableStateLazy } from './common/get_alerts_table_state';
import { getAlertsSearchBarLazy } from './common/get_alerts_search_bar';
@ -113,6 +114,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleEventLogList: <T extends RuleEventLogListOptions>(props: RuleEventLogListProps<T>) => {
return getRuleEventLogListLazy<T>(props);
},
getGlobalRuleEventLogList: (props) => {
return getGlobalRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props) => {
return getRulesListNotifyBadgeLazy(props);
},

View file

@ -64,6 +64,7 @@ import type {
RuleTagBadgeOptions,
RuleEventLogListProps,
RuleEventLogListOptions,
GlobalRuleEventLogListProps,
RulesListProps,
RulesListNotifyBadgePropsWithApi,
AlertsTableConfigurationRegistry,
@ -87,6 +88,7 @@ import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary';
import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
@ -137,6 +139,9 @@ export interface TriggersAndActionsUIPublicPluginStart {
getAlertSummaryWidget: (props: AlertSummaryWidgetProps) => ReactElement<AlertSummaryWidgetProps>;
getRuleSnoozeModal: (props: RuleSnoozeModalProps) => ReactElement<RuleSnoozeModalProps>;
getRulesSettingsLink: () => ReactElement;
getGlobalRuleEventLogList: (
props: GlobalRuleEventLogListProps
) => ReactElement<GlobalRuleEventLogListProps>;
}
interface PluginsSetup {
@ -418,6 +423,9 @@ export class Plugin
getRuleEventLogList: <T extends RuleEventLogListOptions>(props: RuleEventLogListProps<T>) => {
return getRuleEventLogListLazy(props);
},
getGlobalRuleEventLogList: (props: GlobalRuleEventLogListProps) => {
return getGlobalRuleEventLogListLazy(props);
},
getRulesListNotifyBadge: (props: RulesListNotifyBadgePropsWithApi) => {
return getRulesListNotifyBadgeLazy(props);
},

View file

@ -78,6 +78,7 @@ import type {
RuleEventLogListProps,
RuleEventLogListOptions,
} from './application/sections/rule_details/components/rule_event_log_list';
import type { GlobalRuleEventLogListProps } from './application/sections/rule_details/components/global_rule_event_log_list';
import type { AlertSummaryTimeRange } from './application/sections/alert_summary_widget/types';
import type { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout';
import type { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout';
@ -127,6 +128,7 @@ export type {
RuleTagBadgeOptions,
RuleEventLogListProps,
RuleEventLogListOptions,
GlobalRuleEventLogListProps,
RulesListProps,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,

View file

@ -0,0 +1,32 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
const esArchiver = getService('esArchiver');
describe('Global rule event log list', function () {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
await PageObjects.common.navigateToApp('triggersActionsUiExample/global_rule_event_log_list');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('should load from the shareable lazy loader', async () => {
await testSubjects.find('ruleEventLogListTable');
const exists = await testSubjects.exists('ruleEventLogListTable');
expect(exists).to.be(true);
});
});
};

View file

@ -14,6 +14,7 @@ export default ({ loadTestFile }: FtrProviderContext) => {
loadTestFile(require.resolve('./rule_status_filter'));
loadTestFile(require.resolve('./rule_tag_badge'));
loadTestFile(require.resolve('./rule_event_log_list'));
loadTestFile(require.resolve('./global_rule_event_log_list'));
loadTestFile(require.resolve('./rules_list'));
loadTestFile(require.resolve('./alerts_table'));
loadTestFile(require.resolve('./rules_settings_link'));

View file

@ -0,0 +1,35 @@
/*
* 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 { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
import { resolve } from 'path';
// @ts-expect-error we have to check types with "allowJs: false" for now, causing this import to fail
import { REPO_ROOT as KIBANA_ROOT } from '@kbn/repo-info';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const baseConfig = await readConfigFile(require.resolve('../../../config.base.ts'));
return {
...baseConfig.getAll(),
testFiles: [require.resolve('.')],
junit: {
reportName:
'Chrome X-Pack UI Functional Tests with ES SSL - Shared Triggers Actions UI Components',
},
kbnTestServer: {
...baseConfig.get('kbnTestServer'),
serverArgs: [
...baseConfig.get('kbnTestServer.serverArgs'),
'--env.name=development',
...findTestPluginPaths([
resolve(KIBANA_ROOT, 'examples'),
resolve(KIBANA_ROOT, 'x-pack/examples'),
]),
],
},
};
}

View file

@ -0,0 +1,219 @@
/*
* 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 expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { Role, User } from '../../../../cases_api_integration/common/lib/authentication/types';
import {
createUsersAndRoles,
deleteUsersAndRoles,
} from '../../../../cases_api_integration/common/lib/authentication';
import { getUrlPrefix } from '../../../../alerting_api_integration/common/lib';
import { getTestAlertData } from '../../../lib/get_test_data';
const SPACE2 = {
id: 'space-2',
name: 'Space 2',
disabledFeatures: [],
};
const ONLY_S2_ROLE: Role = {
name: 'only_s2',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
base: ['all'],
feature: {},
spaces: [SPACE2.id],
},
],
},
};
const ONLY_S2_USER: User = {
username: 'only_s2_user',
password: 'changeme',
roles: [ONLY_S2_ROLE.name],
};
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'header']);
const spaces = getService('spaces');
const pageObjects = getPageObjects(['security']);
const supertest = getService('supertest');
const retry = getService('retry');
const find = getService('find');
const ensureRuleHasRan = async (id: string, space: string) => {
const { body } = await supertest
.get(`${getUrlPrefix(space)}/api/alerting/rule/${id}`)
.expect(200);
expect(body.last_run.outcome).eql('succeeded');
};
async function deleteAlerts(rules: Array<{ id: string; space: string }>) {
await asyncForEach(rules, async ({ id, space }) => {
await supertest
.delete(`${getUrlPrefix(space)}/api/alerting/rule/${id}`)
.set('kbn-xsrf', 'foo')
.expect(204);
});
}
describe('Shared global rule event log list', function () {
const rulesToDelete: Array<{
space: string;
id: string;
}> = [];
before(async () => {
await spaces.delete(SPACE2.id);
await createUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
await spaces.create(SPACE2);
await PageObjects.common.navigateToApp('triggersActionsUiExample/global_rule_event_log_list');
});
after(async () => {
await deleteAlerts(rulesToDelete);
await deleteUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
await spaces.delete(SPACE2.id);
await pageObjects.security.forceLogout();
});
it('should load from the shareable lazy loader', async () => {
await PageObjects.common.navigateToApp('triggersActionsUiExample/global_rule_event_log_list');
const exists = await testSubjects.exists('ruleEventLogListTable');
const spacesSwitchExists = await testSubjects.exists('showAllSpacesSwitch');
expect(exists).to.be(true);
expect(spacesSwitchExists).to.be(true);
});
it('should filter out rule types based on filteredRuleTypes prop and respect spaces', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('default')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
params: {
threshold: 25,
windowSize: 5,
windowUnit: 'm',
environment: 'ENVIRONMENT_ALL',
},
consumer: 'alerts',
schedule: {
interval: '1m',
},
tags: [],
name: 'Error count threshold',
rule_type_id: 'apm.error_rate',
actions: [],
})
.expect(200);
rulesToDelete.push({ id: createdRule1.id, space: 'default' });
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space-2')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send({
params: {
threshold: 30,
windowSize: 5,
windowUnit: 'm',
environment: 'ENVIRONMENT_ALL',
},
consumer: 'alerts',
schedule: {
interval: '1m',
},
tags: [],
name: 'Failed transaction',
rule_type_id: 'apm.transaction_error_rate',
actions: [],
})
.expect(200);
rulesToDelete.push({ id: createdRule2.id, space: 'space-2' });
const { body: createdRule3 } = await supertest
.post(`${getUrlPrefix('default')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestAlertData({
name: 'test-rule',
})
)
.expect(200);
rulesToDelete.push({ id: createdRule3.id, space: 'default' });
await retry.try(async () => {
await ensureRuleHasRan(createdRule1.id, 'default');
});
await retry.try(async () => {
await ensureRuleHasRan(createdRule2.id, 'space-2');
});
await retry.try(async () => {
await ensureRuleHasRan(createdRule3.id, 'default');
});
await PageObjects.common.navigateToApp('triggersActionsUiExample/global_rule_event_log_list');
let ruleNameCells = await find.allByCssSelector(
'[data-gridcell-column-id="rule_name"][data-test-subj="dataGridRowCell"]'
);
// Should not see the 'test-rule' since that is filtered out by filteredRuleTypes
// Should only see default space rule: Error count threshold
let textCellsMap: Record<string, boolean> = {};
await asyncForEach(ruleNameCells, async (cell) => {
const text = await cell.getVisibleText();
textCellsMap[text] = true;
});
expect(textCellsMap['Error count threshold']).eql(true);
expect(textCellsMap['Failed transaction']).eql(undefined);
expect(textCellsMap['test-rule']).eql(undefined);
const spacesSwitch = await testSubjects.find('showAllSpacesSwitch');
const switchControl = await spacesSwitch.findByCssSelector('button');
await switchControl.click();
await PageObjects.header.waitUntilLoadingHasFinished();
ruleNameCells = await find.allByCssSelector(
'[data-gridcell-column-id="rule_name"][data-test-subj="dataGridRowCell"]'
);
// Should not see the 'test-rule' since that is filtered out by filteredRuleTypes
// Should see both space rules: Error count threshold + Failed transaction
textCellsMap = {};
await asyncForEach(ruleNameCells, async (cell) => {
const text = await cell.getVisibleText();
textCellsMap[text] = true;
});
expect(textCellsMap['Error count threshold']).eql(true);
expect(textCellsMap['Failed transaction']).eql(true);
expect(textCellsMap['test-rule']).eql(undefined);
});
});
};

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Shared Components', function () {
loadTestFile(require.resolve('./global_rule_event_log_list'));
});
};