From bd3032b5fa45d26e37636c7c12cecdc04c300beb Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Thu, 25 Jul 2024 15:36:56 +0200 Subject: [PATCH] [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> --- packages/kbn-alerting-types/index.ts | 13 +- .../search_strategy_types.ts | 23 +- packages/kbn-alerting-types/tsconfig.json | 3 +- .../apis/search_alerts/search_alerts.test.tsx | 333 ++++++++++++ .../apis/search_alerts/search_alerts.ts | 195 +++++++ .../src/common/constants/routes.ts | 1 + .../common/contexts/alerts_query_context.ts | 12 + .../src/common/hooks/index.ts | 1 + .../hooks/use_search_alerts_query.test.tsx | 284 ++++++++++ .../common/hooks/use_search_alerts_query.ts | 71 +++ .../src/common/types/alerts_types.ts | 16 + .../src/common/types/index.ts | 3 +- packages/kbn-alerts-ui-shared/tsconfig.json | 1 + .../shared/alerts/alerts_overview.tsx | 2 +- .../tabs/alerts/alerts_tab_content.tsx | 2 +- .../lib/get_data.ts | 19 +- .../alerting/metric_threshold/lib/get_data.ts | 2 +- .../alerts_flyout/alerts_flyout.tsx | 2 +- .../public/hooks/use_fetch_alert_detail.ts | 2 +- .../public/pages/alerts/alerts.tsx | 2 +- .../alerts/components/alert_actions.test.tsx | 4 +- .../public/pages/overview/overview.tsx | 2 +- .../public/rules/fixtures/example_alerts.ts | 2 +- .../rules/custom_threshold/lib/get_data.ts | 8 +- .../alerts/components/slo_alerts_table.tsx | 2 +- .../components/slo_detail_alerts.tsx | 2 +- x-pack/plugins/rule_registry/common/index.ts | 3 +- .../search_strategy/search_strategy.test.ts | 2 +- .../server/search_strategy/search_strategy.ts | 5 +- x-pack/plugins/rule_registry/tsconfig.json | 2 +- .../components/stack_alerts_page.tsx | 2 +- .../alerts_table/alerts_table.test.tsx | 28 +- .../sections/alerts_table/alerts_table.tsx | 70 +-- .../alerts_table/alerts_table_state.test.tsx | 201 ++++--- .../alerts_table/alerts_table_state.tsx | 192 ++++--- .../bulk_actions/bulk_actions.test.tsx | 38 +- .../contexts/alerts_table_context.ts | 3 - .../sections/alerts_table/empty_state.tsx | 12 +- .../alert_mute/use_get_muted_alerts.test.tsx | 4 +- .../hooks/alert_mute/use_get_muted_alerts.tsx | 4 +- .../hooks/alert_mute/use_mute_alert.test.tsx | 4 +- .../hooks/alert_mute/use_mute_alert.ts | 4 +- .../alert_mute/use_unmute_alert.test.tsx | 4 +- .../hooks/alert_mute/use_unmute_alert.ts | 4 +- .../sections/alerts_table/hooks/index.ts | 2 - .../hooks/use_bulk_actions.test.tsx | 16 +- .../alerts_table/hooks/use_bulk_actions.ts | 10 +- .../hooks/use_bulk_get_cases.test.tsx | 4 +- .../alerts_table/hooks/use_bulk_get_cases.tsx | 4 +- .../hooks/use_bulk_untrack_alerts.tsx | 4 +- .../use_bulk_untrack_alerts_by_query.tsx | 4 +- .../hooks/use_fetch_alerts.test.tsx | 504 ------------------ .../alerts_table/hooks/use_fetch_alerts.tsx | 358 ------------- .../use_fetch_browser_fields_capabilities.tsx | 7 - .../alerts_table/hooks/use_pagination.ts | 2 +- .../toolbar/components/inspect/index.test.tsx | 10 +- .../toolbar/components/inspect/index.tsx | 11 +- .../toolbar/components/inspect/modal.test.tsx | 4 +- .../toolbar/components/inspect/modal.tsx | 8 +- .../toolbar/toolbar_visibility.tsx | 38 +- .../public/common/lib/kibana/kibana_react.ts | 5 +- .../triggers_actions_ui/public/types.ts | 18 +- .../plugins/triggers_actions_ui/tsconfig.json | 1 - .../tests/basic/search_strategy.ts | 4 +- .../alerts/alerts_cell_actions.cy.ts | 1 + .../cypress/screens/timeline.ts | 3 +- .../cypress/tasks/alerts.ts | 2 +- .../cypress/tasks/create_new_rule.ts | 3 +- 68 files changed, 1381 insertions(+), 1231 deletions(-) rename x-pack/plugins/rule_registry/common/search_strategy/index.ts => packages/kbn-alerting-types/search_strategy_types.ts (65%) create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/contexts/alerts_query_context.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts create mode 100644 packages/kbn-alerts-ui-shared/src/common/types/alerts_types.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts index c54289d2ecc65..019cf2577738a 100644 --- a/packages/kbn-alerting-types/index.ts +++ b/packages/kbn-alerting-types/index.ts @@ -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 './action_variable'; 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 './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'; diff --git a/x-pack/plugins/rule_registry/common/search_strategy/index.ts b/packages/kbn-alerting-types/search_strategy_types.ts similarity index 65% rename from x-pack/plugins/rule_registry/common/search_strategy/index.ts rename to packages/kbn-alerting-types/search_strategy_types.ts index 4070b00014e7a..07025fd4f435f 100644 --- a/x-pack/plugins/rule_registry/common/search_strategy/index.ts +++ b/packages/kbn-alerting-types/search_strategy_types.ts @@ -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 { +export interface RuleRegistrySearchResponse extends IEsSearchResponse { inspect?: RuleRegistryInspect; } diff --git a/packages/kbn-alerting-types/tsconfig.json b/packages/kbn-alerting-types/tsconfig.json index 195502cd5a729..6c7d143c31145 100644 --- a/packages/kbn-alerting-types/tsconfig.json +++ b/packages/kbn-alerting-types/tsconfig.json @@ -21,6 +21,7 @@ "@kbn/rule-data-utils", "@kbn/rrule", "@kbn/core", - "@kbn/es-query" + "@kbn/es-query", + "@kbn/search-types" ] } diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.test.tsx b/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.test.tsx new file mode 100644 index 0000000000000..9c3a20811c537 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.test.tsx @@ -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(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(); + 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({ + ...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({ + ...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([]); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts b/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts new file mode 100644 index 0000000000000..ae678b9d25bfa --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/apis/search_alerts/search_alerts.ts @@ -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; + /** + * 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 => + lastValueFrom( + data.search + .search( + { + 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((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>( + (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: [] } + ); diff --git a/packages/kbn-alerts-ui-shared/src/common/constants/routes.ts b/packages/kbn-alerts-ui-shared/src/common/constants/routes.ts index c741192249f9e..3bd42bbfd3f81 100644 --- a/packages/kbn-alerts-ui-shared/src/common/constants/routes.ts +++ b/packages/kbn-alerts-ui-shared/src/common/constants/routes.ts @@ -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'; diff --git a/packages/kbn-alerts-ui-shared/src/common/contexts/alerts_query_context.ts b/packages/kbn-alerts-ui-shared/src/common/contexts/alerts_query_context.ts new file mode 100644 index 0000000000000..c8f24146799ff --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/contexts/alerts_query_context.ts @@ -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(undefined); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts index 614b14515f511..ac7504eac2f82 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts @@ -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'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx new file mode 100644 index 0000000000000..30624b22772cb --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.test.tsx @@ -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(searchResponse); + +const expectedResponse: ReturnType['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 }) => ( + + {children} + + ); + + 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, + }) + ); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts new file mode 100644 index 0000000000000..562da6b45a0bb --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_search_alerts_query.ts @@ -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, + '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: [], + }, + }); +}; diff --git a/packages/kbn-alerts-ui-shared/src/common/types/alerts_types.ts b/packages/kbn-alerts-ui-shared/src/common/types/alerts_types.ts new file mode 100644 index 0000000000000..8d358cf35b428 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/types/alerts_types.ts @@ -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[]; +} diff --git a/packages/kbn-alerts-ui-shared/src/common/types/index.ts b/packages/kbn-alerts-ui-shared/src/common/types/index.ts index 08756c70290b1..0b2460e08fea4 100644 --- a/packages/kbn-alerts-ui-shared/src/common/types/index.ts +++ b/packages/kbn-alerts-ui-shared/src/common/types/index.ts @@ -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'; diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 37cc6b12bcea8..653f26692bab1 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -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", diff --git a/x-pack/plugins/observability_solution/infra/public/components/shared/alerts/alerts_overview.tsx b/x-pack/plugins/observability_solution/infra/public/components/shared/alerts/alerts_overview.tsx index 7002b81e2c4b7..2cdc0ee81d7fe 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/shared/alerts/alerts_overview.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/shared/alerts/alerts_overview.tsx @@ -131,7 +131,7 @@ export const AlertsOverview = ({ featureIds={alertFeatureIds} showAlertStatusWithFlapping query={alertsEsQueryByStatus} - pageSize={5} + initialPageSize={5} /> diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/alerts/alerts_tab_content.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/alerts/alerts_tab_content.tsx index ae6b1649a6eab..6350cab5e3ff6 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/alerts/alerts_tab_content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/hosts/components/tabs/alerts/alerts_tab_content.tsx @@ -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 /> diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts index 8bcdd53e3bb5a..207f2fcb7cb27 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts @@ -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, diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts index 7e97c59549624..d2afb40cecf50 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout.tsx index 8035398960881..78017cb0b7f18 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout.tsx @@ -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'; diff --git a/x-pack/plugins/observability_solution/observability/public/hooks/use_fetch_alert_detail.ts b/x-pack/plugins/observability_solution/observability/public/hooks/use_fetch_alert_detail.ts index d159973be700c..a834778c32425 100644 --- a/x-pack/plugins/observability_solution/observability/public/hooks/use_fetch_alert_detail.ts +++ b/x-pack/plugins/observability_solution/observability/public/hooks/use_fetch_alert_detail.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx index def57b3b4bd1b..0d3933b6204f4 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx @@ -248,7 +248,7 @@ function InternalAlertsPage() { featureIds={observabilityAlertFeatureIds} query={esQuery} showAlertStatusWithFlapping - pageSize={ALERTS_PER_PAGE} + initialPageSize={ALERTS_PER_PAGE} cellContext={{ observabilityRuleTypeRegistry }} /> )} diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx index e505a45fe7f61..32a9a70e76ab1 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx @@ -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( - + diff --git a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx index d4cfba510fc46..37942cdbca7d6 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/overview/overview.tsx @@ -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 }} diff --git a/x-pack/plugins/observability_solution/observability/public/rules/fixtures/example_alerts.ts b/x-pack/plugins/observability_solution/observability/public/rules/fixtures/example_alerts.ts index 4aea0892c13c3..899e0ca048766 100644 --- a/x-pack/plugins/observability_solution/observability/public/rules/fixtures/example_alerts.ts +++ b/x-pack/plugins/observability_solution/observability/public/rules/fixtures/example_alerts.ts @@ -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 = [ { diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/get_data.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/get_data.ts index c3c0d8b2eee94..a102a9d23152e 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/lib/get_data.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx index 157c636ff131e..02e94973e4ffd 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx @@ -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) { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx index ed4f5dd89fb8f..7e002d1cac7d1 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_detail_alerts.tsx @@ -42,7 +42,7 @@ export function SloDetailsAlerts({ slo }: Props) { }, }} showAlertStatusWithFlapping - pageSize={100} + initialPageSize={100} cellContext={{ observabilityRuleTypeRegistry }} /> diff --git a/x-pack/plugins/rule_registry/common/index.ts b/x-pack/plugins/rule_registry/common/index.ts index 6c12d82cb95eb..4e749581ee351 100644 --- a/x-pack/plugins/rule_registry/common/index.ts +++ b/x-pack/plugins/rule_registry/common/index.ts @@ -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'; diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts index b4a3a6aad4640..4dabfb3feb390 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts @@ -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'; diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index 49e7d706b3117..a497008086c88 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -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'; diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index b1f4d230cf0a0..d9d2e5a18d925 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -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": [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/components/stack_alerts_page.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/components/stack_alerts_page.tsx index 1c4c511b2ccca..26f4c3fd43bf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/components/stack_alerts_page.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_page/components/stack_alerts_page.tsx @@ -201,7 +201,7 @@ const PageContent = () => { featureIds={featureIds} query={esQuery} showAlertStatusWithFlapping - pageSize={20} + initialPageSize={20} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx index 9d29e452bf9a3..21f220f0fc49e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx @@ -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( - + ); 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: () =>

Test cell

, }, ], - pagination: { pageIndex: 0, pageSize: 1 }, + pageIndex: 0, + pageSize: 1, }; const wrapper = render(); 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(); + render(); expect(await screen.findByText('Test case')).toBeInTheDocument(); expect(await screen.findByText('Test case 2')).toBeInTheDocument(); }); @@ -721,7 +726,8 @@ describe('AlertsTable', () => { render( ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index c707b8dba875f..76bc9b1ff7f4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -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) => ( = memo((props: Aler leadingControlColumns: passedControlColumns, trailingControlColumns, alertsTableConfiguration, - pagination, + pageIndex, + pageSize, columns, alerts, alertsCount, @@ -302,11 +304,11 @@ const AlertsTable: React.FunctionComponent = 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 = memo((props: Aler NonNullable >({}); - 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 = 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 = 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 = 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 = memo((props: Aler clearSelection, refresh, fieldBrowserOptions, - getInspectQuery, + querySnapshot, showInspectButton, toolbarVisibilityProp, }; @@ -428,7 +438,7 @@ const AlertsTable: React.FunctionComponent = memo((props: Aler clearSelection, refresh, fieldBrowserOptions, - getInspectQuery, + querySnapshot, showInspectButton, toolbarVisibilityProp, alerts, @@ -467,7 +477,7 @@ const AlertsTable: React.FunctionComponent = 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 = 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 = 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 = 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 = memo((props: Aler return alerts.reduce>( (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 = memo((props: Aler } else { return stableMappedRowClasses; } - }, [shouldHighlightRow, alerts, pagination.pageIndex, pagination.pageSize]); + }, [shouldHighlightRow, alerts, pageIndex, pageSize]); const mergedGridStyle = useMemo(() => { const propGridStyle: NonNullable = props.gridStyle ?? {}; @@ -679,12 +688,13 @@ const AlertsTable: React.FunctionComponent = 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 = 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} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx index 8f54eaf5c6278..0028af392455d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.test.tsx @@ -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('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 ; + }); + 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(); @@ -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(); @@ -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(); 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(); await waitFor(() => { @@ -716,7 +738,7 @@ describe('AlertsTableState', () => { ); @@ -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', () => { ); @@ -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(); 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(); + act(() => { + onPageChange({ pageIndex: 1, pageSize: 50 }); + }); + rerender( + + ); + 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(); + act(() => { + onPageChange({ pageIndex: 1, pageSize: 50 }); + }); + act(() => { + refetchAlerts(); + }); + expect(mockUseSearchAlertsQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ pageIndex: 0 }) + ); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx index 31f93378ef9a6..edd18905b1cec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table_state.tsx @@ -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; - 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 ( - + @@ -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['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(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 ( + {ALERTS_TABLE_CONF_ERROR_TITLE}} + body={

{ALERTS_TABLE_CONF_ERROR_MESSAGE}

} + /> + ); + } + + return ( {!isLoading && alertsCount === 0 && ( @@ -539,24 +590,19 @@ const AlertsTableStateWithQueryProvider = memo( {(isLoading || isBrowserFieldDataLoading) && ( )} - {alertsCount !== 0 && isCasesContextAvailable && ( - + {alertsCount > 0 && + (isCasesContextAvailable ? ( + + + + ) : ( - - )} - {alertsCount !== 0 && !isCasesContextAvailable && } + ))} - ) : ( - {ALERTS_TABLE_CONF_ERROR_TITLE}} - body={

{ALERTS_TABLE_CONF_ERROR_MESSAGE}

} - /> ); } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index 4a8243eb0269a..1f37554f49f32 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -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(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/contexts/alerts_table_context.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/contexts/alerts_table_context.ts index e8ffd8647b241..760f420e94c15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/contexts/alerts_table_context.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/contexts/alerts_table_context.ts @@ -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(undefined); - export const AlertsTableContext = createContext({ mutedAlerts: {}, bulkActions: [{}, noop] as unknown as AlertsTableContextType['bulkActions'], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx index 01a936778c366..a48a316b954af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx @@ -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 ( - {showInpectButton && ( + {querySnapshot && showInspectButton && ( - + )} {controls?.right && {controls.right}} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx index f3215cb383620..8d65532c5f10a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.test.tsx @@ -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 () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.tsx index 018f9cf0fd86f..08f172451ca23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_get_muted_alerts.tsx @@ -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') { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx index d697b59028e7c..f7e8b94aa2e66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.test.tsx @@ -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 () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.ts index 2663bfed6bff5..2603426e492d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_mute_alert.ts @@ -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', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx index 7d47cd75a386f..d24a481ba30b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.test.tsx @@ -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 () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.ts index 34c1a47062fa2..33c5befb430e2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/alert_mute/use_unmute_alert.ts @@ -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', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/index.ts index 3ce3b5ae5cd9a..6c27075b53d02 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/index.ts @@ -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'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx index d966dbbba8fcb..d84909e746f27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.test.tsx @@ -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, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts index 1d275abc68d91..727ff4f93ebd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_actions.ts @@ -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; - 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, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx index 40487ddac4257..b4598f56c02f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.test.tsx @@ -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 () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.tsx index 50b5cebc1f6c7..55b7d55b81784 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_get_cases.tsx @@ -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) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts.tsx index 3e1eb572283c7..a75f1b3dbfc04 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts.tsx @@ -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( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts_by_query.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts_by_query.tsx index 3ef0efd7faad8..321d861e03615 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts_by_query.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_bulk_untrack_alerts_by_query.tsx @@ -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', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx deleted file mode 100644 index 7171e793ef52c..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.test.tsx +++ /dev/null @@ -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(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({ ...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({ - ...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({ - ...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 }) => { - 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, - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx deleted file mode 100644 index c47c2d3207248..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_alerts.tsx +++ /dev/null @@ -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; - pagination: { - pageIndex: number; - pageSize: number; - }; - onLoaded?: (alerts: Alerts) => void; - onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void; - runtimeMappings?: MappingRuntimeFields; - sort: SortCombinations[]; - skip: boolean; -} - -type AlertRequest = Omit; - -type Refetch = () => void; - -export interface FetchAlertResp { - /** - * We need to have it because of lot code is expecting this format - * @deprecated - */ - oldAlertsData: Array>; - /** - * 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; -interface AlertStateReducer { - loading: boolean; - request: Omit; - response: AlertResponseState; -} - -type AlertActions = - | { type: 'loading'; loading: boolean } - | { - type: 'response'; - alerts: Alerts; - totalAlerts: number; - oldAlertsData: Array>; - ecsAlertsData: unknown[]; - } - | { type: 'resetPagination' } - | { type: 'request'; request: Omit }; - -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(noop); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(new Subscription()); - const [{ loading, request: alertRequest, response: alertResponse }, dispatch] = useReducer( - alertReducer, - initialAlertState - ); - const prevAlertRequest = useRef(null); - const inspectQuery = useRef({ - 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( - { ...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((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>; - 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 }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_browser_fields_capabilities.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_browser_fields_capabilities.tsx index adb1103915541..b2ad2ef26ab7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_browser_fields_capabilities.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_fetch_browser_fields_capabilities.tsx @@ -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 = ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts index e713c1820cb81..bc0c14772b575 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts @@ -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'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.test.tsx index fb8444bb93540..5d4cc708ca097 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.test.tsx @@ -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', () => { ); fireEvent.click(await screen.findByTestId('inspect-icon-button')); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx index 2be7ae9b15bcc..235deacbd20c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/index.tsx @@ -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 = mem interface InspectButtonProps { onCloseInspect?: () => void; showInspectButton?: boolean; - getInspectQuery: GetInspectQuery; + querySnapshot: EsQuerySnapshot; inspectTitle: string; } -const InspectButtonComponent: React.FC = ({ - getInspectQuery, - inspectTitle, -}) => { +const InspectButtonComponent: React.FC = ({ querySnapshot, inspectTitle }) => { const [isShowingModal, setIsShowingModal] = useState(false); const onOpenModal = useCallback(() => { @@ -66,7 +63,7 @@ const InspectButtonComponent: React.FC = ({ )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx index cb08e3f672fd8..5d9d126f1940d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx @@ -36,10 +36,10 @@ describe('Modal Inspect', () => { const defaultProps: ModalInspectProps = { closeModal, title: 'Inspect', - getInspectQuery: () => ({ + querySnapshot: { request: [getRequest()], response: [response], - }), + }, }; const renderModalInspectQuery = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.tsx index e13284f1048b2..631fde44070f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.tsx @@ -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]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx index 54e2466b524cc..e339b95eae8fa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/toolbar_visibility.tsx @@ -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 && ( - + )} {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 && ( ), @@ -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 && ( ), @@ -270,7 +266,7 @@ export const useGetToolbarVisibility = ({ refresh, setIsBulkActionsLoading, controls, - getInspectQuery, + querySnapshot, showInspectButton, ]); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.ts index 4ea3c7a47e9bc..1a77466f246f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.ts @@ -19,8 +19,9 @@ export type KibanaContext = KibanaReactContextValue useKibana(); +const useTypedKibana = () => { + return useKibana(); +}; export { KibanaContextProvider, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index fdf855527e369..415541b26b378 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -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; - fieldFormats: FieldFormatsRegistry; + fieldFormats: FieldFormatsStart; } & Partial>; export type SetFlyoutAlert = (alertId: string) => void; diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index abcf5c1e5bf87..f393e0a3e944f 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -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", diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index c42a266ccb298..cbdd9d2301294 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -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, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_cell_actions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_cell_actions.cy.ts index 49dd7dc2259fc..4246c92ad7cb4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_cell_actions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_cell_actions.cy.ts @@ -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'); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index 933fd2ad3d188..f7a3c2fdb4e1a 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -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"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts index 1760b6b56582f..2a3038b090240 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts @@ -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'); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 65203d2594d07..bf0b4676bf117 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -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) {