[ResponseOps][Alerts] Migrate alerts fetching to TanStack Query (#186978)

## Summary

Implements a new `useSearchAlertsQuery` hook based on TanStack Query to
replace the `useFetchAlerts` hook, following [this organizational
logic](https://github.com/elastic/kibana/issues/186448#issuecomment-2228853337).

This PR focuses mainly on the fetching logic itself, leaving the
surrounding API surface mostly unchanged since it will be likely
addressed in subsequent PRs.

## To verify

1. Create rules that fire alerts in different solutions
2. Check that the alerts table usages work correctly ({O11y, Security,
Stack} alerts and rule details pages, ...)
1. Check that the alerts displayed in the table are coherent with the
solution, KQL query, time filter, pagination
    2. Check that pagination changes are reflected in the table
3. Check that changing the query when in pages > 0 resets the pagination
to the first page

Closes point 1 of https://github.com/elastic/kibana/issues/186448
Should fix https://github.com/elastic/kibana/issues/171738

### 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:
Umberto Pepato 2024-07-25 15:36:56 +02:00 committed by GitHub
parent 76237d8cf2
commit bd3032b5fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1382 additions and 1232 deletions

View file

@ -6,13 +6,14 @@
* Side Public License, v 1.
*/
export * from './builtin_action_groups_types';
export * from './rule_type_types';
export * from './action_group_types';
export * from './alert_type';
export * from './rule_notify_when_type';
export * from './r_rule_types';
export * from './rule_types';
export * from './alerting_framework_health_types';
export * from './action_variable';
export * from './alert_type';
export * from './alerting_framework_health_types';
export * from './builtin_action_groups_types';
export * from './circuit_breaker_message_header';
export * from './r_rule_types';
export * from './rule_notify_when_type';
export * from './search_strategy_types';
export * from './rule_type_types';
export * from './rule_types';

View file

@ -1,17 +1,20 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TechnicalRuleDataFieldName, ValidFeatureId } from '@kbn/rule-data-utils';
import { IEsSearchRequest, IEsSearchResponse } from '@kbn/search-types';
import type { IEsSearchRequest, IEsSearchResponse } from '@kbn/search-types';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import type {
MappingRuntimeFields,
QueryDslFieldAndFormat,
QueryDslQueryContainer,
SortCombinations,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { Alert } from './alert_type';
export type RuleRegistrySearchRequest = IEsSearchRequest & {
featureIds: ValidFeatureId[];
@ -27,20 +30,10 @@ export interface RuleRegistrySearchRequestPagination {
pageSize: number;
}
export interface BasicFields {
_id: string;
_index: string;
}
export type EcsFieldsResponse = BasicFields & {
[Property in TechnicalRuleDataFieldName]?: string[];
} & {
[x: string]: unknown[];
};
export interface RuleRegistryInspect {
dsl: string[];
}
export interface RuleRegistrySearchResponse extends IEsSearchResponse<EcsFieldsResponse> {
export interface RuleRegistrySearchResponse extends IEsSearchResponse<Alert> {
inspect?: RuleRegistryInspect;
}

View file

@ -21,6 +21,7 @@
"@kbn/rule-data-utils",
"@kbn/rrule",
"@kbn/core",
"@kbn/es-query"
"@kbn/es-query",
"@kbn/search-types"
]
}

View file

@ -0,0 +1,333 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { of, Subject, throwError } from 'rxjs';
import type { IKibanaSearchResponse } from '@kbn/search-types';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { SearchAlertsResult, searchAlerts, SearchAlertsParams } from './search_alerts';
const searchResponse = {
id: '0',
rawResponse: {
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: 2,
max_score: 1,
hits: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['5qcxz8o4j7'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['hdgsmwj08h'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
],
},
},
isPartial: false,
isRunning: false,
total: 2,
loaded: 2,
isRestored: false,
};
const searchResponse$ = of<IKibanaSearchResponse>(searchResponse);
const expectedResponse: SearchAlertsResult = {
total: -1,
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
};
const parsedAlerts = {
alerts: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['5qcxz8o4j7'],
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['hdgsmwj08h'],
},
],
total: 2,
ecsAlertsData: [
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:48:07.518Z'],
user: { name: ['5qcxz8o4j7'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_index: '.internal.alerts-security.alerts-default-000001',
},
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:17:50.769Z'],
user: { name: ['hdgsmwj08h'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_index: '.internal.alerts-security.alerts-default-000001',
},
],
oldAlertsData: [
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:48:07.518Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['5qcxz8o4j7'] },
{
field: 'kibana.alert.reason',
value: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:17:50.769Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['hdgsmwj08h'] },
{
field: 'kibana.alert.reason',
value: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
],
};
describe('searchAlerts', () => {
const mockDataPlugin = {
search: {
search: jest.fn().mockReturnValue(searchResponse$),
showError: jest.fn(),
},
};
const params: SearchAlertsParams = {
data: mockDataPlugin as unknown as DataPublicPluginStart,
featureIds: ['siem'],
fields: [
{ field: 'kibana.rule.type.id', include_unmapped: true },
{ field: '*', include_unmapped: true },
],
query: {
ids: { values: ['alert-id-1'] },
},
pageIndex: 0,
pageSize: 10,
sort: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns the response correctly', async () => {
const result = await searchAlerts(params);
expect(result).toEqual(
expect.objectContaining({
...expectedResponse,
...parsedAlerts,
})
);
});
it('call search with correct arguments', async () => {
await searchAlerts(params);
expect(mockDataPlugin.search.search).toHaveBeenCalledTimes(1);
expect(mockDataPlugin.search.search).toHaveBeenCalledWith(
{
featureIds: params.featureIds,
fields: [...params.fields!],
pagination: {
pageIndex: params.pageIndex,
pageSize: params.pageSize,
},
query: {
ids: {
values: ['alert-id-1'],
},
},
sort: params.sort,
},
{ strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
});
it('handles search error', async () => {
const obs$ = throwError('simulated search error');
mockDataPlugin.search.search.mockReturnValue(obs$);
const result = await searchAlerts(params);
expect(result).toEqual(
expect.objectContaining({
...expectedResponse,
alerts: [],
total: 0,
})
);
expect(mockDataPlugin.search.showError).toHaveBeenCalled();
});
it("doesn't return while the response is still running", async () => {
const response$ = new Subject<IKibanaSearchResponse>();
mockDataPlugin.search.search.mockReturnValue(response$);
let result: SearchAlertsResult | undefined;
const done = searchAlerts(params).then((r) => {
result = r;
});
response$.next({
...searchResponse,
isRunning: true,
});
expect(result).toBeUndefined();
response$.next({ ...searchResponse, isRunning: false });
response$.complete();
await done;
expect(result).toEqual(
expect.objectContaining({
...expectedResponse,
...parsedAlerts,
})
);
});
it('returns the correct total alerts if the total alerts in the response is an object', async () => {
const obs$ = of<IKibanaSearchResponse>({
...searchResponse,
rawResponse: {
...searchResponse.rawResponse,
hits: { ...searchResponse.rawResponse.hits, total: { value: 2 } },
},
});
mockDataPlugin.search.search.mockReturnValue(obs$);
const result = await searchAlerts(params);
expect(result.total).toEqual(2);
});
it('does not return an alert without fields', async () => {
const obs$ = of<IKibanaSearchResponse>({
...searchResponse,
rawResponse: {
...searchResponse.rawResponse,
hits: {
...searchResponse.rawResponse.hits,
hits: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_score: 1,
},
],
},
},
});
mockDataPlugin.search.search.mockReturnValue(obs$);
const result = await searchAlerts(params);
expect(result.alerts).toEqual([]);
});
});

View file

@ -0,0 +1,195 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { catchError, filter, lastValueFrom, map, of } from 'rxjs';
import type {
Alert,
RuleRegistrySearchRequest,
RuleRegistrySearchResponse,
} from '@kbn/alerting-types';
import { set } from '@kbn/safer-lodash-set';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import type {
MappingRuntimeFields,
QueryDslFieldAndFormat,
QueryDslQueryContainer,
SortCombinations,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { EsQuerySnapshot, LegacyField } from '../../types';
export interface SearchAlertsParams {
// Dependencies
/**
* Kibana data plugin, used to perform the query
*/
data: DataPublicPluginStart;
/**
* Abort signal used to cancel the request
*/
signal?: AbortSignal;
// Parameters
/**
* Array of feature ids used for authorization and area-based filtering
*/
featureIds: ValidFeatureId[];
/**
* ES query to perform on the affected alert indices
*/
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
/**
* The alert document fields to include in the response
*/
fields?: QueryDslFieldAndFormat[];
/**
* Sort combinations to apply to the query
*/
sort: SortCombinations[];
/**
* Runtime mappings to apply to the query
*/
runtimeMappings?: MappingRuntimeFields;
/**
* The page index to fetch
*/
pageIndex: number;
/**
* The page size to fetch
*/
pageSize: number;
}
export interface SearchAlertsResult {
alerts: Alert[];
oldAlertsData: LegacyField[][];
ecsAlertsData: unknown[];
total: number;
querySnapshot?: EsQuerySnapshot;
}
/**
* Performs an ES search query to fetch alerts applying alerting RBAC and area-based filtering
*/
export const searchAlerts = ({
data,
signal,
featureIds,
fields,
query,
sort,
runtimeMappings,
pageIndex,
pageSize,
}: SearchAlertsParams): Promise<SearchAlertsResult> =>
lastValueFrom(
data.search
.search<RuleRegistrySearchRequest, RuleRegistrySearchResponse>(
{
featureIds,
fields,
query,
pagination: { pageIndex, pageSize },
sort,
runtimeMappings,
},
{
strategy: 'privateRuleRegistryAlertsSearchStrategy',
abortSignal: signal,
}
)
.pipe(
filter((response) => {
return !response.isRunning;
}),
map((response) => {
const { rawResponse } = response;
const total = parseTotalHits(rawResponse);
const alerts = parseAlerts(rawResponse);
const { oldAlertsData, ecsAlertsData } = transformToLegacyFormat(alerts);
return {
alerts,
oldAlertsData,
ecsAlertsData,
total,
querySnapshot: {
request: response?.inspect?.dsl ?? [],
response: [JSON.stringify(rawResponse)] ?? [],
},
};
}),
catchError((error) => {
data.search.showError(error);
return of({
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
total: 0,
});
})
)
);
/**
* Normalizes the total hits from the raw response
*/
const parseTotalHits = (rawResponse: RuleRegistrySearchResponse['rawResponse']) => {
let total = 0;
if (rawResponse.hits.total) {
if (typeof rawResponse.hits.total === 'number') {
total = rawResponse.hits.total;
} else if (typeof rawResponse.hits.total === 'object') {
total = rawResponse.hits.total?.value ?? 0;
}
}
return total;
};
/**
* Extracts the alerts from the raw response
*/
const parseAlerts = (rawResponse: RuleRegistrySearchResponse['rawResponse']) =>
rawResponse.hits.hits.reduce<Alert[]>((acc, hit) => {
if (hit.fields) {
acc.push({
...hit.fields,
_id: hit._id,
_index: hit._index,
} as Alert);
}
return acc;
}, []);
/**
* Transforms the alerts to legacy formats (will be removed)
* @deprecated Will be removed in v8.16.0
*/
const transformToLegacyFormat = (alerts: Alert[]) =>
alerts.reduce<{
oldAlertsData: LegacyField[][];
ecsAlertsData: unknown[];
}>(
(acc, alert) => {
const itemOldData = Object.entries(alert).reduce<Array<{ field: string; value: string[] }>>(
(oldData, [key, value]) => {
oldData.push({ field: key, value: value as string[] });
return oldData;
},
[]
);
const ecsData = Object.entries(alert).reduce((ecs, [key, value]) => {
set(ecs, key, value ?? []);
return ecs;
}, {});
acc.oldAlertsData.push(itemOldData);
acc.ecsAlertsData.push(ecsData);
return acc;
},
{ oldAlertsData: [], ecsAlertsData: [] }
);

View file

@ -14,5 +14,6 @@ export const INTERNAL_BASE_ALERTING_API_PATH = '/internal/alerting';
export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts';
export const EMPTY_AAD_FIELDS: DataViewField[] = [];
export const BASE_TRIGGERS_ACTIONS_UI_API_PATH = '/internal/triggers_actions_ui';
export const DEFAULT_ALERTS_PAGE_SIZE = 10;
export const BASE_ACTION_API_PATH = '/api/actions';
export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions';

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createContext } from 'react';
import { QueryClient } from '@tanstack/react-query';
export const AlertsQueryContext = createContext<QueryClient | undefined>(undefined);

View file

@ -17,4 +17,5 @@ export * from './use_load_alerting_framework_health';
export * from './use_create_rule';
export * from './use_update_rule';
export * from './use_resolve_rule';
export * from './use_search_alerts_query';
export * from './use_get_alerts_group_aggregations_query';

View file

@ -0,0 +1,284 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FunctionComponent } from 'react';
import { of } from 'rxjs';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { IKibanaSearchResponse } from '@kbn/search-types';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react-hooks';
import type { UseSearchAlertsQueryParams } from '../../..';
import { AlertsQueryContext } from '../contexts/alerts_query_context';
import { useSearchAlertsQuery } from './use_search_alerts_query';
const searchResponse = {
id: '0',
rawResponse: {
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: 2,
max_score: 1,
hits: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['5qcxz8o4j7'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['hdgsmwj08h'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
],
},
},
isPartial: false,
isRunning: false,
total: 2,
loaded: 2,
isRestored: false,
};
const searchResponse$ = of<IKibanaSearchResponse>(searchResponse);
const expectedResponse: ReturnType<typeof useSearchAlertsQuery>['data'] = {
total: -1,
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 0,
staleTime: 0,
retry: false,
},
},
});
describe('useSearchAlertsQuery', () => {
const mockDataPlugin = {
search: {
search: jest.fn().mockReturnValue(searchResponse$),
showError: jest.fn(),
},
};
const params: UseSearchAlertsQueryParams = {
data: mockDataPlugin as unknown as DataPublicPluginStart,
featureIds: ['siem'],
fields: [
{ field: 'kibana.rule.type.id', include_unmapped: true },
{ field: '*', include_unmapped: true },
],
query: {
ids: { values: ['alert-id-1'] },
},
pageIndex: 0,
pageSize: 10,
sort: [],
};
const wrapper: FunctionComponent = ({ children }) => (
<QueryClientProvider client={queryClient} context={AlertsQueryContext}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
queryClient.removeQueries();
});
it('returns the response correctly', async () => {
const { result, waitForValueToChange } = renderHook(() => useSearchAlertsQuery(params), {
wrapper,
});
await waitForValueToChange(() => result.current.data);
expect(result.current.data).toEqual(
expect.objectContaining({
...expectedResponse,
alerts: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['5qcxz8o4j7'],
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['hdgsmwj08h'],
},
],
total: 2,
ecsAlertsData: [
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:48:07.518Z'],
user: { name: ['5qcxz8o4j7'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_index: '.internal.alerts-security.alerts-default-000001',
},
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:17:50.769Z'],
user: { name: ['hdgsmwj08h'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_index: '.internal.alerts-security.alerts-default-000001',
},
],
oldAlertsData: [
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:48:07.518Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['5qcxz8o4j7'] },
{
field: 'kibana.alert.reason',
value: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:17:50.769Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['hdgsmwj08h'] },
{
field: 'kibana.alert.reason',
value: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
],
})
);
});
it('returns empty placeholder data', () => {
const { result } = renderHook(() => useSearchAlertsQuery(params), {
wrapper,
});
expect(result.current.data).toEqual({
total: -1,
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
});
});
it('does not fetch with no feature ids', () => {
const { result } = renderHook(() => useSearchAlertsQuery({ ...params, featureIds: [] }), {
wrapper,
});
expect(mockDataPlugin.search.search).not.toHaveBeenCalled();
expect(result.current.data).toMatchObject(
expect.objectContaining({
...expectedResponse,
alerts: [],
total: -1,
})
);
});
});

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useQuery } from '@tanstack/react-query';
import { SetOptional } from 'type-fest';
import { searchAlerts, type SearchAlertsParams } from '../apis/search_alerts/search_alerts';
import { DEFAULT_ALERTS_PAGE_SIZE } from '../constants';
import { AlertsQueryContext } from '../contexts/alerts_query_context';
export type UseSearchAlertsQueryParams = SetOptional<
Omit<SearchAlertsParams, 'signal'>,
'query' | 'sort' | 'pageIndex' | 'pageSize'
>;
export const queryKeyPrefix = ['alerts', searchAlerts.name];
/**
* Query alerts
*
* When testing components that depend on this hook, prefer mocking the {@link searchAlerts} function instead of the hook itself.
* @external https://tanstack.com/query/v4/docs/framework/react/guides/testing
*/
export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryParams) => {
const {
featureIds,
fields,
query = {
bool: {},
},
sort = [
{
'@timestamp': 'desc',
},
],
runtimeMappings,
pageIndex = 0,
pageSize = DEFAULT_ALERTS_PAGE_SIZE,
} = params;
return useQuery({
queryKey: queryKeyPrefix.concat(JSON.stringify(params)),
queryFn: ({ signal }) =>
searchAlerts({
data,
signal,
featureIds,
fields,
query,
sort,
runtimeMappings,
pageIndex,
pageSize,
}),
refetchOnWindowFocus: false,
context: AlertsQueryContext,
enabled: featureIds.length > 0,
// To avoid flash of empty state with pagination, see https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata
keepPreviousData: true,
placeholderData: {
total: -1,
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
},
});
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface LegacyField {
field: string;
value: string[];
}
export interface EsQuerySnapshot {
request: string[];
response: string[];
}

View file

@ -6,5 +6,6 @@
* Side Public License, v 1.
*/
export * from './rule_types';
export * from './action_types';
export * from './alerts_types';
export * from './rule_types';

View file

@ -39,6 +39,7 @@
"@kbn/data-plugin",
"@kbn/search-types",
"@kbn/utility-types",
"@kbn/safer-lodash-set",
"@kbn/core-application-browser",
"@kbn/react-kibana-mount",
"@kbn/core-i18n-browser",

View file

@ -131,7 +131,7 @@ export const AlertsOverview = ({
featureIds={alertFeatureIds}
showAlertStatusWithFlapping
query={alertsEsQueryByStatus}
pageSize={5}
initialPageSize={5}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -81,7 +81,7 @@ export const AlertsTabContent = () => {
configurationId={AlertConsumers.OBSERVABILITY}
featureIds={infraAlertFeatureIds}
id={ALERTS_TABLE_ID}
pageSize={ALERTS_PER_PAGE}
initialPageSize={ALERTS_PER_PAGE}
query={alertsEsQueryByStatus}
showAlertStatusWithFlapping
/>

View file

@ -5,15 +5,18 @@
* 2.0.
*/
import { AggregationsAggregate, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
import type { AggregationsAggregate, SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common';
import { InventoryMetricConditions } from '../../../../../common/alerting/metrics';
import { InfraTimerangeInput, SnapshotCustomMetricInput } from '../../../../../common/http_api';
import { LogQueryFields } from '../../../metrics/types';
import { InfraSource } from '../../../sources';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import type { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common';
import type { InventoryMetricConditions } from '../../../../../common/alerting/metrics';
import type {
InfraTimerangeInput,
SnapshotCustomMetricInput,
} from '../../../../../common/http_api';
import type { LogQueryFields } from '../../../metrics/types';
import type { InfraSource } from '../../../sources';
import { createRequest } from './create_request';
import {
AdditionalContext,

View file

@ -8,7 +8,7 @@
import { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import { COMPARATORS } from '@kbn/alerting-comparators';
import { convertToBuiltInComparators } from '@kbn/observability-plugin/common';
import { Aggregators, MetricExpressionParams } from '../../../../../common/alerting/metrics';

View file

@ -9,7 +9,7 @@ import React, { useMemo } from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutProps } from '@elastic/eui';
import { ALERT_UUID } from '@kbn/rule-data-utils';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import { AlertsFlyoutHeader } from './alerts_flyout_header';
import { AlertsFlyoutBody } from './alerts_flyout_body';
import { AlertsFlyoutFooter } from './alerts_flyout_footer';

View file

@ -9,7 +9,7 @@ import { isEmpty } from 'lodash';
import { HttpSetup } from '@kbn/core/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import { usePluginContext } from './use_plugin_context';
import { useDataFetcher } from './use_data_fetcher';

View file

@ -248,7 +248,7 @@ function InternalAlertsPage() {
featureIds={observabilityAlertFeatureIds}
query={esQuery}
showAlertStatusWithFlapping
pageSize={ALERTS_PER_PAGE}
initialPageSize={ALERTS_PER_PAGE}
cellContext={{ observabilityRuleTypeRegistry }}
/>
)}

View file

@ -21,10 +21,10 @@ import { allCasesPermissions, noCasesPermissions } from '@kbn/observability-shar
import { noop } from 'lodash';
import { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
import { waitFor } from '@testing-library/react';
import { AlertsTableQueryContext } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_table/contexts/alerts_table_context';
import { Router } from '@kbn/shared-ux-router';
import { createMemoryHistory } from 'history';
import { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
const refresh = jest.fn();
const caseHooksReturnedValue = {
@ -128,7 +128,7 @@ describe('ObservabilityActions component', () => {
const wrapper = mountWithIntl(
<Router history={createMemoryHistory()}>
<QueryClientProvider client={queryClient} context={AlertsTableQueryContext}>
<QueryClientProvider client={queryClient} context={AlertsQueryContext}>
<AlertActions {...props} />
</QueryClientProvider>
</Router>

View file

@ -241,7 +241,7 @@ export function OverviewPage() {
featureIds={observabilityAlertFeatureIds}
hideLazyLoader
id={ALERTS_TABLE_ID}
pageSize={ALERTS_PER_PAGE}
initialPageSize={ALERTS_PER_PAGE}
query={esQuery}
showAlertStatusWithFlapping
cellContext={{ observabilityRuleTypeRegistry }}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
export const inventoryThresholdAlert = [
{

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
import type { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/lib/api/types';
import type { ElasticsearchClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import {
import type { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common';
import type {
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';

View file

@ -108,7 +108,7 @@ export function SloAlertsTable({
featureIds={[AlertConsumers.SLO, AlertConsumers.OBSERVABILITY]}
hideLazyLoader
id={ALERTS_TABLE_ID}
pageSize={ALERTS_PER_PAGE}
initialPageSize={ALERTS_PER_PAGE}
showAlertStatusWithFlapping
onLoaded={() => {
if (onLoaded) {

View file

@ -42,7 +42,7 @@ export function SloDetailsAlerts({ slo }: Props) {
},
}}
showAlertStatusWithFlapping
pageSize={100}
initialPageSize={100}
cellContext={{ observabilityRuleTypeRegistry }}
/>
</EuiFlexItem>

View file

@ -9,6 +9,7 @@ export type {
RuleRegistrySearchRequest,
RuleRegistrySearchResponse,
RuleRegistrySearchRequestPagination,
} from './search_strategy';
Alert as EcsFieldsResponse,
} from '@kbn/alerting-types';
export { BASE_RAC_ALERTS_API_PATH } from './constants';
export type { BrowserFields, BrowserField } from './types';

View file

@ -15,7 +15,7 @@ import { SearchStrategyDependencies } from '@kbn/data-plugin/server';
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { spacesMock } from '@kbn/spaces-plugin/server/mocks';
import { RuleRegistrySearchRequest } from '../../common/search_strategy';
import type { RuleRegistrySearchRequest } from '../../common';
import * as getAuthzFilterImport from '../lib/get_authz_filter';
import { getIsKibanaRequest } from '../lib/get_is_kibana_request';

View file

@ -19,10 +19,7 @@ import {
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import { buildAlertFieldsRequest } from '@kbn/alerts-as-data-utils';
import {
RuleRegistrySearchRequest,
RuleRegistrySearchResponse,
} from '../../common/search_strategy';
import type { RuleRegistrySearchRequest, RuleRegistrySearchResponse } from '../../common';
import { MAX_ALERT_SEARCH_SIZE } from '../../common/constants';
import { AlertAuditAction, alertAuditEvent } from '..';
import { getSpacesFilter, getAuthzFilter } from '../lib';

View file

@ -34,8 +34,8 @@
"@kbn/alerts-as-data-utils",
"@kbn/core-http-router-server-mocks",
"@kbn/core-http-server",
"@kbn/search-types",
"@kbn/alerting-state-types",
"@kbn/alerting-types",
"@kbn/field-formats-plugin"
],
"exclude": [

View file

@ -201,7 +201,7 @@ const PageContent = () => {
featureIds={featureIds}
query={esQuery}
showAlertStatusWithFlapping
pageSize={20}
initialPageSize={20}
/>
</Suspense>
</EuiFlexGroup>

View file

@ -37,7 +37,8 @@ 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';
import { AlertsTableContext, AlertsTableQueryContext } from './contexts/alerts_table_context';
import { AlertsTableContext } from './contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
const mockCaseService = createCasesServiceMock();
@ -312,14 +313,15 @@ describe('AlertsTable', () => {
onChangeVisibleColumns: () => {},
browserFields,
query: {},
pagination: { pageIndex: 0, pageSize: 1 },
pageIndex: 0,
pageSize: 1,
sort: [],
isLoading: false,
alerts,
oldAlertsData,
ecsAlertsData,
getInspectQuery: () => ({ request: [], response: [] }),
refetch: () => {},
querySnapshot: { request: [], response: [] },
refetchAlerts: () => {},
alertsCount: alerts.length,
onSortChange: jest.fn(),
onPageChange: jest.fn(),
@ -340,7 +342,7 @@ describe('AlertsTable', () => {
const AlertsTableWithProviders: React.FunctionComponent<
AlertsTableProps & { initialBulkActionsState?: BulkActionsState }
> = (props) => {
const renderer = useMemo(() => createAppMockRenderer(AlertsTableQueryContext), []);
const renderer = useMemo(() => createAppMockRenderer(AlertsQueryContext), []);
const AppWrapper = renderer.AppWrapper;
const initialBulkActionsState = useReducer(
@ -398,7 +400,7 @@ describe('AlertsTable', () => {
it('should support pagination', async () => {
const renderResult = render(
<AlertsTableWithProviders {...tableProps} pagination={{ pageIndex: 0, pageSize: 1 }} />
<AlertsTableWithProviders {...tableProps} pageIndex={0} pageSize={1} />
);
userEvent.click(renderResult.getByTestId('pagination-button-1'), undefined, {
skipPointerEventsCheck: true,
@ -421,7 +423,8 @@ describe('AlertsTable', () => {
const props = {
...tableProps,
showAlertStatusWithFlapping: true,
pagination: { pageIndex: 0, pageSize: 10 },
pageIndex: 0,
pageSize: 10,
alertsTableConfiguration: {
...alertsTableConfiguration,
getRenderCellValue: undefined,
@ -447,7 +450,8 @@ describe('AlertsTable', () => {
rowCellRender: () => <h2 data-test-subj="testCell">Test cell</h2>,
},
],
pagination: { pageIndex: 0, pageSize: 1 },
pageIndex: 0,
pageSize: 1,
};
const wrapper = render(<AlertsTableWithProviders {...customTableProps} />);
expect(wrapper.queryByTestId('testHeader')).not.toBe(null);
@ -565,7 +569,8 @@ describe('AlertsTable', () => {
mockedFn = jest.fn();
customTableProps = {
...tableProps,
pagination: { pageIndex: 0, pageSize: 10 },
pageIndex: 0,
pageSize: 10,
alertsTableConfiguration: {
...alertsTableConfiguration,
useActionsColumn: () => {
@ -712,7 +717,7 @@ describe('AlertsTable', () => {
});
it('should show the cases titles correctly', async () => {
render(<AlertsTableWithProviders {...props} pagination={{ pageIndex: 0, pageSize: 10 }} />);
render(<AlertsTableWithProviders {...props} pageIndex={0} pageSize={10} />);
expect(await screen.findByText('Test case')).toBeInTheDocument();
expect(await screen.findByText('Test case 2')).toBeInTheDocument();
});
@ -721,7 +726,8 @@ describe('AlertsTable', () => {
render(
<AlertsTableWithProviders
{...props}
pagination={{ pageIndex: 0, pageSize: 10 }}
pageIndex={0}
pageSize={10}
cases={{ ...props.cases, isLoading: true }}
/>
);

View file

@ -33,9 +33,9 @@ import {
} from '@elastic/eui';
import { useQueryClient } from '@tanstack/react-query';
import styled from '@emotion/styled';
import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { useSorting, usePagination, useBulkActions, useActionsColumn } from './hooks';
import type {
AlertsTableProps,
@ -50,7 +50,6 @@ import { InspectButtonContainer } from './toolbar/components/inspect';
import { SystemCellId } from './types';
import { SystemCellFactory, systemCells } from './cells';
import { triggersActionsUiQueriesKeys } from '../../hooks/constants';
import { AlertsTableQueryContext } from './contexts/alerts_table_context';
const AlertsFlyout = lazy(() => import('./alerts_flyout'));
const DefaultGridStyle: EuiDataGridStyle = {
@ -233,7 +232,8 @@ type CustomGridBodyProps = Pick<
> & {
alertsData: FetchAlertData['oldAlertsData'];
isLoading: boolean;
pagination: RuleRegistrySearchRequestPagination;
pageIndex: number;
pageSize: number;
actualGridStyle: EuiDataGridStyle;
stripes?: boolean;
};
@ -242,7 +242,8 @@ const CustomGridBody = memo(
({
alertsData,
isLoading,
pagination,
pageIndex,
pageSize,
actualGridStyle,
visibleColumns,
Cell,
@ -251,11 +252,11 @@ const CustomGridBody = memo(
return (
<>
{alertsData
.concat(isLoading ? Array.from({ length: pagination.pageSize - alertsData.length }) : [])
.concat(isLoading ? Array.from({ length: pageSize - alertsData.length }) : [])
.map((_row, rowIndex) => (
<Row
role="row"
key={`${rowIndex},${pagination.pageIndex}`}
key={`${rowIndex},${pageIndex}`}
// manually add stripes if props.gridStyle.stripes is true because presence of rowClasses
// overrides the props.gridStyle.stripes option. And rowClasses will always be there.
// Adding stripes only on even rows. It will be replaced by alertsTableHighlightedRow if
@ -292,7 +293,8 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
leadingControlColumns: passedControlColumns,
trailingControlColumns,
alertsTableConfiguration,
pagination,
pageIndex,
pageSize,
columns,
alerts,
alertsCount,
@ -302,11 +304,11 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
onSortChange,
onPageChange,
sort: sortingFields,
refetch: alertsRefresh,
getInspectQuery,
refetchAlerts,
rowHeightsOptions,
dynamicRowHeight,
query,
querySnapshot,
featureIds,
cases: { data: cases, isLoading: isLoadingCases },
maintenanceWindows: { data: maintenanceWindows, isLoading: isLoadingMaintenanceWindows },
@ -321,7 +323,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
NonNullable<EuiDataGridStyle['rowClasses']>
>({});
const queryClient = useQueryClient({ context: AlertsTableQueryContext });
const queryClient = useQueryClient({ context: AlertsQueryContext });
const { sortingColumns, onSort } = useSorting(onSortChange, visibleColumns, sortingFields);
@ -337,15 +339,23 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
const bulkActionArgs = useMemo(() => {
return {
alerts,
alertsCount: alerts.length,
casesConfig: alertsTableConfiguration.cases,
query,
useBulkActionsConfig: alertsTableConfiguration.useBulkActions,
refresh: alertsRefresh,
refresh: refetchAlerts,
featureIds,
hideBulkActions: Boolean(alertsTableConfiguration.hideBulkActions),
};
}, [alerts, alertsTableConfiguration, query, alertsRefresh, featureIds]);
}, [
alerts.length,
alertsTableConfiguration.cases,
alertsTableConfiguration.useBulkActions,
alertsTableConfiguration.hideBulkActions,
query,
refetchAlerts,
featureIds,
]);
const {
isBulkActionsColumnActive,
@ -357,11 +367,11 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
} = useBulkActions(bulkActionArgs);
const refreshData = useCallback(() => {
alertsRefresh();
refetchAlerts();
queryClient.invalidateQueries(triggersActionsUiQueriesKeys.cases());
queryClient.invalidateQueries(triggersActionsUiQueriesKeys.mutedAlerts());
queryClient.invalidateQueries(triggersActionsUiQueriesKeys.maintenanceWindows());
}, [alertsRefresh, queryClient]);
}, [refetchAlerts, queryClient]);
const refresh = useCallback(() => {
refreshData();
@ -377,8 +387,8 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
setFlyoutAlertIndex,
} = usePagination({
onPageChange,
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageIndex,
pageSize,
});
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
@ -411,7 +421,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
clearSelection,
refresh,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
toolbarVisibilityProp,
};
@ -428,7 +438,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
clearSelection,
refresh,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
toolbarVisibilityProp,
alerts,
@ -467,7 +477,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
}
}, [bulkActionsColumn, customActionsRow, passedControlColumns]);
const rowIndex = flyoutAlertIndex + pagination.pageIndex * pagination.pageSize;
const rowIndex = flyoutAlertIndex + pageIndex * pageSize;
useEffect(() => {
// Row classes do not deal with visible row indices, so we need to handle page offset
setActiveRowClasses({
@ -547,7 +557,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
renderCellPopover
? (_props: EuiDataGridCellPopoverElementProps) => {
try {
const idx = _props.rowIndex - pagination.pageSize * pagination.pageIndex;
const idx = _props.rowIndex - pageSize * pageIndex;
const alert = alerts[idx];
if (alert) {
return renderCellPopover({
@ -561,7 +571,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
}
}
: undefined,
[alerts, pagination.pageIndex, pagination.pageSize, renderCellPopover]
[alerts, pageIndex, pageSize, renderCellPopover]
);
const dataGridPagination = useMemo(
@ -588,8 +598,8 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
data: oldAlertsData,
ecsData: ecsAlertsData,
dataGridRef,
pageSize: pagination.pageSize,
pageIndex: pagination.pageIndex,
pageSize,
pageIndex,
})
: getCellActionsStub;
@ -615,8 +625,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
return alerts.reduce<NonNullable<EuiDataGridStyle['rowClasses']>>(
(rowClasses, alert, index) => {
if (shouldHighlightRow(alert)) {
rowClasses[index + pagination.pageIndex * pagination.pageSize] =
'alertsTableHighlightedRow';
rowClasses[index + pageIndex * pageSize] = 'alertsTableHighlightedRow';
}
return rowClasses;
@ -626,7 +635,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
} else {
return stableMappedRowClasses;
}
}, [shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]);
}, [shouldHighlightRow, alerts, pageIndex, pageSize]);
const mergedGridStyle = useMemo(() => {
const propGridStyle: NonNullable<EuiDataGridStyle> = props.gridStyle ?? {};
@ -679,12 +688,13 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
Cell={Cell}
actualGridStyle={actualGridStyle}
alertsData={oldAlertsData}
pagination={pagination}
pageIndex={pageIndex}
pageSize={pageSize}
isLoading={isLoading}
stripes={props.gridStyle?.stripes}
/>
),
[actualGridStyle, oldAlertsData, pagination, isLoading, props.gridStyle?.stripes]
[actualGridStyle, oldAlertsData, pageIndex, pageSize, isLoading, props.gridStyle?.stripes]
);
const sortProps = useMemo(() => {
@ -704,7 +714,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = memo((props: Aler
alertsCount={alertsCount}
onClose={handleFlyoutClose}
alertsTableConfiguration={alertsTableConfiguration}
flyoutIndex={flyoutAlertIndex + pagination.pageIndex * pagination.pageSize}
flyoutIndex={flyoutAlertIndex + pageIndex * pageSize}
onPaginate={onPaginateFlyout}
isLoading={isLoading}
id={props.id}

View file

@ -8,7 +8,7 @@ import React from 'react';
import { BehaviorSubject } from 'rxjs';
import userEvent from '@testing-library/user-event';
import { get } from 'lodash';
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
import { fireEvent, render, waitFor, screen, act } from '@testing-library/react';
import {
AlertConsumers,
ALERT_CASE_IDS,
@ -22,12 +22,13 @@ import {
AlertsField,
AlertsTableConfigurationRegistry,
AlertsTableFlyoutBaseProps,
AlertsTableProps,
FetchAlertData,
RenderCustomActionsRowArgs,
} from '../../../types';
import { PLUGIN_ID } from '../../../common/constants';
import AlertsTableState, { AlertsTableStateProps } from './alerts_table_state';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { AlertsTable } from './alerts_table';
import { useFetchBrowserFieldCapabilities } from './hooks/use_fetch_browser_fields_capabilities';
import { useBulkGetCases } from './hooks/use_bulk_get_cases';
import { DefaultSort } from './hooks';
@ -38,8 +39,17 @@ import { createCasesServiceMock } from './index.mock';
import { useBulkGetMaintenanceWindows } from './hooks/use_bulk_get_maintenance_windows';
import { getMaintenanceWindowMockMap } from './maintenance_windows/index.mock';
import { AlertTableConfigRegistry } from '../../alert_table_config_registry';
import { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks';
jest.mock('@kbn/alerts-ui-shared/src/common/hooks/use_search_alerts_query');
jest.mock('./alerts_table', () => {
return {
AlertsTable: jest.fn(),
};
});
const MockAlertsTable = AlertsTable as jest.Mock;
jest.mock('./hooks/use_fetch_alerts');
jest.mock('./hooks/use_fetch_browser_fields_capabilities');
jest.mock('./hooks/use_bulk_get_cases');
jest.mock('./hooks/use_bulk_get_maintenance_windows');
@ -49,7 +59,7 @@ jest.mock('@kbn/kibana-utils-plugin/public');
const mockCurrentAppId$ = new BehaviorSubject<string>('testAppId');
const mockCaseService = createCasesServiceMock();
jest.mock('@kbn/kibana-react-plugin/public', () => ({
jest.mock('../../../common/lib/kibana/kibana_react', () => ({
useKibana: () => ({
services: {
application: {
@ -71,6 +81,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
addDanger: () => {},
},
},
data: {},
},
}),
}));
@ -295,18 +306,19 @@ storageMock.mockImplementation(() => {
});
const refetchMock = jest.fn();
const hookUseFetchAlerts = useFetchAlerts as jest.Mock;
const fetchAlertsResponse = {
alerts,
isInitializing: false,
getInspectQuery: jest.fn(),
const mockUseSearchAlertsQuery = useSearchAlertsQuery as jest.Mock;
const searchAlertsResponse = {
data: {
alerts,
ecsAlertsData,
oldAlertsData,
total: alerts.length,
querySnapshot: { request: [], response: [] },
},
refetch: refetchMock,
totalAlerts: alerts.length,
ecsAlertsData,
oldAlertsData,
};
hookUseFetchAlerts.mockReturnValue([false, fetchAlertsResponse]);
mockUseSearchAlertsQuery.mockReturnValue(searchAlertsResponse);
const hookUseFetchBrowserFieldCapabilities = useFetchBrowserFieldCapabilities as jest.Mock;
hookUseFetchBrowserFieldCapabilities.mockImplementation(() => [false, {}]);
@ -363,6 +375,16 @@ describe('AlertsTableState', () => {
};
};
let onPageChange: AlertsTableProps['onPageChange'];
let refetchAlerts: AlertsTableProps['refetchAlerts'];
MockAlertsTable.mockImplementation((props) => {
const { AlertsTable: AlertsTableComponent } = jest.requireActual('./alerts_table');
onPageChange = props.onPageChange;
refetchAlerts = props.refetchAlerts;
return <AlertsTableComponent {...props} />;
});
beforeEach(() => {
jest.clearAllMocks();
useBulkGetCasesMock.mockReturnValue({ data: casesMap, isFetching: false });
@ -409,13 +431,13 @@ describe('AlertsTableState', () => {
});
it('remove duplicated case ids', async () => {
hookUseFetchAlerts.mockReturnValue([
false,
{
...fetchAlertsResponse,
alerts: [...fetchAlertsResponse.alerts, ...fetchAlertsResponse.alerts],
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
data: {
...searchAlertsResponse.data,
alerts: [...searchAlertsResponse.data.alerts, ...searchAlertsResponse.data.alerts],
},
]);
});
render(<AlertsTableWithLocale {...tableProps} />);
@ -425,16 +447,16 @@ describe('AlertsTableState', () => {
});
it('skips alerts with empty case ids', async () => {
hookUseFetchAlerts.mockReturnValue([
false,
{
...fetchAlertsResponse,
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
data: {
...searchAlertsResponse.data,
alerts: [
{ ...fetchAlertsResponse.alerts[0], 'kibana.alert.case_ids': [] },
fetchAlertsResponse.alerts[1],
{ ...searchAlertsResponse.data.alerts[0], 'kibana.alert.case_ids': [] },
searchAlertsResponse.data.alerts[1],
],
},
]);
});
render(<AlertsTableWithLocale {...tableProps} />);
@ -598,13 +620,13 @@ describe('AlertsTableState', () => {
});
it('should remove duplicated maintenance window ids', async () => {
hookUseFetchAlerts.mockReturnValue([
false,
{
...fetchAlertsResponse,
alerts: [...fetchAlertsResponse.alerts, ...fetchAlertsResponse.alerts],
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
data: {
...searchAlertsResponse.data,
alerts: [...searchAlertsResponse.data.alerts, ...searchAlertsResponse.data.alerts],
},
]);
});
render(<AlertsTableWithLocale {...tableProps} />);
await waitFor(() => {
@ -618,16 +640,16 @@ describe('AlertsTableState', () => {
});
it('should skip alerts with empty maintenance window ids', async () => {
hookUseFetchAlerts.mockReturnValue([
false,
{
...fetchAlertsResponse,
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
data: {
...searchAlertsResponse.data,
alerts: [
{ ...fetchAlertsResponse.alerts[0], 'kibana.alert.maintenance_window_ids': [] },
fetchAlertsResponse.alerts[1],
{ ...searchAlertsResponse.data.alerts[0], 'kibana.alert.maintenance_window_ids': [] },
searchAlertsResponse.data.alerts[1],
],
},
]);
});
render(<AlertsTableWithLocale {...tableProps} />);
await waitFor(() => {
@ -716,7 +738,7 @@ describe('AlertsTableState', () => {
<AlertsTableWithLocale
{...{
...tableProps,
pageSize: 1,
initialPageSize: 1,
}}
/>
);
@ -725,26 +747,22 @@ describe('AlertsTableState', () => {
const result = await wrapper.findAllByTestId('alertsFlyout');
expect(result.length).toBe(1);
hookUseFetchAlerts.mockClear();
mockUseSearchAlertsQuery.mockClear();
userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]);
expect(hookUseFetchAlerts).toHaveBeenCalledWith(
expect(mockUseSearchAlertsQuery).toHaveBeenCalledWith(
expect.objectContaining({
pagination: {
pageIndex: 1,
pageSize: 1,
},
pageIndex: 1,
pageSize: 1,
})
);
hookUseFetchAlerts.mockClear();
mockUseSearchAlertsQuery.mockClear();
userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]);
expect(hookUseFetchAlerts).toHaveBeenCalledWith(
expect(mockUseSearchAlertsQuery).toHaveBeenCalledWith(
expect.objectContaining({
pagination: {
pageIndex: 0,
pageSize: 1,
},
pageIndex: 0,
pageSize: 1,
})
);
});
@ -754,7 +772,7 @@ describe('AlertsTableState', () => {
<AlertsTableWithLocale
{...{
...tableProps,
pageSize: 2,
initialPageSize: 2,
}}
/>
);
@ -763,26 +781,22 @@ describe('AlertsTableState', () => {
const result = await wrapper.findAllByTestId('alertsFlyout');
expect(result.length).toBe(1);
hookUseFetchAlerts.mockClear();
mockUseSearchAlertsQuery.mockClear();
userEvent.click(wrapper.queryAllByTestId('pagination-button-last')[0]);
expect(hookUseFetchAlerts).toHaveBeenCalledWith(
expect(mockUseSearchAlertsQuery).toHaveBeenCalledWith(
expect.objectContaining({
pagination: {
pageIndex: 1,
pageSize: 2,
},
pageIndex: 1,
pageSize: 2,
})
);
hookUseFetchAlerts.mockClear();
mockUseSearchAlertsQuery.mockClear();
userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]);
expect(hookUseFetchAlerts).toHaveBeenCalledWith(
expect(mockUseSearchAlertsQuery).toHaveBeenCalledWith(
expect.objectContaining({
pagination: {
pageIndex: 0,
pageSize: 2,
},
pageIndex: 0,
pageSize: 2,
})
);
});
@ -912,7 +926,9 @@ describe('AlertsTableState', () => {
});
it('should show the inspect button if the right prop is set', async () => {
const props = mockCustomProps({ showInspectButton: true });
const props = mockCustomProps({
showInspectButton: true,
});
render(<AlertsTableWithLocale {...props} />);
expect(await screen.findByTestId('inspect-icon-button')).toBeInTheDocument();
});
@ -921,16 +937,14 @@ describe('AlertsTableState', () => {
describe('empty state', () => {
beforeEach(() => {
refetchMock.mockClear();
hookUseFetchAlerts.mockImplementation(() => [
false,
{
mockUseSearchAlertsQuery.mockReturnValue({
data: {
alerts: [],
isInitializing: false,
getInspectQuery: jest.fn(),
refetch: refetchMock,
totalAlerts: 0,
total: 0,
querySnapshot: { request: [], response: [] },
},
]);
refetch: refetchMock,
});
});
it('should render an empty screen if there are no alerts', async () => {
@ -985,4 +999,43 @@ describe('AlertsTableState', () => {
expect(screen.queryByTestId('dataGridColumnSortingButton')).not.toBeInTheDocument();
});
});
describe('Pagination', () => {
it('resets the page index when any query parameter changes', () => {
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
alerts: Array.from({ length: 100 }).map((_, i) => ({ [AlertsField.uuid]: `alert-${i}` })),
});
const { rerender } = render(<AlertsTableWithLocale {...tableProps} />);
act(() => {
onPageChange({ pageIndex: 1, pageSize: 50 });
});
rerender(
<AlertsTableWithLocale
{...tableProps}
query={{ bool: { filter: [{ term: { 'kibana.alert.rule.name': 'test' } }] } }}
/>
);
expect(mockUseSearchAlertsQuery).toHaveBeenLastCalledWith(
expect.objectContaining({ pageIndex: 0 })
);
});
it('resets the page index when refetching alerts', () => {
mockUseSearchAlertsQuery.mockReturnValue({
...searchAlertsResponse,
alerts: Array.from({ length: 100 }).map((_, i) => ({ [AlertsField.uuid]: `alert-${i}` })),
});
render(<AlertsTableWithLocale {...tableProps} />);
act(() => {
onPageChange({ pageIndex: 1, pageSize: 50 });
});
act(() => {
refetchAlerts();
});
expect(mockUseSearchAlertsQuery).toHaveBeenLastCalledWith(
expect.objectContaining({ pageIndex: 0 })
);
});
});
});

View file

@ -20,7 +20,6 @@ import {
EuiDataGridControlColumn,
} from '@elastic/eui';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import { ALERT_CASE_IDS, ALERT_MAINTENANCE_WINDOW_IDS } from '@kbn/rule-data-utils';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import type {
@ -28,14 +27,17 @@ import type {
RuleRegistrySearchRequestPagination,
} from '@kbn/rule-registry-plugin/common';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type {
QueryDslQueryContainer,
SortCombinations,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { QueryClientProvider } from '@tanstack/react-query';
import { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks';
import { DEFAULT_ALERTS_PAGE_SIZE } from '@kbn/alerts-ui-shared/src/common/constants';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import deepEqual from 'fast-deep-equal';
import { useKibana } from '../../../common/lib/kibana';
import { useGetMutedAlerts } from './hooks/alert_mute/use_get_muted_alerts';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { AlertsTable } from './alerts_table';
import { EmptyState } from './empty_state';
import {
@ -61,21 +63,16 @@ import { alertsTableQueryClient } from './query_client';
import { useBulkGetCases } from './hooks/use_bulk_get_cases';
import { useBulkGetMaintenanceWindows } from './hooks/use_bulk_get_maintenance_windows';
import { CasesService } from './types';
import { AlertsTableContext, AlertsTableQueryContext } from './contexts/alerts_table_context';
import { AlertsTableContext } from './contexts/alerts_table_context';
import { ErrorBoundary, FallbackComponent } from '../common/components/error_boundary';
const DefaultPagination = {
pageSize: 10,
pageIndex: 0,
};
export type AlertsTableStateProps = {
alertsTableConfigurationRegistry: AlertsTableConfigurationRegistryContract;
configurationId: string;
id: string;
featureIds: ValidFeatureId[];
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
pageSize?: number;
initialPageSize?: number;
browserFields?: BrowserFields;
onUpdate?: (args: TableUpdateHandlerArgs) => void;
onLoaded?: (alerts: Alerts) => void;
@ -180,7 +177,7 @@ const ErrorBoundaryFallback: FallbackComponent = ({ error }) => {
const AlertsTableState = memo((props: AlertsTableStateProps) => {
return (
<QueryClientProvider client={alertsTableQueryClient} context={AlertsTableQueryContext}>
<QueryClientProvider client={alertsTableQueryClient} context={AlertsQueryContext}>
<ErrorBoundary fallback={ErrorBoundaryFallback}>
<AlertsTableStateWithQueryProvider {...props} />
</ErrorBoundary>
@ -199,7 +196,7 @@ const AlertsTableStateWithQueryProvider = memo(
id,
featureIds,
query,
pageSize,
initialPageSize = DEFAULT_ALERTS_PAGE_SIZE,
leadingControlColumns = DEFAULT_LEADING_CONTROL_COLUMNS,
trailingControlColumns,
rowHeightsOptions,
@ -217,10 +214,13 @@ const AlertsTableStateWithQueryProvider = memo(
lastReloadRequestTime,
emptyStateHeight,
}: AlertsTableStateProps) => {
const { cases: casesService, fieldFormats } = useKibana<{
const {
data,
cases: casesService,
fieldFormats,
} = useKibana().services as ReturnType<typeof useKibana>['services'] & {
cases?: CasesService;
fieldFormats: FieldFormatsRegistry;
}>().services;
};
const hasAlertsTableConfiguration =
alertsTableConfigurationRegistry?.has(configurationId) ?? false;
@ -273,13 +273,13 @@ const AlertsTableStateWithQueryProvider = memo(
storageAlertsTable.current = getStorageConfig();
const [sort, setSort] = useState<SortCombinations[]>(storageAlertsTable.current.sort);
const [pagination, setPagination] = useState({
...DefaultPagination,
pageSize: pageSize ?? DefaultPagination.pageSize,
});
const onPageChange = useCallback((_pagination: RuleRegistrySearchRequestPagination) => {
setPagination(_pagination);
const onPageChange = useCallback((pagination: RuleRegistrySearchRequestPagination) => {
setQueryParams((prevQueryParams) => ({
...prevQueryParams,
pageSize: pagination.pageSize,
pageIndex: pagination.pageIndex,
}));
}, []);
const {
@ -301,29 +301,68 @@ const AlertsTableStateWithQueryProvider = memo(
initialBrowserFields: propBrowserFields,
});
const [
isLoading,
{
alerts,
oldAlertsData,
ecsAlertsData,
isInitializing,
getInspectQuery,
refetch: refresh,
totalAlerts: alertsCount,
},
] = useFetchAlerts({
fields,
const [queryParams, setQueryParams] = useState({
featureIds,
fields,
query,
pagination,
onPageChange,
onLoaded,
runtimeMappings,
sort,
skip: false,
runtimeMappings,
pageIndex: 0,
pageSize: initialPageSize,
});
useEffect(() => {
setQueryParams(({ pageIndex: oldPageIndex, pageSize: oldPageSize, ...prevQueryParams }) => ({
featureIds,
fields,
query,
sort,
runtimeMappings,
// Go back to the first page if the query changes
pageIndex: !deepEqual(prevQueryParams, {
featureIds,
fields,
query,
sort,
runtimeMappings,
})
? 0
: oldPageIndex,
pageSize: oldPageSize,
}));
}, [featureIds, fields, query, runtimeMappings, sort]);
const {
data: alertsData,
refetch,
isSuccess,
isFetching: isLoading,
} = useSearchAlertsQuery({
data,
...queryParams,
});
const {
alerts = [],
oldAlertsData = [],
ecsAlertsData = [],
total: alertsCount = -1,
querySnapshot,
} = alertsData ?? {};
const refetchAlerts = useCallback(() => {
if (queryParams.pageIndex !== 0) {
// Refetch from the first page
setQueryParams((prevQueryParams) => ({ ...prevQueryParams, pageIndex: 0 }));
}
refetch();
}, [queryParams.pageIndex, refetch]);
useEffect(() => {
if (onLoaded && !isLoading && isSuccess) {
onLoaded(alerts);
}
}, [alerts, isLoading, isSuccess, onLoaded]);
const mutedAlertIds = useMemo(() => {
return [...new Set(alerts.map((a) => a['kibana.alert.rule.uuid']![0]))];
}, [alerts]);
@ -350,14 +389,15 @@ const AlertsTableStateWithQueryProvider = memo(
useEffect(() => {
if (onUpdate) {
onUpdate({ isLoading, totalCount: alertsCount, refresh });
onUpdate({ isLoading, totalCount: alertsCount, refresh: refetch });
}
}, [isLoading, alertsCount, onUpdate, refresh]);
}, [isLoading, alertsCount, onUpdate, refetch]);
useEffect(() => {
if (lastReloadRequestTime) {
refresh();
refetch();
}
}, [lastReloadRequestTime, refresh]);
}, [lastReloadRequestTime, refetch]);
const caseIds = useMemo(() => getCaseIdsFromAlerts(alerts), [alerts]);
const maintenanceWindowIds = useMemo(() => getMaintenanceWindowIdsFromAlerts(alerts), [alerts]);
@ -380,7 +420,7 @@ const AlertsTableStateWithQueryProvider = memo(
return {
ids: Array.from(maintenanceWindowIds.values()),
canFetchMaintenanceWindows: fetchMaintenanceWindows,
queryContext: AlertsTableQueryContext,
queryContext: AlertsQueryContext,
};
}, [fetchMaintenanceWindows, maintenanceWindowIds]);
@ -462,15 +502,15 @@ const AlertsTableStateWithQueryProvider = memo(
shouldHighlightRow,
dynamicRowHeight,
featureIds,
isInitializing,
pagination,
querySnapshot,
pageIndex: queryParams.pageIndex,
pageSize: queryParams.pageSize,
sort,
isLoading,
alerts,
oldAlertsData,
ecsAlertsData,
getInspectQuery,
refetch: refresh,
refetchAlerts,
alertsCount,
onSortChange,
onPageChange,
@ -483,8 +523,8 @@ const AlertsTableStateWithQueryProvider = memo(
columns,
id,
leadingControlColumns,
trailingControlColumns,
showAlertStatusWithFlapping,
trailingControlColumns,
visibleColumns,
browserFields,
onToggleColumn,
@ -493,6 +533,7 @@ const AlertsTableStateWithQueryProvider = memo(
onColumnResize,
query,
rowHeightsOptions,
cellContext,
gridStyle,
persistentControls,
showInspectButton,
@ -500,16 +541,15 @@ const AlertsTableStateWithQueryProvider = memo(
shouldHighlightRow,
dynamicRowHeight,
featureIds,
cellContext,
isInitializing,
pagination,
querySnapshot,
queryParams.pageIndex,
queryParams.pageSize,
sort,
isLoading,
alerts,
oldAlertsData,
ecsAlertsData,
getInspectQuery,
refresh,
refetchAlerts,
alertsCount,
onSortChange,
onPageChange,
@ -524,14 +564,25 @@ const AlertsTableStateWithQueryProvider = memo(
};
}, [activeBulkActionsReducer, mutedAlerts]);
return hasAlertsTableConfiguration ? (
if (!hasAlertsTableConfiguration) {
return (
<EuiEmptyPrompt
data-test-subj="alertsTableNoConfiguration"
iconType="watchesApp"
title={<h2>{ALERTS_TABLE_CONF_ERROR_TITLE}</h2>}
body={<p>{ALERTS_TABLE_CONF_ERROR_MESSAGE}</p>}
/>
);
}
return (
<AlertsTableContext.Provider value={alertsTableContext}>
{!isLoading && alertsCount === 0 && (
<InspectButtonContainer>
<EmptyState
controls={persistentControls}
getInspectQuery={getInspectQuery}
showInpectButton={showInspectButton}
querySnapshot={querySnapshot}
showInspectButton={showInspectButton}
height={emptyStateHeight}
/>
</InspectButtonContainer>
@ -539,24 +590,19 @@ const AlertsTableStateWithQueryProvider = memo(
{(isLoading || isBrowserFieldDataLoading) && (
<EuiProgress size="xs" color="accent" data-test-subj="internalAlertsPageLoading" />
)}
{alertsCount !== 0 && isCasesContextAvailable && (
<CasesContext
owner={alertsTableConfiguration.cases?.owner ?? []}
permissions={casesPermissions}
features={{ alerts: { sync: alertsTableConfiguration.cases?.syncAlerts ?? false } }}
>
{alertsCount > 0 &&
(isCasesContextAvailable ? (
<CasesContext
owner={alertsTableConfiguration.cases?.owner ?? []}
permissions={casesPermissions}
features={{ alerts: { sync: alertsTableConfiguration.cases?.syncAlerts ?? false } }}
>
<AlertsTable {...tableProps} />
</CasesContext>
) : (
<AlertsTable {...tableProps} />
</CasesContext>
)}
{alertsCount !== 0 && !isCasesContextAvailable && <AlertsTable {...tableProps} />}
))}
</AlertsTableContext.Provider>
) : (
<EuiEmptyPrompt
data-test-subj="alertsTableNoConfiguration"
iconType="watchesApp"
title={<h2>{ALERTS_TABLE_CONF_ERROR_TITLE}</h2>}
body={<p>{ALERTS_TABLE_CONF_ERROR_MESSAGE}</p>}
/>
);
}
);

View file

@ -25,7 +25,8 @@ import { createAppMockRenderer } from '../../test_utils';
import { getCasesMockMap } from '../cases/index.mock';
import { getMaintenanceWindowMockMap } from '../maintenance_windows/index.mock';
import { createCasesServiceMock } from '../index.mock';
import { AlertsTableContext, AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { AlertsTableContext } from '../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('@kbn/data-plugin/public');
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
@ -133,6 +134,10 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {
const originalGetComputedStyle = Object.assign({}, window.getComputedStyle);
type AlertsTableWithBulkActionsContextProps = AlertsTableProps & {
initialBulkActionsState?: BulkActionsState;
};
describe('AlertsTable.BulkActions', () => {
beforeAll(() => {
// The JSDOM implementation is too slow
@ -240,23 +245,25 @@ describe('AlertsTable.BulkActions', () => {
onChangeVisibleColumns: () => {},
browserFields: {},
query: {},
pagination: { pageIndex: 0, pageSize: 1 },
pageIndex: 0,
pageSize: 1,
sort: [],
isLoading: false,
alerts,
oldAlertsData,
ecsAlertsData,
getInspectQuery: () => ({ request: [], response: [] }),
refetch: refreshMockFn,
querySnapshot: { request: [], response: [] },
refetchAlerts: refreshMockFn,
alertsCount: alerts.length,
onSortChange: () => {},
onPageChange: () => {},
fieldFormats: mockFieldFormatsRegistry,
};
const tablePropsWithBulkActions = {
const tablePropsWithBulkActions: AlertsTableWithBulkActionsContextProps = {
...tableProps,
pagination: { pageIndex: 0, pageSize: 10 },
pageIndex: 0,
pageSize: 10,
alertsTableConfiguration: {
...alertsTableConfiguration,
@ -317,9 +324,9 @@ describe('AlertsTable.BulkActions', () => {
};
const AlertsTableWithBulkActionsContext: React.FunctionComponent<
AlertsTableProps & { initialBulkActionsState?: BulkActionsState }
AlertsTableWithBulkActionsContextProps
> = (props) => {
const renderer = useMemo(() => createAppMockRenderer(AlertsTableQueryContext), []);
const renderer = useMemo(() => createAppMockRenderer(AlertsQueryContext), []);
const AppWrapper = renderer.AppWrapper;
const initialBulkActionsState = useReducer(
@ -416,9 +423,8 @@ describe('AlertsTable.BulkActions', () => {
] as unknown as Alerts,
};
const props = {
const props: AlertsTableWithBulkActionsContextProps = {
...tablePropsWithBulkActions,
useFetchAlertsData: () => newAlertsData,
initialBulkActionsState: {
...defaultBulkActionsState,
isAllSelected: true,
@ -569,23 +575,17 @@ describe('AlertsTable.BulkActions', () => {
},
] as unknown as Alerts;
const allAlerts = [...alerts, ...secondPageAlerts];
const props = {
const props: AlertsTableWithBulkActionsContextProps = {
...tablePropsWithBulkActions,
alerts: allAlerts,
alertsCount: allAlerts.length,
useFetchAlertsData: () => {
return {
...alertsData,
alertsCount: secondPageAlerts.length,
activePage: 1,
};
},
initialBulkActionsState: {
...defaultBulkActionsState,
areAllVisibleRowsSelected: true,
rowSelection: new Map([[0, { isLoading: false }]]),
},
pagination: { pageIndex: 1, pageSize: 2 },
pageIndex: 1,
pageSize: 2,
};
render(<AlertsTableWithBulkActionsContext {...props} />);

View file

@ -7,11 +7,8 @@
import { createContext } from 'react';
import { noop } from 'lodash';
import { QueryClient } from '@tanstack/react-query';
import { AlertsTableContextType } from '../types';
export const AlertsTableQueryContext = createContext<QueryClient | undefined>(undefined);
export const AlertsTableContext = createContext<AlertsTableContextType>({
mutedAlerts: {},
bulkActions: [{}, noop] as unknown as AlertsTableContextType['bulkActions'],

View file

@ -16,7 +16,7 @@ import {
EuiDataGridToolBarAdditionalControlsOptions,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { GetInspectQuery } from '../../../types';
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
import icon from './assets/illustration_product_no_results_magnifying_glass.svg';
import { InspectButton } from './toolbar/components/inspect';
import { ALERTS_TABLE_TITLE } from './translations';
@ -33,15 +33,15 @@ const panelStyle = {
export const EmptyState: React.FC<{
height?: keyof typeof heights;
controls?: EuiDataGridToolBarAdditionalControlsOptions;
getInspectQuery: GetInspectQuery;
showInpectButton?: boolean;
}> = ({ height = 'tall', controls, getInspectQuery, showInpectButton }) => {
querySnapshot?: EsQuerySnapshot;
showInspectButton?: boolean;
}> = ({ height = 'tall', controls, querySnapshot, showInspectButton }) => {
return (
<EuiPanel color="subdued" data-test-subj="alertsStateTableEmptyState">
<EuiFlexGroup alignItems="flexEnd" justifyContent="flexEnd">
{showInpectButton && (
{querySnapshot && showInspectButton && (
<EuiFlexItem grow={false}>
<InspectButton getInspectQuery={getInspectQuery} inspectTitle={ALERTS_TABLE_TITLE} />
<InspectButton querySnapshot={querySnapshot} inspectTitle={ALERTS_TABLE_TITLE} />
</EuiFlexItem>
)}
{controls?.right && <EuiFlexItem grow={false}>{controls.right}</EuiFlexItem>}

View file

@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
import { useKibana } from '../../../../../common/lib/kibana';
import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils';
import { useGetMutedAlerts } from './use_get_muted_alerts';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('../apis/get_rules_muted_alerts');
jest.mock('../../../../../common/lib/kibana');
@ -25,7 +25,7 @@ describe('useGetMutedAlerts', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer(AlertsTableQueryContext);
appMockRender = createAppMockRenderer(AlertsQueryContext);
});
it('calls the api when invoked with the correct parameters', async () => {

View file

@ -7,11 +7,11 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { getMutedAlerts } from '../apis/get_rules_muted_alerts';
import { useKibana } from '../../../../../common';
import { triggersActionsUiQueriesKeys } from '../../../../hooks/constants';
import { MutedAlerts, ServerError } from '../../types';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
const ERROR_TITLE = i18n.translate('xpack.triggersActionsUI.mutedAlerts.api.get', {
defaultMessage: 'Error fetching muted alerts data',
@ -32,7 +32,7 @@ export const useGetMutedAlerts = (ruleIds: string[], enabled = true) => {
}, {} as MutedAlerts)
),
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
enabled: ruleIds.length > 0 && enabled,
onError: (error: ServerError) => {
if (error.name !== 'AbortError') {

View file

@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
import { useKibana } from '../../../../../common/lib/kibana';
import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils';
import { useMuteAlert } from './use_mute_alert';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('../../../../lib/rule_api/mute_alert');
jest.mock('../../../../../common/lib/kibana');
@ -25,7 +25,7 @@ describe('useMuteAlert', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer(AlertsTableQueryContext);
appMockRender = createAppMockRenderer(AlertsQueryContext);
});
it('calls the api when invoked with the correct parameters', async () => {

View file

@ -7,7 +7,7 @@
import { useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { muteAlertInstance } from '../../../../lib/rule_api/mute_alert';
import { useKibana } from '../../../../..';
import { ServerError, ToggleAlertParams } from '../../types';
@ -25,7 +25,7 @@ export const useMuteAlert = () => {
({ ruleId, alertInstanceId }: ToggleAlertParams) =>
muteAlertInstance({ http, id: ruleId, instanceId: alertInstanceId }),
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
onSuccess() {
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.alertsTable.alertMuted', {

View file

@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
import { useKibana } from '../../../../../common/lib/kibana';
import { AppMockRenderer, createAppMockRenderer } from '../../../test_utils';
import { useUnmuteAlert } from './use_unmute_alert';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('../../../../lib/rule_api/mute_alert');
jest.mock('../../../../../common/lib/kibana');
@ -25,7 +25,7 @@ describe('useUnmuteAlert', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer(AlertsTableQueryContext);
appMockRender = createAppMockRenderer(AlertsQueryContext);
});
it('calls the api when invoked with the correct parameters', async () => {

View file

@ -7,7 +7,7 @@
import { useMutation } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { AlertsTableQueryContext } from '../../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { ServerError, ToggleAlertParams } from '../../types';
import { unmuteAlertInstance } from '../../../../lib/rule_api/unmute_alert';
import { useKibana } from '../../../../..';
@ -25,7 +25,7 @@ export const useUnmuteAlert = () => {
({ ruleId, alertInstanceId }: ToggleAlertParams) =>
unmuteAlertInstance({ http, id: ruleId, instanceId: alertInstanceId }),
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
onSuccess() {
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.alertsTable.alertUnuted', {

View file

@ -8,8 +8,6 @@ export type { UsePagination } from './use_pagination';
export { usePagination } from './use_pagination';
export type { UseSorting } from './use_sorting';
export { useSorting } from './use_sorting';
export type { UseFetchAlerts } from './use_fetch_alerts';
export { useFetchAlerts } from './use_fetch_alerts';
export { DefaultSort } from './constants';
export { useBulkActions } from './use_bulk_actions';
export { useActionsColumn } from './use_actions_column';

View file

@ -9,8 +9,8 @@ import { renderHook } from '@testing-library/react-hooks';
import { useBulkActions, useBulkAddToCaseActions, useBulkUntrackActions } from './use_bulk_actions';
import { AppMockRenderer, createAppMockRenderer } from '../../test_utils';
import { createCasesServiceMock } from '../index.mock';
import { AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { BulkActionsVerbs } from '../../../../types';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('./apis/bulk_get_cases');
jest.mock('../../../../common/lib/kibana');
@ -39,7 +39,7 @@ describe('bulk action hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer(AlertsTableQueryContext);
appMockRender = createAppMockRenderer(AlertsQueryContext);
});
const refresh = jest.fn();
@ -348,7 +348,7 @@ describe('bulk action hooks', () => {
it('appends the case and untrack bulk actions', async () => {
const { result } = renderHook(
() => useBulkActions({ alerts: [], query: {}, casesConfig, refresh }),
() => useBulkActions({ alertsCount: 0, query: {}, casesConfig, refresh }),
{
wrapper: appMockRender.AppWrapper,
}
@ -391,7 +391,8 @@ describe('bulk action hooks', () => {
it('appends only the case bulk actions for SIEM', async () => {
const { result } = renderHook(
() => useBulkActions({ alerts: [], query: {}, casesConfig, refresh, featureIds: ['siem'] }),
() =>
useBulkActions({ alertsCount: 0, query: {}, casesConfig, refresh, featureIds: ['siem'] }),
{
wrapper: appMockRender.AppWrapper,
}
@ -444,7 +445,8 @@ describe('bulk action hooks', () => {
];
const useBulkActionsConfig = () => customBulkActionConfig;
const { result, rerender } = renderHook(
() => useBulkActions({ alerts: [], query: {}, casesConfig, refresh, useBulkActionsConfig }),
() =>
useBulkActions({ alertsCount: 0, query: {}, casesConfig, refresh, useBulkActionsConfig }),
{
wrapper: appMockRender.AppWrapper,
}
@ -470,7 +472,7 @@ describe('bulk action hooks', () => {
const { result: resultWithoutHideBulkActions } = renderHook(
() =>
useBulkActions({
alerts: [],
alertsCount: 0,
query: {},
casesConfig,
refresh,
@ -486,7 +488,7 @@ describe('bulk action hooks', () => {
const { result: resultWithHideBulkActions } = renderHook(
() =>
useBulkActions({
alerts: [],
alertsCount: 0,
query: {},
casesConfig,
refresh,

View file

@ -10,7 +10,6 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ALERT_CASE_IDS, ValidFeatureId } from '@kbn/rule-data-utils';
import { AlertsTableContext } from '../contexts/alerts_table_context';
import {
Alerts,
AlertsTableConfigurationRegistry,
BulkActionsConfig,
BulkActionsPanelConfig,
@ -37,7 +36,7 @@ import { useBulkUntrackAlertsByQuery } from './use_bulk_untrack_alerts_by_query'
interface BulkActionsProps {
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
alerts: Alerts;
alertsCount: number;
casesConfig?: AlertsTableConfigurationRegistry['cases'];
useBulkActionsConfig?: UseBulkActionsRegistry;
refresh: () => void;
@ -273,7 +272,7 @@ export const useBulkUntrackActions = ({
};
export function useBulkActions({
alerts,
alertsCount,
casesConfig,
query,
refresh,
@ -326,9 +325,10 @@ export function useBulkActions({
useEffect(() => {
updateBulkActionsState({
action: BulkActionsVerbs.rowCountUpdate,
rowCount: alerts.length,
rowCount: alertsCount,
});
}, [alerts, updateBulkActionsState]);
}, [alertsCount, updateBulkActionsState]);
return useMemo(() => {
return {
isBulkActionsColumnActive,

View file

@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
import { useKibana } from '../../../../common/lib/kibana';
import { useBulkGetCases } from './use_bulk_get_cases';
import { AppMockRenderer, createAppMockRenderer } from '../../test_utils';
import { AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
jest.mock('./apis/bulk_get_cases');
jest.mock('../../../../common/lib/kibana');
@ -28,7 +28,7 @@ describe('useBulkGetCases', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRender = createAppMockRenderer(AlertsTableQueryContext);
appMockRender = createAppMockRenderer(AlertsQueryContext);
});
it('calls the api when invoked with the correct parameters', async () => {

View file

@ -7,9 +7,9 @@
import { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { useKibana } from '../../../../common';
import { triggersActionsUiQueriesKeys } from '../../../hooks/constants';
import { AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { ServerError } from '../types';
import { bulkGetCases, Case, CasesBulkGetResponse } from './apis/bulk_get_cases';
@ -37,7 +37,7 @@ export const useBulkGetCases = (caseIds: string[], fetchCases: boolean) => {
triggersActionsUiQueriesKeys.casesBulkGet(caseIds),
({ signal }) => bulkGetCases(http, { ids: caseIds }, signal),
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
enabled: caseIds.length > 0 && fetchCases,
select: transformCases,
onError: (error: ServerError) => {

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { useMutation } from '@tanstack/react-query';
import { INTERNAL_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { useKibana } from '../../../../common';
export const useBulkUntrackAlerts = () => {
@ -31,7 +31,7 @@ export const useBulkUntrackAlerts = () => {
}
},
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
onError: (_err, params) => {
toasts.addDanger(
i18n.translate(

View file

@ -10,7 +10,7 @@ import { useMutation } from '@tanstack/react-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { INTERNAL_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { ValidFeatureId } from '@kbn/rule-data-utils';
import { AlertsTableQueryContext } from '../contexts/alerts_table_context';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { useKibana } from '../../../../common';
export const useBulkUntrackAlertsByQuery = () => {
@ -39,7 +39,7 @@ export const useBulkUntrackAlertsByQuery = () => {
}
},
{
context: AlertsTableQueryContext,
context: AlertsQueryContext,
onError: () => {
toasts.addDanger(
i18n.translate('xpack.triggersActionsUI.alertsTable.untrackByQuery.failedMessage', {

View file

@ -1,504 +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 sinon from 'sinon';
import { of, throwError } from 'rxjs';
import { act, renderHook } from '@testing-library/react-hooks';
import { useFetchAlerts, FetchAlertsArgs, FetchAlertResp } from './use_fetch_alerts';
import { useKibana } from '../../../../common/lib/kibana';
import type { IKibanaSearchResponse } from '@kbn/search-types';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useState } from 'react';
jest.mock('../../../../common/lib/kibana');
const searchResponse = {
id: '0',
rawResponse: {
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
hits: {
total: 2,
max_score: 1,
hits: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['5qcxz8o4j7'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_score: 1,
fields: {
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'user.name': ['hdgsmwj08h'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'host.name': ['Host-4dbzugdlqd'],
},
},
],
},
},
isPartial: false,
isRunning: false,
total: 2,
loaded: 2,
isRestored: false,
};
const searchResponse$ = of<IKibanaSearchResponse>(searchResponse);
const expectedResponse: FetchAlertResp = {
alerts: [],
getInspectQuery: expect.anything(),
refetch: expect.anything(),
isInitializing: true,
totalAlerts: -1,
oldAlertsData: [],
ecsAlertsData: [],
};
describe('useFetchAlerts', () => {
let clock: sinon.SinonFakeTimers;
const onPageChangeMock = jest.fn();
const args: FetchAlertsArgs = {
featureIds: ['siem'],
fields: [
{ field: 'kibana.rule.type.id', include_unmapped: true },
{ field: '*', include_unmapped: true },
],
query: {
ids: { values: ['alert-id-1'] },
},
pagination: {
pageIndex: 0,
pageSize: 10,
},
onPageChange: onPageChangeMock,
sort: [],
skip: false,
};
const dataSearchMock = useKibana().services.data.search.search as jest.Mock;
const showErrorMock = useKibana().services.data.search.showError as jest.Mock;
dataSearchMock.mockReturnValue(searchResponse$);
beforeAll(() => {
clock = sinon.useFakeTimers(new Date('2021-01-01T12:00:00.000Z'));
});
beforeEach(() => {
jest.clearAllMocks();
clock.reset();
});
afterAll(() => clock.restore());
it('returns the response correctly', () => {
const { result } = renderHook(() => useFetchAlerts(args));
expect(result.current).toEqual([
false,
{
...expectedResponse,
alerts: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
'@timestamp': ['2022-03-22T16:48:07.518Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['5qcxz8o4j7'],
},
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
'@timestamp': ['2022-03-22T16:17:50.769Z'],
'host.name': ['Host-4dbzugdlqd'],
'kibana.alert.reason': [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
'kibana.alert.risk_score': [21],
'kibana.alert.rule.name': ['test'],
'kibana.alert.severity': ['low'],
'process.name': ['iexlorer.exe'],
'user.name': ['hdgsmwj08h'],
},
],
totalAlerts: 2,
isInitializing: false,
getInspectQuery: expect.anything(),
refetch: expect.anything(),
ecsAlertsData: [
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:48:07.518Z'],
user: { name: ['5qcxz8o4j7'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_index: '.internal.alerts-security.alerts-default-000001',
},
{
kibana: {
alert: {
severity: ['low'],
risk_score: [21],
rule: { name: ['test'] },
reason: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
},
process: { name: ['iexlorer.exe'] },
'@timestamp': ['2022-03-22T16:17:50.769Z'],
user: { name: ['hdgsmwj08h'] },
host: { name: ['Host-4dbzugdlqd'] },
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
_index: '.internal.alerts-security.alerts-default-000001',
},
],
oldAlertsData: [
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:48:07.518Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['5qcxz8o4j7'] },
{
field: 'kibana.alert.reason',
value: [
'registry event with process iexlorer.exe, by 5qcxz8o4j7 on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
[
{ field: 'kibana.alert.severity', value: ['low'] },
{ field: 'process.name', value: ['iexlorer.exe'] },
{ field: '@timestamp', value: ['2022-03-22T16:17:50.769Z'] },
{ field: 'kibana.alert.risk_score', value: [21] },
{ field: 'kibana.alert.rule.name', value: ['test'] },
{ field: 'user.name', value: ['hdgsmwj08h'] },
{
field: 'kibana.alert.reason',
value: [
'network event with process iexlorer.exe, by hdgsmwj08h on Host-4dbzugdlqd created low alert test.',
],
},
{ field: 'host.name', value: ['Host-4dbzugdlqd'] },
{
field: '_id',
value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
},
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
],
],
},
]);
});
it('call search with correct arguments', () => {
renderHook(() => useFetchAlerts(args));
expect(dataSearchMock).toHaveBeenCalledTimes(1);
expect(dataSearchMock).toHaveBeenCalledWith(
{
featureIds: args.featureIds,
fields: [...args.fields],
pagination: args.pagination,
query: {
ids: {
values: ['alert-id-1'],
},
},
sort: args.sort,
},
{ abortSignal: expect.anything(), strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
});
it('skips the fetch correctly', () => {
const { result } = renderHook(() => useFetchAlerts({ ...args, skip: true }));
expect(dataSearchMock).not.toHaveBeenCalled();
expect(result.current).toEqual([
false,
{
...expectedResponse,
alerts: [],
getInspectQuery: expect.anything(),
refetch: expect.anything(),
isInitializing: true,
totalAlerts: -1,
},
]);
});
it('handles search error', () => {
const obs$ = throwError('simulated search error');
dataSearchMock.mockReturnValue(obs$);
const { result } = renderHook(() => useFetchAlerts(args));
expect(result.current).toEqual([
false,
{
...expectedResponse,
alerts: [],
getInspectQuery: expect.anything(),
refetch: expect.anything(),
isInitializing: true,
totalAlerts: -1,
},
]);
expect(showErrorMock).toHaveBeenCalled();
});
it('returns the correct response if the search response is running', () => {
const obs$ = of<IKibanaSearchResponse>({ ...searchResponse, isRunning: true });
dataSearchMock.mockReturnValue(obs$);
const { result } = renderHook(() => useFetchAlerts(args));
expect(result.current).toEqual([
true,
{
...expectedResponse,
alerts: [],
getInspectQuery: expect.anything(),
refetch: expect.anything(),
isInitializing: true,
totalAlerts: -1,
},
]);
});
it('returns the correct total alerts if the total alerts in the response is an object', () => {
const obs$ = of<IKibanaSearchResponse>({
...searchResponse,
rawResponse: {
...searchResponse.rawResponse,
hits: { ...searchResponse.rawResponse.hits, total: { value: 2 } },
},
});
dataSearchMock.mockReturnValue(obs$);
const { result } = renderHook(() => useFetchAlerts(args));
const [_, alerts] = result.current;
expect(alerts.totalAlerts).toEqual(2);
});
it('does not return an alert without fields', () => {
const obs$ = of<IKibanaSearchResponse>({
...searchResponse,
rawResponse: {
...searchResponse.rawResponse,
hits: {
...searchResponse.rawResponse.hits,
hits: [
{
_index: '.internal.alerts-security.alerts-default-000001',
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
_score: 1,
},
],
},
},
});
dataSearchMock.mockReturnValue(obs$);
const { result } = renderHook(() => useFetchAlerts(args));
const [_, alerts] = result.current;
expect(alerts.alerts).toEqual([]);
});
it('resets pagination on refetch correctly', async () => {
const { result } = renderHook(() =>
useFetchAlerts({
...args,
pagination: {
pageIndex: 5,
pageSize: 10,
},
})
);
const [_, alerts] = result.current;
expect(dataSearchMock).toHaveBeenCalledWith(
{
featureIds: args.featureIds,
fields: [...args.fields],
pagination: {
pageIndex: 5,
pageSize: 10,
},
query: {
ids: {
values: ['alert-id-1'],
},
},
sort: args.sort,
},
{ abortSignal: expect.anything(), strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
await act(async () => {
alerts.refetch();
});
expect(dataSearchMock).toHaveBeenCalledWith(
{
featureIds: args.featureIds,
fields: [...args.fields],
pagination: {
pageIndex: 0,
pageSize: 10,
},
query: {
ids: {
values: ['alert-id-1'],
},
},
sort: args.sort,
},
{ abortSignal: expect.anything(), strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
});
it('does not fetch with no feature ids', () => {
const { result } = renderHook(() => useFetchAlerts({ ...args, featureIds: [] }));
expect(dataSearchMock).not.toHaveBeenCalled();
expect(result.current).toEqual([
false,
{
...expectedResponse,
alerts: [],
getInspectQuery: expect.anything(),
refetch: expect.anything(),
isInitializing: true,
totalAlerts: -1,
},
]);
});
it('reset pagination when query is used', async () => {
const useWrapperHook = ({ query }: { query: Pick<QueryDslQueryContainer, 'bool' | 'ids'> }) => {
const [pagination, setPagination] = useState({ pageIndex: 5, pageSize: 10 });
const handlePagination = (newPagination: { pageIndex: number; pageSize: number }) => {
onPageChangeMock(newPagination);
setPagination(newPagination);
};
const result = useFetchAlerts({
...args,
pagination,
onPageChange: handlePagination,
query,
});
return result;
};
const { rerender } = renderHook(
({ initialValue }) =>
useWrapperHook({
query: initialValue,
}),
{
initialProps: { initialValue: {} },
}
);
expect(dataSearchMock).lastCalledWith(
{
featureIds: args.featureIds,
fields: [...args.fields],
pagination: {
pageIndex: 5,
pageSize: 10,
},
query: {},
sort: args.sort,
},
{ abortSignal: expect.anything(), strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
rerender({
initialValue: {
ids: {
values: ['alert-id-1'],
},
},
});
expect(dataSearchMock).lastCalledWith(
{
featureIds: args.featureIds,
fields: [...args.fields],
pagination: {
pageIndex: 0,
pageSize: 10,
},
query: {
ids: {
values: ['alert-id-1'],
},
},
sort: args.sort,
},
{ abortSignal: expect.anything(), strategy: 'privateRuleRegistryAlertsSearchStrategy' }
);
expect(onPageChangeMock).lastCalledWith({
pageIndex: 0,
pageSize: 10,
});
});
});

View file

@ -1,358 +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 type { ValidFeatureId } from '@kbn/rule-data-utils';
import { set } from '@kbn/safer-lodash-set';
import deepEqual from 'fast-deep-equal';
import { noop } from 'lodash';
import { useCallback, useEffect, useReducer, useRef, useMemo } from 'react';
import { Subscription } from 'rxjs';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type {
RuleRegistrySearchRequest,
RuleRegistrySearchRequestPagination,
RuleRegistrySearchResponse,
} from '@kbn/rule-registry-plugin/common/search_strategy';
import type {
MappingRuntimeFields,
QueryDslFieldAndFormat,
QueryDslQueryContainer,
SortCombinations,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { Alert, Alerts, GetInspectQuery, InspectQuery } from '../../../../types';
import { useKibana } from '../../../../common/lib/kibana';
import { DefaultSort } from './constants';
export interface FetchAlertsArgs {
featureIds: ValidFeatureId[];
fields: QueryDslFieldAndFormat[];
query: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
pagination: {
pageIndex: number;
pageSize: number;
};
onLoaded?: (alerts: Alerts) => void;
onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void;
runtimeMappings?: MappingRuntimeFields;
sort: SortCombinations[];
skip: boolean;
}
type AlertRequest = Omit<FetchAlertsArgs, 'featureIds' | 'skip' | 'onPageChange'>;
type Refetch = () => void;
export interface FetchAlertResp {
/**
* We need to have it because of lot code is expecting this format
* @deprecated
*/
oldAlertsData: Array<Array<{ field: string; value: string[] }>>;
/**
* We need to have it because of lot code is expecting this format
* @deprecated
*/
ecsAlertsData: unknown[];
alerts: Alerts;
isInitializing: boolean;
getInspectQuery: GetInspectQuery;
refetch: Refetch;
totalAlerts: number;
}
type AlertResponseState = Omit<FetchAlertResp, 'getInspectQuery' | 'refetch'>;
interface AlertStateReducer {
loading: boolean;
request: Omit<FetchAlertsArgs, 'skip' | 'onPageChange'>;
response: AlertResponseState;
}
type AlertActions =
| { type: 'loading'; loading: boolean }
| {
type: 'response';
alerts: Alerts;
totalAlerts: number;
oldAlertsData: Array<Array<{ field: string; value: string[] }>>;
ecsAlertsData: unknown[];
}
| { type: 'resetPagination' }
| { type: 'request'; request: Omit<FetchAlertsArgs, 'skip' | 'onPageChange'> };
const initialAlertState: AlertStateReducer = {
loading: false,
request: {
featureIds: [],
fields: [],
query: {
bool: {},
},
pagination: {
pageIndex: 0,
pageSize: 50,
},
sort: DefaultSort,
},
response: {
alerts: [],
oldAlertsData: [],
ecsAlertsData: [],
totalAlerts: -1,
isInitializing: true,
},
};
function alertReducer(state: AlertStateReducer, action: AlertActions) {
switch (action.type) {
case 'loading':
return { ...state, loading: action.loading };
case 'response':
return {
...state,
loading: false,
response: {
isInitializing: false,
alerts: action.alerts,
totalAlerts: action.totalAlerts,
oldAlertsData: action.oldAlertsData,
ecsAlertsData: action.ecsAlertsData,
},
};
case 'resetPagination':
return {
...state,
request: {
...state.request,
pagination: {
...state.request.pagination,
pageIndex: 0,
},
},
};
case 'request':
return { ...state, request: action.request };
default:
throw new Error();
}
}
export type UseFetchAlerts = ({
featureIds,
fields,
query,
pagination,
onLoaded,
onPageChange,
runtimeMappings,
skip,
sort,
}: FetchAlertsArgs) => [boolean, FetchAlertResp];
const useFetchAlerts = ({
featureIds,
fields,
query,
pagination,
onLoaded,
onPageChange,
runtimeMappings,
skip,
sort,
}: FetchAlertsArgs): [boolean, FetchAlertResp] => {
const refetch = useRef<Refetch>(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const [{ loading, request: alertRequest, response: alertResponse }, dispatch] = useReducer(
alertReducer,
initialAlertState
);
const prevAlertRequest = useRef<AlertRequest | null>(null);
const inspectQuery = useRef<InspectQuery>({
request: [],
response: [],
});
const { data } = useKibana().services;
const getInspectQuery = useCallback(() => inspectQuery.current, []);
const refetchGrid = useCallback(() => {
if ((prevAlertRequest.current?.pagination?.pageIndex ?? 0) !== 0) {
dispatch({ type: 'resetPagination' });
} else {
refetch.current();
}
}, []);
const fetchAlerts = useCallback(
(request: AlertRequest | null) => {
if (request == null || skip) {
return;
}
const asyncSearch = async () => {
prevAlertRequest.current = request;
abortCtrl.current = new AbortController();
dispatch({ type: 'loading', loading: true });
if (data && data.search) {
searchSubscription$.current = data.search
.search<RuleRegistrySearchRequest, RuleRegistrySearchResponse>(
{ ...request, featureIds, fields, query },
{
strategy: 'privateRuleRegistryAlertsSearchStrategy',
abortSignal: abortCtrl.current.signal,
}
)
.subscribe({
next: (response: RuleRegistrySearchResponse) => {
if (!isRunningResponse(response)) {
const { rawResponse } = response;
inspectQuery.current = {
request: response?.inspect?.dsl ?? [],
response: [JSON.stringify(rawResponse)] ?? [],
};
let totalAlerts = 0;
if (rawResponse.hits.total && typeof rawResponse.hits.total === 'number') {
totalAlerts = rawResponse.hits.total;
} else if (rawResponse.hits.total && typeof rawResponse.hits.total === 'object') {
totalAlerts = rawResponse.hits.total?.value ?? 0;
}
const alerts = rawResponse.hits.hits.reduce<Alerts>((acc, hit) => {
if (hit.fields) {
acc.push({
...hit.fields,
_id: hit._id,
_index: hit._index,
} as Alert);
}
return acc;
}, []);
const { oldAlertsData, ecsAlertsData } = alerts.reduce<{
oldAlertsData: Array<Array<{ field: string; value: string[] }>>;
ecsAlertsData: unknown[];
}>(
(acc, alert) => {
const itemOldData = Object.entries(alert).reduce<
Array<{ field: string; value: string[] }>
>((oldData, [key, value]) => {
oldData.push({ field: key, value: value as string[] });
return oldData;
}, []);
const ecsData = Object.entries(alert).reduce((ecs, [key, value]) => {
set(ecs, key, value ?? []);
return ecs;
}, {});
acc.oldAlertsData.push(itemOldData);
acc.ecsAlertsData.push(ecsData);
return acc;
},
{ oldAlertsData: [], ecsAlertsData: [] }
);
dispatch({
type: 'response',
alerts,
oldAlertsData,
ecsAlertsData,
totalAlerts,
});
dispatch({ type: 'loading', loading: false });
onLoaded?.(alerts);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
dispatch({ type: 'loading', loading: false });
onLoaded?.([]);
data.search.showError(msg);
searchSubscription$.current.unsubscribe();
},
});
}
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[skip, data, featureIds, query, fields, onLoaded]
);
// FUTURE ENGINEER
// This useEffect is only to fetch the alert when these props below changed
// fields, pagination, sort, runtimeMappings
useEffect(() => {
if (featureIds.length === 0) {
return;
}
const newAlertRequest = {
featureIds,
fields,
pagination,
query: prevAlertRequest.current?.query ?? {},
runtimeMappings,
sort,
};
if (
newAlertRequest.fields.length > 0 &&
!deepEqual(newAlertRequest, prevAlertRequest.current)
) {
dispatch({
type: 'request',
request: newAlertRequest,
});
}
}, [featureIds, fields, pagination, sort, runtimeMappings]);
// FUTURE ENGINEER
// This useEffect is only to fetch the alert when query props changed
// because we want to reset the pageIndex of pagination to 0
useEffect(() => {
if (featureIds.length === 0 || !prevAlertRequest.current) {
return;
}
const resetPagination = {
pageIndex: 0,
pageSize: prevAlertRequest.current?.pagination?.pageSize ?? 50,
};
const newAlertRequest = {
...prevAlertRequest.current,
featureIds,
pagination: resetPagination,
query,
};
if (
(newAlertRequest?.fields ?? []).length > 0 &&
!deepEqual(newAlertRequest.query, prevAlertRequest.current.query)
) {
dispatch({
type: 'request',
request: newAlertRequest,
});
onPageChange(resetPagination);
}
}, [featureIds, onPageChange, query]);
useEffect(() => {
if (alertRequest.featureIds.length > 0 && !deepEqual(alertRequest, prevAlertRequest.current)) {
fetchAlerts(alertRequest);
}
}, [alertRequest, fetchAlerts]);
const alertResponseMemo = useMemo(
() => ({
...alertResponse,
getInspectQuery,
refetch: refetchGrid,
}),
[alertResponse, getInspectQuery, refetchGrid]
);
return [loading, alertResponseMemo];
};
export { useFetchAlerts };

View file

@ -9,7 +9,6 @@ import { isValidFeatureId, ValidFeatureId } from '@kbn/rule-data-utils';
import { BASE_RAC_ALERTS_API_PATH, BrowserFields } from '@kbn/rule-registry-plugin/common';
import { useCallback, useEffect, useState } from 'react';
import type { FieldDescriptor } from '@kbn/data-views-plugin/server';
import type { Alerts } from '../../../../types';
import { useKibana } from '../../../../common/lib/kibana';
import { ERROR_FETCH_BROWSER_FIELDS } from './translations';
@ -18,12 +17,6 @@ export interface FetchAlertsArgs {
initialBrowserFields?: BrowserFields;
}
export interface FetchAlertResp {
alerts: Alerts;
}
export type UseFetchAlerts = ({ featureIds }: FetchAlertsArgs) => [boolean, FetchAlertResp];
const INVALID_FEATURE_ID = 'siem';
export const useFetchBrowserFieldCapabilities = ({

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useContext, useEffect, useState } from 'react';
import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import type { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import { AlertsTableContext } from '../contexts/alerts_table_context';
import { BulkActionsVerbs } from '../../../../types';

View file

@ -15,11 +15,9 @@ jest.mock('./modal', () => ({
}));
describe('Inspect Button', () => {
const getInspectQuery = () => {
return {
request: [''],
response: [''],
};
const querySnapshot = {
request: [''],
response: [''],
};
afterEach(() => {
@ -31,7 +29,7 @@ describe('Inspect Button', () => {
<InspectButton
inspectTitle={'Inspect Title'}
showInspectButton
getInspectQuery={getInspectQuery}
querySnapshot={querySnapshot}
/>
);
fireEvent.click(await screen.findByTestId('inspect-icon-button'));

View file

@ -7,8 +7,8 @@
import { EuiButtonIcon } from '@elastic/eui';
import React, { useState, memo, useCallback } from 'react';
import { GetInspectQuery } from '../../../../../../types';
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
import { HoverVisibilityContainer } from './hover_visibility_container';
import { ModalInspectQuery } from './modal';
@ -33,14 +33,11 @@ export const InspectButtonContainer: React.FC<InspectButtonContainerProps> = mem
interface InspectButtonProps {
onCloseInspect?: () => void;
showInspectButton?: boolean;
getInspectQuery: GetInspectQuery;
querySnapshot: EsQuerySnapshot;
inspectTitle: string;
}
const InspectButtonComponent: React.FC<InspectButtonProps> = ({
getInspectQuery,
inspectTitle,
}) => {
const InspectButtonComponent: React.FC<InspectButtonProps> = ({ querySnapshot, inspectTitle }) => {
const [isShowingModal, setIsShowingModal] = useState(false);
const onOpenModal = useCallback(() => {
@ -66,7 +63,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
<ModalInspectQuery
closeModal={onCloseModal}
data-test-subj="inspect-modal"
getInspectQuery={getInspectQuery}
querySnapshot={querySnapshot}
title={inspectTitle}
/>
)}

View file

@ -36,10 +36,10 @@ describe('Modal Inspect', () => {
const defaultProps: ModalInspectProps = {
closeModal,
title: 'Inspect',
getInspectQuery: () => ({
querySnapshot: {
request: [getRequest()],
response: [response],
}),
},
};
const renderModalInspectQuery = () => {

View file

@ -24,12 +24,12 @@ import React from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { isEmpty } from 'lodash';
import { GetInspectQuery } from '../../../../../../types';
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
import * as i18n from './translations';
export interface ModalInspectProps {
closeModal: () => void;
getInspectQuery: GetInspectQuery;
querySnapshot: EsQuerySnapshot;
title: string;
}
@ -77,8 +77,8 @@ const stringify = (object: Request | Response): string => {
}
};
const ModalInspectQueryComponent = ({ closeModal, getInspectQuery, title }: ModalInspectProps) => {
const { request, response } = getInspectQuery();
const ModalInspectQueryComponent = ({ closeModal, querySnapshot, title }: ModalInspectProps) => {
const { request, response } = querySnapshot;
// using index 0 as there will be only one request and response for now
const parsedRequest: Request = parse(request[0]);
const parsedResponse: Response = parse(response[0]);

View file

@ -11,14 +11,10 @@ import {
} from '@elastic/eui';
import React, { lazy, Suspense, memo, useMemo, useContext } from 'react';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
import { AlertsCount } from './components/alerts_count/alerts_count';
import { AlertsTableContext } from '../contexts/alerts_table_context';
import type {
Alerts,
BulkActionsPanelConfig,
GetInspectQuery,
RowSelection,
} from '../../../../types';
import type { Alerts, BulkActionsPanelConfig, RowSelection } from '../../../../types';
import { LastUpdatedAt } from './components/last_updated_at';
import { FieldBrowser } from '../../field_browser';
import { FieldBrowserOptions } from '../../field_browser/types';
@ -30,11 +26,11 @@ const BulkActionsToolbar = lazy(() => import('../bulk_actions/components/toolbar
const RightControl = memo(
({
controls,
getInspectQuery,
querySnapshot,
showInspectButton,
}: {
controls?: EuiDataGridToolBarAdditionalControlsOptions;
getInspectQuery: GetInspectQuery;
querySnapshot: EsQuerySnapshot;
showInspectButton: boolean;
}) => {
const {
@ -43,7 +39,7 @@ const RightControl = memo(
return (
<>
{showInspectButton && (
<InspectButton inspectTitle={ALERTS_TABLE_TITLE} getInspectQuery={getInspectQuery} />
<InspectButton inspectTitle={ALERTS_TABLE_TITLE} querySnapshot={querySnapshot} />
)}
<LastUpdatedAt updatedAt={bulkActionsState.updatedAt} />
{controls?.right}
@ -96,7 +92,7 @@ const useGetDefaultVisibility = ({
browserFields,
controls,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
toolbarVisibilityProp,
}: {
@ -107,7 +103,7 @@ const useGetDefaultVisibility = ({
browserFields: BrowserFields;
controls?: EuiDataGridToolBarAdditionalControlsOptions;
fieldBrowserOptions?: FieldBrowserOptions;
getInspectQuery: GetInspectQuery;
querySnapshot?: EsQuerySnapshot;
showInspectButton: boolean;
toolbarVisibilityProp?: EuiDataGridToolBarVisibilityOptions;
}): EuiDataGridToolBarVisibilityOptions => {
@ -115,10 +111,10 @@ const useGetDefaultVisibility = ({
const hasBrowserFields = Object.keys(browserFields).length > 0;
return {
additionalControls: {
right: (
right: querySnapshot && (
<RightControl
controls={controls}
getInspectQuery={getInspectQuery}
querySnapshot={querySnapshot}
showInspectButton={showInspectButton}
/>
),
@ -146,7 +142,7 @@ const useGetDefaultVisibility = ({
browserFields,
columnIds,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
onResetColumns,
onToggleColumn,
showInspectButton,
@ -170,7 +166,7 @@ export const useGetToolbarVisibility = ({
controls,
refresh,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
toolbarVisibilityProp,
}: {
@ -188,7 +184,7 @@ export const useGetToolbarVisibility = ({
controls?: EuiDataGridToolBarAdditionalControlsOptions;
refresh: () => void;
fieldBrowserOptions?: FieldBrowserOptions;
getInspectQuery: GetInspectQuery;
querySnapshot?: EsQuerySnapshot;
showInspectButton: boolean;
toolbarVisibilityProp?: EuiDataGridToolBarVisibilityOptions;
}): EuiDataGridToolBarVisibilityOptions => {
@ -202,7 +198,7 @@ export const useGetToolbarVisibility = ({
browserFields,
controls,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
};
}, [
@ -213,7 +209,7 @@ export const useGetToolbarVisibility = ({
browserFields,
controls,
fieldBrowserOptions,
getInspectQuery,
querySnapshot,
showInspectButton,
]);
const defaultVisibility = useGetDefaultVisibility(defaultVisibilityProps);
@ -231,10 +227,10 @@ export const useGetToolbarVisibility = ({
showColumnSelector: false,
showSortSelector: false,
additionalControls: {
right: (
right: querySnapshot && (
<RightControl
controls={controls}
getInspectQuery={getInspectQuery}
querySnapshot={querySnapshot}
showInspectButton={showInspectButton}
/>
),
@ -270,7 +266,7 @@ export const useGetToolbarVisibility = ({
refresh,
setIsBulkActionsLoading,
controls,
getInspectQuery,
querySnapshot,
showInspectButton,
]);

View file

@ -19,8 +19,9 @@ export type KibanaContext = KibanaReactContextValue<TriggersAndActionsUiServices
export interface WithKibanaProps {
kibana: KibanaContext;
}
const useTypedKibana = () => useKibana<TriggersAndActionsUiServices>();
const useTypedKibana = () => {
return useKibana<TriggersAndActionsUiServices>();
};
export {
KibanaContextProvider,

View file

@ -13,7 +13,6 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import type {
EuiDataGridCellValueElementProps,
EuiDataGridToolBarAdditionalControlsOptions,
@ -57,8 +56,10 @@ import {
SanitizedRuleAction as RuleAction,
} from '@kbn/alerting-plugin/common';
import type { BulkOperationError } from '@kbn/alerting-plugin/server';
import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import type {
RuleRegistrySearchRequestPagination,
EcsFieldsResponse,
} from '@kbn/rule-registry-plugin/common';
import {
QueryDslQueryContainer,
SortCombinations,
@ -71,8 +72,10 @@ import {
UserConfiguredActionConnector,
ActionConnector,
ActionTypeRegistryContract,
EsQuerySnapshot,
} from '@kbn/alerts-ui-shared/src/common/types';
import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown';
import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter';
import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter';
@ -487,19 +490,20 @@ export type AlertsTableProps = {
*/
dynamicRowHeight?: boolean;
featureIds?: ValidFeatureId[];
pagination: RuleRegistrySearchRequestPagination;
pageIndex: number;
pageSize: number;
sort: SortCombinations[];
isLoading: boolean;
alerts: Alerts;
oldAlertsData: FetchAlertData['oldAlertsData'];
ecsAlertsData: FetchAlertData['ecsAlertsData'];
getInspectQuery: GetInspectQuery;
refetch: () => void;
querySnapshot?: EsQuerySnapshot;
refetchAlerts: () => void;
alertsCount: number;
onSortChange: (sort: EuiDataGridSorting['columns']) => void;
onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void;
renderCellPopover?: ReturnType<GetRenderCellPopover>;
fieldFormats: FieldFormatsRegistry;
fieldFormats: FieldFormatsStart;
} & Partial<Pick<EuiDataGridProps, 'gridStyle' | 'rowHeightsOptions'>>;
export type SetFlyoutAlert = (alertId: string) => void;

View file

@ -66,7 +66,6 @@
"@kbn/react-kibana-mount",
"@kbn/react-kibana-context-theme",
"@kbn/controls-plugin",
"@kbn/search-types",
"@kbn/alerting-comparators",
"@kbn/alerting-types",
"@kbn/visualization-utils",

View file

@ -7,8 +7,8 @@
import expect from '@kbn/expect';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RuleRegistrySearchResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import type { RuleRegistrySearchResponse } from '@kbn/rule-registry-plugin/common';
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
import {
obsOnlySpacesAll,
logsOnlySpacesAll,

View file

@ -74,6 +74,7 @@ describe('Alerts cell actions', { tags: ['@ess', '@serverless'] }, () => {
cy.log('filter out alert property');
scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER);
filterOutAlertProperty(ALERT_TABLE_FILE_NAME_VALUES, 0);
cy.get(FILTER_BADGE).first().should('have.text', 'file.name: exists');

View file

@ -273,8 +273,7 @@ export const ALERT_TABLE_FILE_NAME_HEADER = '[data-gridcell-column-id="file.name
export const ALERT_TABLE_SEVERITY_HEADER = '[data-gridcell-column-id="kibana.alert.severity"]';
export const ALERT_TABLE_FILE_NAME_VALUES =
'[data-gridcell-column-id="file.name"][data-test-subj="dataGridRowCell"]'; // empty column for the test data
export const ALERT_TABLE_FILE_NAME_VALUES = `${ALERT_TABLE_FILE_NAME_HEADER}[data-test-subj="dataGridRowCell"]`; // empty column for the test data
export const ACTIVE_TIMELINE_BOTTOM_BAR = '[data-test-subj="timeline-bottom-bar-title-button"]';

View file

@ -489,7 +489,7 @@ export const updateAlertTags = () => {
};
export const showHoverActionsEventRenderedView = (fieldSelector: string) => {
cy.get(fieldSelector).first().trigger('mouseover');
cy.get(fieldSelector).first().realHover();
cy.get(HOVER_ACTIONS_CONTAINER).should('be.visible');
};

View file

@ -150,8 +150,8 @@ import { EUI_FILTER_SELECT_ITEM, COMBO_BOX_INPUT } from '../screens/common/contr
import { ruleFields } from '../data/detection_engine';
import { waitForAlerts } from './alerts';
import { refreshPage } from './security_header';
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
import { COMBO_BOX_OPTION, TOOLTIP } from '../screens/common';
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
export const createAndEnableRule = () => {
cy.get(CREATE_AND_ENABLE_BTN).click();
@ -864,6 +864,7 @@ export const waitForAlertsToPopulate = (alertCountThreshold = 1) => {
() => {
cy.log('Waiting for alerts to appear');
refreshPage();
cy.get([EMPTY_ALERT_TABLE, ALERTS_TABLE_COUNT].join(', '));
return cy.root().then(($el) => {
const emptyTableState = $el.find(EMPTY_ALERT_TABLE);
if (emptyTableState.length > 0) {