mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
03efa64480
commit
ba96a720f1
40 changed files with 1374 additions and 893 deletions
|
@ -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
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
"developerExamples",
|
||||
"kibanaReact",
|
||||
"cases"
|
||||
]
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"spaces"
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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={() => (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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'))
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
|
|
|
@ -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'),
|
||||
]),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue