mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
76237d8cf2
commit
bd3032b5fa
68 changed files with 1382 additions and 1232 deletions
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
"@kbn/rule-data-utils",
|
||||
"@kbn/rrule",
|
||||
"@kbn/core",
|
||||
"@kbn/es-query"
|
||||
"@kbn/es-query",
|
||||
"@kbn/search-types"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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: [] }
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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: [],
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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[];
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -131,7 +131,7 @@ export const AlertsOverview = ({
|
|||
featureIds={alertFeatureIds}
|
||||
showAlertStatusWithFlapping
|
||||
query={alertsEsQueryByStatus}
|
||||
pageSize={5}
|
||||
initialPageSize={5}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -248,7 +248,7 @@ function InternalAlertsPage() {
|
|||
featureIds={observabilityAlertFeatureIds}
|
||||
query={esQuery}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={ALERTS_PER_PAGE}
|
||||
initialPageSize={ALERTS_PER_PAGE}
|
||||
cellContext={{ observabilityRuleTypeRegistry }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export function SloDetailsAlerts({ slo }: Props) {
|
|||
},
|
||||
}}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={100}
|
||||
initialPageSize={100}
|
||||
cellContext={{ observabilityRuleTypeRegistry }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -201,7 +201,7 @@ const PageContent = () => {
|
|||
featureIds={featureIds}
|
||||
query={esQuery}
|
||||
showAlertStatusWithFlapping
|
||||
pageSize={20}
|
||||
initialPageSize={20}
|
||||
/>
|
||||
</Suspense>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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} />);
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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 = ({
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -36,10 +36,10 @@ describe('Modal Inspect', () => {
|
|||
const defaultProps: ModalInspectProps = {
|
||||
closeModal,
|
||||
title: 'Inspect',
|
||||
getInspectQuery: () => ({
|
||||
querySnapshot: {
|
||||
request: [getRequest()],
|
||||
response: [response],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const renderModalInspectQuery = () => {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"]';
|
||||
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue