From 6d4ec537709508f249924a7c8156eb9c9f149864 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 03:57:47 +1100 Subject: [PATCH 01/10] [8.x] Add prompt for connector configuration being in editable mode (#202463) (#203117) # Backport This will backport the following commits from `main` to `8.x`: - [Add prompt for connector configuration being in editable mode (#202463)](https://github.com/elastic/kibana/pull/202463) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Artem Shelkovnikov --- .../configuration/connector_configuration.tsx | 11 ++++++ .../create_connector/configuration_step.tsx | 39 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx index cd80b2489012e..8adc6ac2e38c4 100644 --- a/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx +++ b/packages/kbn-search-connectors/components/configuration/connector_configuration.tsx @@ -47,6 +47,7 @@ interface ConnectorConfigurationProps { isLoading: boolean; saveConfig: (configuration: Record) => void; saveAndSync?: (configuration: Record) => void; + onEditStateChange?: (isEdit: boolean) => void; stackManagementLink?: string; subscriptionLink?: string; children?: React.ReactNode; @@ -94,6 +95,7 @@ export const ConnectorConfigurationComponent: FC< isLoading, saveConfig, saveAndSync, + onEditStateChange, subscriptionLink, stackManagementLink, }) => { @@ -110,6 +112,15 @@ export const ConnectorConfigurationComponent: FC< ); const [isEditing, setIsEditing] = useState(false); + useEffect( + function propogateEditState() { + if (onEditStateChange) { + onEditStateChange(isEditing); + } + }, + [isEditing, onEditStateChange] + ); + useEffect(() => { if (!isDeepEqual(configuration, configurationRef.current)) { configurationRef.current = configuration; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx index 9749c49ea4d68..f8bff23aa56a1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/configuration_step.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors'; @@ -40,6 +41,8 @@ export const ConfigurationStep: React.FC = ({ title, set const { connector } = useValues(ConnectorViewLogic); const { updateConnectorConfiguration } = useActions(ConnectorViewLogic); const { setFormDirty } = useActions(NewConnectorLogic); + const { overlays } = useKibana().services; + const [isFormEditing, setIsFormEditing] = useState(false); const { status } = useValues(ConnectorConfigurationApiLogic); const isSyncing = false; @@ -77,6 +80,7 @@ export const ConfigurationStep: React.FC = ({ title, set connectorId: connector.id, }); }} + onEditStateChange={setIsFormEditing} /> {isSyncing && ( @@ -111,7 +115,38 @@ export const ConfigurationStep: React.FC = ({ title, set { + onClick={async () => { + if (isFormEditing) { + const confirmResponse = await overlays?.openConfirm( + i18n.translate('xpack.enterpriseSearch.configureConnector.unsavedPrompt.body', { + defaultMessage: + 'You are still editing connector configuration, are you sure you want to continue without saving? You can complete the setup later in the connector configuration page, but this guided flow offers more help.', + }), + { + title: i18n.translate( + 'xpack.enterpriseSearch.configureConnector.unsavedPrompt.title', + { + defaultMessage: 'Connector configuration is not saved', + } + ), + cancelButtonText: i18n.translate( + 'xpack.enterpriseSearch.configureConnector.unsavedPrompt.cancel', + { + defaultMessage: 'Continue setup', + } + ), + confirmButtonText: i18n.translate( + 'xpack.enterpriseSearch.configureConnector.unsavedPrompt.confirm', + { + defaultMessage: 'Leave the page', + } + ), + } + ); + if (!confirmResponse) { + return; + } + } setFormDirty(false); setCurrentStep('finish'); }} From 967baa1ad2f1d8c6c700652fa7fc101efad4a1f3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 04:30:50 +1100 Subject: [PATCH 02/10] [8.x] [APM] Update alert rule producer terms in getServicesAlerts query (#203100) (#203123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[APM] Update alert rule producer terms in getServicesAlerts query (#203100)](https://github.com/elastic/kibana/pull/203100) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Irene Blanco --- .../common/alerting/config/apm_alerting_feature_ids.ts | 4 ++-- .../apm/public/components/app/alerts_overview/index.tsx | 8 ++++---- .../apm/server/lib/helpers/get_apm_alerts_client.ts | 6 ++++-- .../routes/services/get_services/get_service_alerts.ts | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/common/alerting/config/apm_alerting_feature_ids.ts b/x-pack/plugins/observability_solution/apm/common/alerting/config/apm_alerting_feature_ids.ts index 784ab0b534256..ff6e86a9f79bc 100644 --- a/x-pack/plugins/observability_solution/apm/common/alerting/config/apm_alerting_feature_ids.ts +++ b/x-pack/plugins/observability_solution/apm/common/alerting/config/apm_alerting_feature_ids.ts @@ -11,7 +11,7 @@ import { type ValidFeatureId, } from '@kbn/rule-data-utils'; -export const apmAlertingConsumers: ValidFeatureId[] = [ +export const APM_ALERTING_CONSUMERS: ValidFeatureId[] = [ AlertConsumers.LOGS, AlertConsumers.APM, AlertConsumers.SLO, @@ -20,4 +20,4 @@ export const apmAlertingConsumers: ValidFeatureId[] = [ AlertConsumers.ALERTS, ]; -export const apmAlertingRuleTypeIds: string[] = [...OBSERVABILITY_RULE_TYPE_IDS]; +export const APM_ALERTING_RULE_TYPE_IDS: string[] = [...OBSERVABILITY_RULE_TYPE_IDS]; diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/alerts_overview/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/alerts_overview/index.tsx index 682634819e623..a14731db9efac 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/alerts_overview/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/alerts_overview/index.tsx @@ -14,8 +14,8 @@ import { BoolQuery } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { - apmAlertingConsumers, - apmAlertingRuleTypeIds, + APM_ALERTING_CONSUMERS, + APM_ALERTING_RULE_TYPE_IDS, } from '../../../../common/alerting/config/apm_alerting_feature_ids'; import { ApmPluginStartDeps } from '../../../plugin'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; @@ -111,8 +111,8 @@ export function AlertsOverview() { alertsTableConfigurationRegistry={alertsTableConfigurationRegistry} id={'service-overview-alerts'} configurationId={AlertConsumers.OBSERVABILITY} - ruleTypeIds={apmAlertingRuleTypeIds} - consumers={apmAlertingConsumers} + ruleTypeIds={APM_ALERTING_RULE_TYPE_IDS} + consumers={APM_ALERTING_CONSUMERS} query={esQuery} showAlertStatusWithFlapping cellContext={{ observabilityRuleTypeRegistry }} diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts index 95c29472dbc79..fb519e2ef859f 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts @@ -12,7 +12,7 @@ import { DataTier } from '@kbn/observability-shared-plugin/common'; import { searchExcludedDataTiers } from '@kbn/observability-plugin/common/ui_settings_keys'; import { estypes } from '@elastic/elasticsearch'; import { getDataTierFilterCombined } from '@kbn/apm-data-access-plugin/server/utils'; -import { apmAlertingRuleTypeIds } from '../../../common/alerting/config/apm_alerting_feature_ids'; +import { APM_ALERTING_RULE_TYPE_IDS } from '../../../common/alerting/config/apm_alerting_feature_ids'; import type { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/register_apm_server_routes'; export type ApmAlertsClient = Awaited>; @@ -32,7 +32,9 @@ export async function getApmAlertsClient({ const ruleRegistryPluginStart = await plugins.ruleRegistry.start(); const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request); - const apmAlertsIndices = await alertsClient.getAuthorizedAlertsIndices(apmAlertingRuleTypeIds); + const apmAlertsIndices = await alertsClient.getAuthorizedAlertsIndices( + APM_ALERTING_RULE_TYPE_IDS + ); if (!apmAlertsIndices || isEmpty(apmAlertsIndices)) { throw Error('No alert indices exist for "apm"'); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts index 01a125f456443..c47668bc1ee32 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/get_services/get_service_alerts.ts @@ -18,7 +18,7 @@ import { ALERT_STATUS_ACTIVE, ALERT_UUID, } from '@kbn/rule-data-utils'; -import { observabilityFeatureId } from '@kbn/observability-shared-plugin/common'; +import { APM_ALERTING_CONSUMERS } from '../../../../common/alerting/config/apm_alerting_feature_ids'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; import { ServiceGroup } from '../../../../common/service_groups'; import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; @@ -58,7 +58,7 @@ export async function getServicesAlerts({ query: { bool: { filter: [ - ...termsQuery(ALERT_RULE_PRODUCER, 'apm', observabilityFeatureId), + ...termsQuery(ALERT_RULE_PRODUCER, ...APM_ALERTING_CONSUMERS), ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...rangeQuery(start, end), ...kqlQuery(kuery), From 13dc35695f05375e96ca6ae39d4a2f3151fb6233 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 04:41:20 +1100 Subject: [PATCH 03/10] [8.x] fix: [Search:Stateful:EnterpriseSearch:WorkplaceSearch:UsersandRoles]Assign to specific groups input combobox is missing form label (#202462) (#203125) # Backport This will backport the following commits from `main` to `8.x`: - [fix: [Search:Stateful:EnterpriseSearch:WorkplaceSearch:UsersandRoles]Assign to specific groups input combobox is missing form label (#202462)](https://github.com/elastic/kibana/pull/202462) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Alexey Antonov --- .../role_mappings/group_assignment_selector.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx index 5d8ef7ababc8c..f6c88cb7ae6ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/group_assignment_selector.tsx @@ -9,7 +9,13 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup } from '@elastic/eui'; +import { + EuiComboBox, + EuiFormRow, + EuiHorizontalRule, + EuiRadioGroup, + useGeneratedHtmlId, +} from '@elastic/eui'; import { RoleOptionLabel } from '../../../shared/role_mapping'; @@ -31,6 +37,8 @@ export const GroupAssignmentSelector: React.FC = () => { const { includeInAllGroups, availableGroups, selectedGroups, selectedOptions } = useValues(RoleMappingsLogic); + const groupAssigmentLabelId = useGeneratedHtmlId(); + const hasGroupAssignment = selectedGroups.size > 0 || includeInAllGroups; const groupOptions = [ @@ -51,11 +59,12 @@ export const GroupAssignmentSelector: React.FC = () => { handleAllGroupsSelectionChange(id === 'all')} legend={{ - children: {GROUP_ASSIGNMENT_LABEL}, + children: {GROUP_ASSIGNMENT_LABEL}, }} /> @@ -69,6 +78,7 @@ export const GroupAssignmentSelector: React.FC = () => { }} fullWidth isDisabled={includeInAllGroups} + aria-labelledby={groupAssigmentLabelId} /> From 67e70739edd4abaec05f305156e39a7a7c69e719 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 04:44:18 +1100 Subject: [PATCH 04/10] [8.x] [RCA] [Events timeline] Show all events (#202235) (#202956) # Backport This will backport the following commits from `main` to `8.x`: - [[RCA] [Events timeline] Show all events (#202235)](https://github.com/elastic/kibana/pull/202235) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Bena Kansara <69037875+benakansara@users.noreply.github.com> --- .../components/events_timeline/events_timeline.tsx | 12 ------------ .../investigate_app/tsconfig.json | 1 - 2 files changed, 13 deletions(-) diff --git a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx index c3f92139bd936..45b245f68b4b0 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/pages/details/components/events_timeline/events_timeline.tsx @@ -11,9 +11,6 @@ import { Chart, Axis, AreaSeries, Position, ScaleType, Settings } from '@elastic import { useActiveCursor } from '@kbn/charts-plugin/public'; import { EuiSkeletonText } from '@elastic/eui'; import { getBrushData } from '@kbn/observability-utils-browser/chart/utils'; -import { Group } from '@kbn/observability-alerting-rule-utils'; -import { ALERT_GROUP } from '@kbn/rule-data-utils'; -import { SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; import { AnnotationEvent } from './annotation_event'; import { TIME_LINE_THEME } from './timeline_theme'; import { useFetchEvents } from '../../../../hooks/use_fetch_events'; @@ -27,19 +24,10 @@ export const EventsTimeLine = () => { const baseTheme = dependencies.start.charts.theme.useChartsBaseTheme(); const { globalParams, updateInvestigationParams } = useInvestigation(); - const { alert } = useInvestigation(); - - const filter = useMemo(() => { - const group = (alert?.[ALERT_GROUP] as unknown as Group[])?.find( - ({ field }) => field === SERVICE_NAME - ); - return group ? `{"${SERVICE_NAME}":"${alert?.[SERVICE_NAME]}"}` : ''; - }, [alert]); const { data: events, isLoading } = useFetchEvents({ rangeFrom: globalParams.timeRange.from, rangeTo: globalParams.timeRange.to, - filter, }); const chartRef = useRef(null); diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index 0851a13367091..bc67b591a57b8 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -66,7 +66,6 @@ "@kbn/calculate-auto", "@kbn/ml-random-sampler-utils", "@kbn/charts-plugin", - "@kbn/observability-alerting-rule-utils", "@kbn/observability-utils-browser", "@kbn/usage-collection-plugin", "@kbn/inference-common", From 627d84bcf65eb8287d165136aa6259d558fc4ac8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 05:02:50 +1100 Subject: [PATCH 05/10] [8.x] [ML] Transform: Fix <DefinePivotSummary /> jest test. (#195998) (#203129) # Backport This will backport the following commits from `main` to `8.x`: - [[ML] Transform: Fix <DefinePivotSummary /> jest test. (#195998)](https://github.com/elastic/kibana/pull/195998) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Walter Rafelsberger --- .../transform/public/app/__mocks__/app_dependencies.tsx | 9 ++++++++- .../components/step_define/step_define_summary.test.tsx | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx index cdb159c158d10..49bd43232c9f5 100644 --- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx @@ -67,7 +67,14 @@ dataStart.search.search = jest.fn(({ params }: IKibanaSearchRequest) => { }) as ISearchGeneric; // Replace mock to support tests for `use_index_data`. -coreSetup.http.post = jest.fn().mockResolvedValue([]); +coreSetup.http.post = jest.fn().mockImplementation((endpoint) => { + if (endpoint === '/internal/transform/transforms/_preview') { + return Promise.resolve({ + generated_dest_index: { mappings: { properties: {} } }, + preview: [], + }); + } +}); const appDependencies: AppDependencies = { analytics: coreStart.analytics, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 5b6e314e951aa..7c08a2462f65a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -20,9 +20,7 @@ import { StepDefineSummary } from './step_define_summary'; jest.mock('../../../../app_dependencies'); -// Failing: https://github.com/elastic/kibana/issues/195992 -describe.skip('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. +describe('Transform: ', () => { test('Minimal initialization', async () => { // Arrange const queryClient = new QueryClient(); From a70bdf01cd8208c0f2358a98f5fcc5d10ac0a7d6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 05:05:47 +1100 Subject: [PATCH 06/10] [8.x] [OBX-UX-MGMT][ALERTING] Fix APM rule error msg when KQL filter is invalid (#203096) (#203134) # Backport This will backport the following commits from `main` to `8.x`: - [[OBX-UX-MGMT][ALERTING] Fix APM rule error msg when KQL filter is invalid (#203096)](https://github.com/elastic/kibana/pull/203096) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Faisal Kanout --- .../apm_rule_unified_search_bar.tsx | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/apm_rule_unified_search_bar.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/apm_rule_unified_search_bar.tsx index d91807ec08462..393492cfae9a0 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/apm_rule_unified_search_bar.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/apm_rule_unified_search_bar.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Query } from '@kbn/es-query'; +import { EuiFormErrorText } from '@elastic/eui'; +import { Query, fromKueryExpression } from '@kbn/es-query'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ApmPluginStartDeps } from '../../../plugin'; import { useAdHocApmDataView } from '../../../hooks/use_adhoc_apm_data_view'; @@ -26,7 +27,7 @@ export function ApmRuleUnifiedSearchBar({ setRuleParams: (key: string, value: any) => void; }) { const { services } = useKibana(); - + const [queryError, setQueryError] = React.useState(); const { unifiedSearch: { ui: { SearchBar }, @@ -38,27 +39,38 @@ export function ApmRuleUnifiedSearchBar({ const handleSubmit = (payload: { query?: Query }) => { const { query } = payload; - setRuleParams('searchConfiguration', { query }); + try { + setQueryError(undefined); + fromKueryExpression(query?.query as string); + setRuleParams('searchConfiguration', { query }); + } catch (e) { + setQueryError(e.message); + } }; return ( - + <> + + {queryError && ( + {queryError} + )} + ); } From ba5086b157246b4d9543291bc84e928065c6bde5 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Thu, 5 Dec 2024 10:23:24 -0800 Subject: [PATCH 07/10] [8.x] [Cloud Security][Bug] Fix for Page Crashing (#203028) (#203139) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security][Bug] Fix for Page Crashing (#203028)](https://github.com/elastic/kibana/pull/203028) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../cloud_security_posture/components/alerts/alerts_preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index 8edd0b7981936..8592ed61abe33 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -93,7 +93,7 @@ export const AlertsPreview = ({ const { goToEntityInsightTab } = useNavigateEntityInsight({ field, value, - queryIdExtension: 'ALERTS_PREVIEW', + queryIdExtension: isPreviewMode ? 'ALERTS_PREVIEW_TRUE' : 'ALERTS_PREVIEW_FALSE', subTab: CspInsightLeftPanelSubTab.ALERTS, }); const link = useMemo( From 6445464eca0099d402bc848bde2f01e378108ddf Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 05:27:00 +1100 Subject: [PATCH 08/10] [8.x] [SecuritySolution][Onboarding] Siem migration start card and flyout - Part 1 (#202886) (#203141) # Backport This will backport the following commits from `main` to `8.x`: - [[SecuritySolution][Onboarding] Siem migration start card and flyout - Part 1 (#202886)](https://github.com/elastic/kibana/pull/202886) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sergi Massaneda --- .../common/api/quickstart_client.gen.ts | 4 +- .../common/siem_migrations/constants.ts | 2 + .../model/api/rules/rule_migration.gen.ts | 13 +- .../api/rules/rule_migration.schema.yaml | 211 +++++++++-------- .../siem_migrations/model/common.gen.ts | 9 - .../siem_migrations/model/common.schema.yaml | 5 - .../model/rule_migration.gen.ts | 23 +- .../model/rule_migration.schema.yaml | 42 ++-- .../components/onboarding_body/body_config.ts | 7 + .../cards/common/card_content_panel.tsx | 2 +- .../ai_connector/ai_connector_card.tsx | 23 +- .../ai_connector/connectors_check_complete.ts | 2 +- .../siem_migrations/ai_connector/index.ts | 6 +- .../start_migration/context.tsx | 35 +++ .../images/card_header_icon.png | Bin 0 -> 1251 bytes .../siem_migrations/start_migration/index.ts | 28 +++ .../missing_ai_connector_callout.tsx | 38 +++ .../panels/migration_progress_panel.tsx | 45 ++++ .../panels/migration_ready_panel.tsx | 68 ++++++ .../panels/migration_result_panel.tsx | 87 +++++++ .../panels/upload_rules_panel.styles.ts | 19 ++ .../panels/upload_rules_panel.tsx | 80 +++++++ .../start_migration_card.styles.ts | 20 ++ .../start_migration/start_migration_card.tsx | 79 +++++++ .../start_migration_check_complete.ts | 16 ++ .../start_migration/translations.ts | 118 ++++++++++ .../start_migration/upload_rules_panels.tsx | 46 ++++ .../public/onboarding/constants.ts | 1 + .../siem_migrations/common/icon/index.tsx | 8 + .../common/icon/siem_migrations.svg | 28 +++ .../public/siem_migrations/rules/api/index.ts | 219 ++++++++++-------- .../components/data_input_flyout/constants.ts | 26 +++ .../data_input_flyout/data_input_flyout.tsx | 112 +++++++++ .../components/data_input_flyout/index.ts | 8 + .../steps/common/sub_step_wrapper.tsx | 26 +++ .../steps/rules/rules_data_input.tsx | 95 ++++++++ .../rules/sub_steps/check_resources/index.tsx | 30 +++ .../sub_steps/check_resources/translations.ts | 20 ++ .../copy_export_query/copy_export_query.tsx | 53 +++++ .../sub_steps/copy_export_query/index.tsx | 26 +++ .../copy_export_query/translations.ts | 18 ++ .../sub_steps/rules_file_upload/index.tsx | 58 +++++ .../rules_file_upload/parse_rules_file.ts | 79 +++++++ .../rules_file_upload/rules_file_upload.tsx | 122 ++++++++++ .../rules_file_upload/translations.ts | 70 ++++++ .../steps/rules/translations.ts | 13 ++ .../components/data_input_flyout/types.ts | 10 + .../rules/components/header_buttons/index.tsx | 4 +- .../siem_migrations/rules/pages/index.tsx | 2 +- .../hooks/common/api_request_reducer.ts | 26 +++ .../rules/service/hooks/translations.ts | 17 ++ .../service/hooks/use_create_migration.ts | 55 +++++ .../{ => service}/hooks/use_latest_stats.ts | 2 +- .../service/hooks/use_start_migration.ts | 52 +++++ .../rules/service/rule_migrations_service.ts | 145 +++++++++--- .../siem_migrations/rules/service/storage.ts | 30 ++- .../rules/service/success_notification.tsx | 6 +- .../rules/service/translations.ts | 23 ++ .../public/siem_migrations/rules/types.ts | 4 +- .../lib/siem_migrations/rules/api/create.ts | 13 +- .../lib/siem_migrations/rules/api/get.ts | 20 +- .../rules/api/util/installation.ts | 6 +- .../data/rule_migrations_data_rules_client.ts | 108 ++++----- .../rules/data/rule_migrations_field_maps.ts | 3 +- .../match_prebuilt_rule.ts | 2 +- .../services/security_solution_api.gen.ts | 13 +- 66 files changed, 2194 insertions(+), 387 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/context.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/images/card_header_icon.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts rename x-pack/plugins/security_solution/public/siem_migrations/rules/{ => service}/hooks/use_latest_stats.ts (91%) create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts create mode 100644 x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index c545474b9eed3..eba4c4124337d 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -374,6 +374,7 @@ import type { ResolveTimelineResponse, } from './timeline/resolve_timeline/resolve_timeline_route.gen'; import type { + CreateRuleMigrationRequestParamsInput, CreateRuleMigrationRequestBodyInput, CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, @@ -696,7 +697,7 @@ Migrations are initiated per index. While the process is neither destructive nor this.log.info(`${new Date().toISOString()} Calling API CreateRuleMigration`); return this.kbnClient .request({ - path: '/internal/siem_migrations/rules', + path: replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '1', }, @@ -2290,6 +2291,7 @@ export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } export interface CreateRuleMigrationProps { + params: CreateRuleMigrationRequestParamsInput; body: CreateRuleMigrationRequestBodyInput; } export interface CreateTimelinesProps { diff --git a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts index 789947150a67e..e04130e7f44d7 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/constants.ts @@ -11,6 +11,8 @@ export const SIEM_MIGRATIONS_PATH = '/internal/siem_migrations' as const; export const SIEM_RULE_MIGRATIONS_PATH = `${SIEM_MIGRATIONS_PATH}/rules` as const; export const SIEM_RULE_MIGRATIONS_ALL_STATS_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/stats` as const; +export const SIEM_RULE_MIGRATION_CREATE_PATH = + `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id?}` as const; export const SIEM_RULE_MIGRATION_PATH = `${SIEM_RULE_MIGRATIONS_PATH}/{migration_id}` as const; export const SIEM_RULE_MIGRATION_START_PATH = `${SIEM_RULE_MIGRATION_PATH}/start` as const; export const SIEM_RULE_MIGRATION_RETRY_PATH = `${SIEM_RULE_MIGRATION_PATH}/retry` as const; diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index aa69f3b3c27f0..8a549e8e11817 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -17,19 +17,28 @@ import { z } from '@kbn/zod'; import { ArrayFromString } from '@kbn/zod-helpers'; +import { NonEmptyString } from '../../../../api/model/primitives.gen'; import { - OriginalRule, ElasticRulePartial, RuleMigrationTranslationResult, RuleMigrationComments, RuleMigrationTaskStats, + OriginalRule, RuleMigration, RuleMigrationTranslationStats, RuleMigrationResourceData, RuleMigrationResourceType, RuleMigrationResource, } from '../../rule_migration.gen'; -import { NonEmptyString, ConnectorId, LangSmithOptions } from '../../common.gen'; +import { ConnectorId, LangSmithOptions } from '../../common.gen'; + +export type CreateRuleMigrationRequestParams = z.infer; +export const CreateRuleMigrationRequestParams = z.object({ + migration_id: NonEmptyString.optional(), +}); +export type CreateRuleMigrationRequestParamsInput = z.input< + typeof CreateRuleMigrationRequestParams +>; export type CreateRuleMigrationRequestBody = z.infer; export const CreateRuleMigrationRequestBody = z.array(OriginalRule); diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index a062b75d41699..8b9d264cf4104 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -4,38 +4,7 @@ info: version: '1' paths: # Rule migrations APIs - /internal/siem_migrations/rules: - post: - summary: Creates a new rule migration - operationId: CreateRuleMigration - x-codegen-enabled: true - x-internal: true - description: Creates a new SIEM rules migration using the original vendor rules provided - tags: - - SIEM Rule Migrations - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule' - responses: - 200: - description: Indicates migration have been created correctly. - content: - application/json: - schema: - type: object - required: - - migration_id - properties: - migration_id: - description: The migration id created. - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' - put: summary: Updates rules migrations operationId: UpdateRuleMigration @@ -57,7 +26,7 @@ paths: properties: id: description: The rule migration id - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' elastic_rule: description: The migrated elastic rule attributes to update. $ref: '../../rule_migration.schema.yaml#/components/schemas/ElasticRulePartial' @@ -81,95 +50,64 @@ paths: type: boolean description: Indicates rules migrations have been updated. - /internal/siem_migrations/rules/{migration_id}/install: - post: - summary: Installs translated migration rules - operationId: InstallMigrationRules + /internal/siem_migrations/rules/stats: + get: + summary: Retrieves the stats for all rule migrations + operationId: GetAllStatsRuleMigration x-codegen-enabled: true - description: Installs migration rules + x-internal: true + description: Retrieves the rule migrations stats for all migrations stored in the system tags: - SIEM Rule Migrations - parameters: - - name: migration_id - in: path - required: true - schema: - description: The migration id to isnstall rules for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' - requestBody: - required: true - content: - application/json: - schema: - type: array - items: - description: The rule migration id - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' responses: 200: - description: Indicates rules migrations have been installed correctly. + description: Indicates rule migrations have been retrieved correctly. content: application/json: schema: - type: object - required: - - installed - properties: - installed: - type: boolean - description: Indicates rules migrations have been installed. + type: array + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats' - /internal/siem_migrations/rules/{migration_id}/install_translated: + ## Specific rule migration APIs + + /internal/siem_migrations/rules/{migration_id}: post: - summary: Installs all translated migration rules - operationId: InstallTranslatedMigrationRules + summary: Creates a new rule migration + operationId: CreateRuleMigration x-codegen-enabled: true - description: Installs all translated migration rules + x-internal: true + description: Creates a new SIEM rules migration using the original vendor rules provided tags: - SIEM Rule Migrations parameters: - name: migration_id in: path - required: true + required: false schema: - description: The migration id to install translated rules for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + description: The migration id to create rules for + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '../../rule_migration.schema.yaml#/components/schemas/OriginalRule' responses: 200: - description: Indicates rules migrations have been installed correctly. + description: Indicates migration have been created correctly. content: application/json: schema: type: object required: - - installed + - migration_id properties: - installed: - type: boolean - description: Indicates rules migrations have been installed. - - /internal/siem_migrations/rules/stats: - get: - summary: Retrieves the stats for all rule migrations - operationId: GetAllStatsRuleMigration - x-codegen-enabled: true - x-internal: true - description: Retrieves the rule migrations stats for all migrations stored in the system - tags: - - SIEM Rule Migrations - responses: - 200: - description: Indicates rule migrations have been retrieved correctly. - content: - application/json: - schema: - type: array - items: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationTaskStats' - - ## Specific rule migration APIs - - /internal/siem_migrations/rules/{migration_id}: + migration_id: + description: The migration id created. + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' get: summary: Retrieves all the rules of a migration operationId: GetRuleMigration @@ -184,7 +122,7 @@ paths: required: true schema: description: The migration id to start - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - name: page in: query required: false @@ -222,6 +160,73 @@ paths: 204: description: Indicates the migration id was not found. + /internal/siem_migrations/rules/{migration_id}/install: + post: + summary: Installs translated migration rules + operationId: InstallMigrationRules + x-codegen-enabled: true + description: Installs migration rules + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to install rules for + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + description: The rule migration id + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been installed correctly. + content: + application/json: + schema: + type: object + required: + - installed + properties: + installed: + type: boolean + description: Indicates rules migrations have been installed. + + /internal/siem_migrations/rules/{migration_id}/install_translated: + post: + summary: Installs all translated migration rules + operationId: InstallTranslatedMigrationRules + x-codegen-enabled: true + description: Installs all translated migration rules + tags: + - SIEM Rule Migrations + parameters: + - name: migration_id + in: path + required: true + schema: + description: The migration id to install translated rules for + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' + responses: + 200: + description: Indicates rules migrations have been installed correctly. + content: + application/json: + schema: + type: object + required: + - installed + properties: + installed: + type: boolean + description: Indicates rules migrations have been installed. + /internal/siem_migrations/rules/{migration_id}/start: put: summary: Starts a rule migration @@ -237,7 +242,7 @@ paths: required: true schema: description: The migration id to start - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -282,7 +287,7 @@ paths: required: true schema: description: The migration id to fetch stats for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates the migration stats has been retrieved correctly. @@ -307,7 +312,7 @@ paths: required: true schema: description: The migration id to fetch translation stats for - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates the migration stats has been retrieved correctly. @@ -333,7 +338,7 @@ paths: required: true schema: description: The migration id to stop - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' responses: 200: description: Indicates migration task stop has been processed successfully. @@ -368,7 +373,7 @@ paths: required: true schema: description: The migration id to attach the resources - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' requestBody: required: true content: @@ -406,7 +411,7 @@ paths: required: true schema: description: The migration id to attach the resources - $ref: '../../common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - name: type in: query required: false diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts index 9b1d0756c3a3b..c6d0959cc10cf 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.gen.ts @@ -16,15 +16,6 @@ import { z } from '@kbn/zod'; -/** - * A string that is not empty and does not contain only whitespace - */ -export type NonEmptyString = z.infer; -export const NonEmptyString = z - .string() - .min(1) - .regex(/^(?! *$).+$/); - /** * The GenAI connector id to use. */ diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml index a50225df778ad..14a5160427f8a 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/common.schema.yaml @@ -6,11 +6,6 @@ paths: {} components: x-codegen-enabled: true schemas: - NonEmptyString: - type: string - pattern: ^(?! *$).+$ - minLength: 1 - description: A string that is not empty and does not contain only whitespace ConnectorId: type: string description: The GenAI connector id to use. diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts index 61706077d9549..b52cdb1c91f19 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.gen.ts @@ -16,7 +16,7 @@ import { z } from '@kbn/zod'; -import { NonEmptyString } from './common.gen'; +import { NonEmptyString } from '../../api/model/primitives.gen'; /** * The original rule vendor identifier. @@ -24,6 +24,19 @@ import { NonEmptyString } from './common.gen'; export type OriginalRuleVendor = z.infer; export const OriginalRuleVendor = z.literal('splunk'); +/** + * The original rule annotations containing additional information. + */ +export type OriginalRuleAnnotations = z.infer; +export const OriginalRuleAnnotations = z + .object({ + /** + * The original rule Mitre Attack IDs. + */ + mitre_attack: z.array(z.string()).optional(), + }) + .catchall(z.unknown()); + /** * The original rule to migrate. */ @@ -40,7 +53,7 @@ export const OriginalRule = z.object({ /** * The original rule name. */ - title: z.string(), + title: NonEmptyString, /** * The original rule description. */ @@ -48,15 +61,15 @@ export const OriginalRule = z.object({ /** * The original rule query. */ - query: z.string(), + query: z.string().min(1), /** * The original rule query language. */ query_language: z.string(), /** - * The original rule Mitre Attack technique IDs. + * The original rule annotations containing additional information. */ - mitre_attack_ids: z.array(z.string()).optional(), + annotations: OriginalRuleAnnotations.optional(), }); /** diff --git a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml index fdcbb7b04515a..4c88c66fc604d 100644 --- a/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml +++ b/x-pack/plugins/security_solution/common/siem_migrations/model/rule_migration.schema.yaml @@ -12,6 +12,17 @@ components: enum: - splunk + OriginalRuleAnnotations: + type: object + description: The original rule annotations containing additional information. + additionalProperties: true + properties: + mitre_attack: + type: array + description: The original rule Mitre Attack IDs. + items: + type: string + OriginalRule: type: object description: The original rule to migrate. @@ -25,27 +36,26 @@ components: properties: id: description: The original rule id. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' vendor: description: The original rule vendor identifier. $ref: '#/components/schemas/OriginalRuleVendor' title: - type: string description: The original rule name. + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: type: string description: The original rule description. query: type: string + minLength: 1 description: The original rule query. query_language: type: string description: The original rule query language. - mitre_attack_ids: - type: array - items: - type: string - description: The original rule Mitre Attack technique IDs. + annotations: + description: The original rule annotations containing additional information. + $ref: '#/components/schemas/OriginalRuleAnnotations' ElasticRule: type: object @@ -72,7 +82,7 @@ components: - esql prebuilt_rule_id: description: The Elastic prebuilt rule id matched. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' integration_ids: type: array items: @@ -80,7 +90,7 @@ components: description: The Elastic integration IDs related to the rule. id: description: The Elastic rule id installed as a result. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' ElasticRulePartial: description: The partial version of the migrated elastic rule. @@ -96,7 +106,7 @@ components: properties: id: description: The rule migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - $ref: '#/components/schemas/RuleMigrationData' RuleMigrationData: @@ -114,10 +124,10 @@ components: description: The moment of creation migration_id: description: The migration id. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' created_by: description: The username of the user who created the migration. - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' original_rule: description: The original rule to migrate. $ref: '#/components/schemas/OriginalRule' @@ -153,7 +163,7 @@ components: properties: id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' status: description: Indicates if the migration task status. $ref: '#/components/schemas/RuleMigrationTaskStatus' @@ -207,7 +217,7 @@ components: properties: id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' rules: type: object description: The rules migration translation stats. @@ -293,10 +303,10 @@ components: properties: id: description: The rule resource migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' migration_id: description: The migration id - $ref: './common.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' updated_at: type: string description: The moment of the last update diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts index 93690f98b48e8..59d11314171a6 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts @@ -13,6 +13,7 @@ import { rulesCardConfig } from './cards/rules'; import { alertsCardConfig } from './cards/alerts'; import { assistantCardConfig } from './cards/assistant'; import { aiConnectorCardConfig } from './cards/siem_migrations/ai_connector'; +import { startMigrationCardConfig } from './cards/siem_migrations/start_migration'; export const defaultBodyConfig: OnboardingGroupConfig[] = [ { @@ -43,4 +44,10 @@ export const siemMigrationsBodyConfig: OnboardingGroupConfig[] = [ }), cards: [aiConnectorCardConfig], }, + { + title: i18n.translate('xpack.securitySolution.onboarding.migrate.title', { + defaultMessage: 'Migrate rules & add data', + }), + cards: [startMigrationCardConfig], + }, ]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx index be1b01fc77081..7f3ba00593fc0 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -17,7 +17,7 @@ export const OnboardingCardContentPanel = React.memo - + {children} diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 127e6b4d57ebd..e42834e85d488 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -6,19 +6,14 @@ */ import React, { useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - useEuiTheme, - COLOR_MODES_STANDARD, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state'; import type { OnboardingCardComponent } from '../../../../../types'; import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; import { ConnectorCards } from '../../common/connectors/connector_cards'; +import { CardSubduedText } from '../../common/card_subdued_text'; import type { AIConnectorCardMetadata } from './types'; import { MissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; @@ -28,9 +23,6 @@ export const AIConnectorCard: OnboardingCardComponent = setComplete, }) => { const { siemMigrations } = useKibana().services; - const { euiTheme, colorMode } = useEuiTheme(); - const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; - const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( siemMigrations.rules.connectorIdStorage.key, null @@ -48,18 +40,11 @@ export const AIConnectorCard: OnboardingCardComponent = const canCreateConnectors = checkCompleteMetadata?.canCreateConnectors; return ( - + {canExecuteConnectors ? ( - - {i18n.AI_CONNECTOR_CARD_DESCRIPTION} - + {i18n.AI_CONNECTOR_CARD_DESCRIPTION} = async ({ http, application, siemMigrations }) => { let isComplete = false; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts index 45080123889d5..d0b32eb1bd638 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts @@ -6,11 +6,11 @@ */ import React from 'react'; -import { AssistantAvatar } from '@kbn/elastic-assistant'; +import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; import type { OnboardingCardConfig } from '../../../../../types'; import { OnboardingCardId } from '../../../../../constants'; import { AI_CONNECTOR_CARD_TITLE } from './translations'; -import { checkAssistantCardComplete } from './connectors_check_complete'; +import { checkAiConnectorsCardComplete } from './connectors_check_complete'; import type { AIConnectorCardMetadata } from './types'; export const aiConnectorCardConfig: OnboardingCardConfig = { @@ -24,6 +24,6 @@ export const aiConnectorCardConfig: OnboardingCardConfig void; + closeFlyout: () => void; +} + +const StartMigrationContext = createContext(null); + +export const StartMigrationContextProvider: React.FC< + PropsWithChildren +> = React.memo(({ children, openFlyout, closeFlyout }) => { + const value = useMemo( + () => ({ openFlyout, closeFlyout }), + [openFlyout, closeFlyout] + ); + return {children}; +}); +StartMigrationContextProvider.displayName = 'StartMigrationContextProvider'; + +export const useStartMigrationContext = (): StartMigrationContextValue => { + const context = useContext(StartMigrationContext); + if (context == null) { + throw new Error('useStartMigrationContext must be used within a StartMigrationContextProvider'); + } + return context; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/images/card_header_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/images/card_header_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b4848e0be1d7442fb00b012629861feec095cf GIT binary patch literal 1251 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9EO-XP4l)OOlRpde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N+zL+@$B+ufw{znCq8vqzo#)w-S+mJ{f%OH^ z4(<$~ke$5vK{HX@K3 zuds_=_^he=`P&X13%_5lvov&9;ClI(!z~7aOi!+v8*47*W6awW_+8Wg-QC9R+b8`G zsa8DlPKCpzqaOTzH zfz@{{UpR#^e15{sm?|M?%>LA$$&>NSEdB>m6&1gzC#K(>KUq$N)uZuNXGGbpUn}p4 zI_b@F&i||&oE7K(v--%C&E@~p{P>HQe)#VXYJCt`_}A&g&&yR?YohEkzOJ7qQ@h({ zbvU=I;{6%bP4TNYsr9lZO%PVl-4k#;9Ov{ zQN%_mtMF5ma#P%*Z_Llbk{CN23-|+y9m7+#w(@VBxOT!$V+WqD*#!@*YKy)+6-xT_ z{oe#RVflH1qfjB{*sQ&DVx_|3)i-%-8IqeM zFIfe!t}l*{I4QonUh{m3t>UBqzPFw=uyHsU@qS$$*>Hp@?b)I#E4OWY7Pjlce69<- z9XXTujA_vXy(2!?gYEa;Hi-Fm&uqPEF+<{s@I%jDUd?^>|LgDh3qJpyc7L1NpC{jE za)i4q-d!hNGAr`i?W+ z5g0&;j0n{E$Eb6kwB?zFYD>2FK61OBl`6q)L zJ^e3aQ{q|gr{6j8ue@g4l?%8%}rcAO}inZM(Q}a&?ZuIq+ob9#I*!EaC zwpf|J@9vKozBe?@KM6$i@&jEvaox^|?8k~~mSpEWY1}9y|8kaeWkAkx_g&B2n)7Iojc`SCjMhJ!QCGXvf9opt9EH#xT1 zA3t6Wn11KP;RItl{?Z2RyeEkpMdz!x&fv~_61YKde)`l8TXs)$SAF@z<#tesl + import( + /* webpackChunkName: "onboarding_siem_migrations_start_migration_card" */ + './start_migration_card' + ) + ), + checkComplete: checkStartMigrationCardComplete, + licenseTypeRequired: 'enterprise', +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx new file mode 100644 index 0000000000000..324dd405d5141 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { CardCallOut } from '../../common/card_callout'; +import * as i18n from './translations'; + +interface MissingAIConnectorCalloutProps { + onExpandAiConnectorsCard: () => void; +} + +export const MissingAIConnectorCallout = React.memo( + ({ onExpandAiConnectorsCard }) => ( + + + + {i18n.START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON} + + + + + + } + /> + + ) +); +MissingAIConnectorCallout.displayName = 'MissingAIConnectorCallout'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx new file mode 100644 index 0000000000000..0527e1cfbdf17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_progress_panel.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiPanel, EuiProgress } from '@elastic/eui'; +import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; +import * as i18n from '../translations'; +import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; + +export interface MigrationProgressPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationProgressPanel = React.memo( + ({ migrationStats }) => { + const progressValue = useMemo(() => { + const finished = migrationStats.rules.completed + migrationStats.rules.failed; + return (finished / migrationStats.rules.total) * 100; + }, [migrationStats.rules]); + + return ( + + + + +

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

+
+
+ + +

{i18n.START_MIGRATION_CARD_PROGRESS_DESCRIPTION}

+
+
+ + + +
+
+ ); + } +); +MigrationProgressPanel.displayName = 'MigrationProgressPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx new file mode 100644 index 0000000000000..8603511fa2d6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_ready_panel.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiButtonEmpty, + EuiPanel, +} from '@elastic/eui'; +import { useStartMigration } from '../../../../../../../siem_migrations/rules/service/hooks/use_start_migration'; +import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; +import * as i18n from '../translations'; +import { useStartMigrationContext } from '../context'; +import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; + +export interface MigrationReadyPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationReadyPanel = React.memo(({ migrationStats }) => { + const { openFlyout } = useStartMigrationContext(); + const onOpenFlyout = useCallback(() => { + openFlyout(migrationStats); + }, [openFlyout, migrationStats]); + + const { startMigration, isLoading } = useStartMigration(); + const onStartMigration = useCallback(() => { + startMigration(migrationStats.id); + }, [migrationStats.id, startMigration]); + + return ( + + + + + + +

{i18n.START_MIGRATION_CARD_MIGRATION_TITLE(migrationStats.number)}

+
+
+ + +

{i18n.START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION}

+
+
+
+
+ + + {i18n.START_MIGRATION_CARD_TRANSLATE_BUTTON} + + + + + {i18n.START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON} + + +
+
+ ); +}); +MigrationReadyPanel.displayName = 'MigrationReadyPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx new file mode 100644 index 0000000000000..b73b3cc8b4921 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/migration_result_panel.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import moment from 'moment'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPanel, + EuiHorizontalRule, + EuiIcon, +} from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { AssistantAvatar } from '@kbn/elastic-assistant/impl/assistant/assistant_avatar/assistant_avatar'; +import { SecuritySolutionLinkButton } from '../../../../../../../common/components/links'; +import type { RuleMigrationStats } from '../../../../../../../siem_migrations/rules/types'; +import * as i18n from '../translations'; +import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; + +export interface MigrationResultPanelProps { + migrationStats: RuleMigrationStats; +} +export const MigrationResultPanel = React.memo(({ migrationStats }) => { + return ( + + + + + +

{i18n.START_MIGRATION_CARD_RESULT_TITLE(migrationStats.number)}

+
+
+ + +

+ {i18n.START_MIGRATION_CARD_RESULT_DESCRIPTION( + moment(migrationStats.created_at).format('MMMM Do YYYY, h:mm:ss a'), + moment(migrationStats.last_updated_at).fromNow() + )} +

+
+
+
+
+ + + + + + + + + + +

{i18n.VIEW_TRANSLATED_RULES_TITLE}

+
+
+
+
+ + + + +

{'TODO: chart'}

+
+ + + {i18n.VIEW_TRANSLATED_RULES_BUTTON} + + +
+
+
+
+
+
+ ); +}); +MigrationResultPanel.displayName = 'MigrationResultPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts new file mode 100644 index 0000000000000..0aef40dfeb442 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.styles.ts @@ -0,0 +1,19 @@ +/* + * 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 { css } from '@emotion/css'; + +export const useStyles = (compressed: boolean) => { + const logoSize = compressed ? '32px' : '88px'; + return css` + .siemMigrationsIcon { + width: ${logoSize}; + block-size: ${logoSize}; + inline-size: ${logoSize}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx new file mode 100644 index 0000000000000..edcff3646c5aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/panels/upload_rules_panel.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + EuiButton, + EuiButtonEmpty, + EuiPanel, +} from '@elastic/eui'; +import { SiemMigrationsIcon } from '../../../../../../../siem_migrations/common/icon'; +import * as i18n from '../translations'; +import { useStartMigrationContext } from '../context'; +import { TITLE_CLASS_NAME } from '../start_migration_card.styles'; +import { useStyles } from './upload_rules_panel.styles'; + +export interface UploadRulesPanelProps { + isUploadMore?: boolean; +} +export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { + const styles = useStyles(isUploadMore); + const { openFlyout } = useStartMigrationContext(); + const onOpenFlyout = useCallback(() => { + openFlyout(); + }, [openFlyout]); + + return ( + + + + + + + {isUploadMore ? ( + +

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

+
+ ) : ( + + + +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_READ_MORE}

+
+
+
+ )} +
+ + {isUploadMore ? ( + + {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} + + ) : ( + + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} + + )} + +
+
+ ); +}); +UploadRulesPanel.displayName = 'UploadRulesPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts new file mode 100644 index 0000000000000..82446ba308402 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.styles.ts @@ -0,0 +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. + */ + +import { css } from '@emotion/css'; +import { useEuiTheme } from '@elastic/eui'; + +export const TITLE_CLASS_NAME = 'siemMigrationsStartTitle'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + return css` + .${TITLE_CLASS_NAME} { + font-weight: ${euiTheme.font.weight.semiBold}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx new file mode 100644 index 0000000000000..c1e7539c8e101 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { OnboardingCardId } from '../../../../../constants'; +import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import { useLatestStats } from '../../../../../../siem_migrations/rules/service/hooks/use_latest_stats'; +import { MigrationDataInputFlyout } from '../../../../../../siem_migrations/rules/components/data_input_flyout'; +import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; +import type { OnboardingCardComponent } from '../../../../../types'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { UploadRulesPanels } from './upload_rules_panels'; +import { StartMigrationContextProvider } from './context'; +import { useStyles } from './start_migration_card.styles'; +import * as i18n from './translations'; +import { MissingAIConnectorCallout } from './missing_ai_connector_callout'; + +export const StartMigrationCard: OnboardingCardComponent = React.memo( + ({ checkComplete, isCardComplete, setExpandedCardId }) => { + const styles = useStyles(); + const { data: migrationsStats, isLoading } = useLatestStats(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(); + const [flyoutMigrationStats, setFlyoutMigrationStats] = useState< + RuleMigrationTaskStats | undefined + >(); + + const closeFlyout = useCallback(() => { + setIsFlyoutOpen(false); + setFlyoutMigrationStats(undefined); + if (!isCardComplete(OnboardingCardId.siemMigrationsStart)) { + checkComplete(); + } + }, [checkComplete, isCardComplete]); + + const openFlyout = useCallback((migrationStats?: RuleMigrationTaskStats) => { + setFlyoutMigrationStats(migrationStats); + setIsFlyoutOpen(true); + }, []); + + if (!isCardComplete(OnboardingCardId.siemMigrationsAiConnectors)) { + return ( + + setExpandedCardId(OnboardingCardId.siemMigrationsAiConnectors) + } + /> + ); + } + + return ( + + + {isLoading ? ( + + ) : ( + + )} + + +

{i18n.START_MIGRATION_CARD_FOOTER_NOTE}

+
+
+ {isFlyoutOpen && ( + + )} +
+ ); + } +); +StartMigrationCard.displayName = 'StartMigrationCard'; + +// eslint-disable-next-line import/no-default-export +export default StartMigrationCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts new file mode 100644 index 0000000000000..41e65352d4bc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { OnboardingCardCheckComplete } from '../../../../../types'; + +export const checkStartMigrationCardComplete: OnboardingCardCheckComplete = async ({ + siemMigrations, +}) => { + const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); + const isComplete = migrationsStats.length > 0; + return isComplete; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts new file mode 100644 index 0000000000000..bdb3f31842549 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -0,0 +1,118 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const START_MIGRATION_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.title', + { defaultMessage: 'Translate your existing SIEM Rules to Elastic' } +); +export const START_MIGRATION_CARD_FOOTER_NOTE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.footerNote', + { + defaultMessage: + 'Splunk and related marks are trademarks or registered trademarks of Splunk LLC in the United States and other countries.', + } +); +export const START_MIGRATION_CARD_CONNECTOR_MISSING_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', + { + defaultMessage: 'Rule migrations require an AI connector to be configured.', + } +); +export const START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', + { defaultMessage: 'AI provider step' } +); + +export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.title', + { defaultMessage: 'Export your Splunk® SIEM rules to start translation.' } +); + +export const START_MIGRATION_CARD_UPLOAD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.description', + { + defaultMessage: + 'Upload your rules before importing data to identify the integrations, data streams, and available details of your SIEM rules. Click “Upload Rules” to view step-by-step instructions to export and uploading the rules.', + } +); + +export const START_MIGRATION_CARD_UPLOAD_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.button', + { defaultMessage: 'Upload rules' } +); + +export const START_MIGRATION_CARD_UPLOAD_MORE_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.uploadMore.title', + { defaultMessage: 'Need to migrate more rules?' } +); +export const START_MIGRATION_CARD_UPLOAD_MORE_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.uploadMore.button', + { defaultMessage: 'Upload more rules' } +); + +export const START_MIGRATION_CARD_UPLOAD_READ_MORE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.readMore', + { defaultMessage: 'Read more about our AI powered translations and other features.' } +); + +export const START_MIGRATION_CARD_UPLOAD_READ_DOCS = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.upload.readAiDocsLink', + { defaultMessage: 'Read AI docs' } +); + +export const START_MIGRATION_CARD_MIGRATION_READY_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.ready.description', + { + defaultMessage: + 'Migration is created and ready but the translation has not started yet. You can either upload macros & lookups or start the translation process', + } +); +export const START_MIGRATION_CARD_TRANSLATE_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.translate.button', + { defaultMessage: 'Start translation' } +); +export const START_MIGRATION_CARD_UPLOAD_MACROS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.uploadMacros.button', + { defaultMessage: 'Upload macros' } +); + +export const START_MIGRATION_CARD_MIGRATION_TITLE = (number: number) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.migrationTitle', { + defaultMessage: 'SIEM rules migration #{number}', + values: { number }, + }); + +export const START_MIGRATION_CARD_PROGRESS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.progress.description', + { + defaultMessage: `This may take a few minutes & the task will work in the background. Just stay logged in and we'll notify you when done.`, + } +); + +export const START_MIGRATION_CARD_RESULT_TITLE = (number: number) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.result.title', { + defaultMessage: 'SIEM rules migration #{number} complete', + values: { number }, + }); + +export const START_MIGRATION_CARD_RESULT_DESCRIPTION = (createdAt: string, finishedAt: string) => + i18n.translate('xpack.securitySolution.onboarding.startMigration.result.description', { + defaultMessage: 'Export uploaded on {createdAt} and translation finished {finishedAt}.', + values: { createdAt, finishedAt }, + }); + +export const VIEW_TRANSLATED_RULES_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.result.translatedRules.title', + { defaultMessage: 'Translation Summary' } +); + +export const VIEW_TRANSLATED_RULES_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.startMigration.result.translatedRules.button', + { defaultMessage: 'View translated rules' } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx new file mode 100644 index 0000000000000..95b53d921fd1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panels.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; +import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; +import { UploadRulesPanel } from './panels/upload_rules_panel'; +import { MigrationProgressPanel } from './panels/migration_progress_panel'; +import { MigrationResultPanel } from './panels/migration_result_panel'; +import { MigrationReadyPanel } from './panels/migration_ready_panel'; + +export interface UploadRulesPanelsProps { + migrationsStats: RuleMigrationStats[]; +} +export const UploadRulesPanels = React.memo(({ migrationsStats }) => { + if (migrationsStats.length === 0) { + return ; + } + + return ( + + + + + {migrationsStats.map((migrationStats) => ( + + {migrationStats.status === SiemMigrationTaskStatus.READY && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.RUNNING && ( + + )} + {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( + + )} + + ))} + + ); +}); +UploadRulesPanels.displayName = 'UploadRulesPanels'; diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index e360e4591bb37..94b87721513bc 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -21,4 +21,5 @@ export enum OnboardingCardId { // siem_migrations topic cards siemMigrationsAiConnectors = 'ai_connectors', + siemMigrationsStart = 'start', } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx new file mode 100644 index 0000000000000..c0528a9a04afe --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 SiemMigrationsIconSVG from './siem_migrations.svg'; +export const SiemMigrationsIcon = SiemMigrationsIconSVG; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg new file mode 100644 index 0000000000000..e8568a943f70c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 592b93f438197..db6f0117d4a77 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -7,161 +7,188 @@ import { replaceParams } from '@kbn/openapi-common/shared'; +import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; import { KibanaServices } from '../../../common/lib/kibana'; import { + SIEM_RULE_MIGRATIONS_PATH, SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, SIEM_RULE_MIGRATION_INSTALL_PATH, SIEM_RULE_MIGRATION_PATH, SIEM_RULE_MIGRATION_START_PATH, + SIEM_RULE_MIGRATION_STATS_PATH, SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, } from '../../../../common/siem_migrations/constants'; import type { + CreateRuleMigrationRequestBody, + CreateRuleMigrationResponse, GetAllStatsRuleMigrationResponse, GetRuleMigrationResponse, GetRuleMigrationTranslationStatsResponse, InstallTranslatedMigrationRulesResponse, InstallMigrationRulesResponse, StartRuleMigrationRequestBody, + GetRuleMigrationStatsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; -/** - * Retrieves the stats for all the existing migrations, aggregated by `migration_id`. - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ +export interface GetRuleMigrationStatsParams { + /** `id` of the migration to get stats for */ + migrationId: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves the stats for all the existing migrations, aggregated by `migration_id`. */ +export const getRuleMigrationStats = async ({ + migrationId, + signal, +}: GetRuleMigrationStatsParams): Promise => { + return KibanaServices.get().http.get( + replaceParams(SIEM_RULE_MIGRATION_STATS_PATH, { migration_id: migrationId }), + { version: '1', signal } + ); +}; + +export interface GetRuleMigrationsStatsAllParams { + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves the stats for all the existing migrations, aggregated by `migration_id`. */ export const getRuleMigrationsStatsAll = async ({ signal, -}: { - signal: AbortSignal | undefined; -}): Promise => { - return KibanaServices.get().http.fetch( +}: GetRuleMigrationsStatsAllParams = {}): Promise => { + return KibanaServices.get().http.get( SIEM_RULE_MIGRATIONS_ALL_STATS_PATH, - { method: 'GET', version: '1', signal } + { version: '1', signal } ); }; -/** - * Starts a new migration with the provided rules. - * - * @param migrationId `id` of the migration to start - * @param body The body containing the `connectorId` to use for the migration - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const startRuleMigration = async ({ +export interface CreateRuleMigrationParams { + /** Optional `id` of migration to add the rules to. + * The id is necessary only for batching the migration creation in multiple requests */ + migrationId?: string; + /** The body containing the `connectorId` to use for the migration */ + body: CreateRuleMigrationRequestBody; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Starts a new migration with the provided rules. */ +export const createRuleMigration = async ({ migrationId, body, signal, -}: { - migrationId: string; - body: StartRuleMigrationRequestBody; - signal: AbortSignal | undefined; -}): Promise => { - return KibanaServices.get().http.put( - replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), +}: CreateRuleMigrationParams): Promise => { + return KibanaServices.get().http.post( + `${SIEM_RULE_MIGRATIONS_PATH}${migrationId ? `/${migrationId}` : ''}`, { body: JSON.stringify(body), version: '1', signal } ); }; -/** - * Retrieves the translation stats for the migraion. - * - * @param migrationId `id` of the migration to retrieve translation stats for - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRuleMigrationTranslationStats = async ({ +export interface StartRuleMigrationParams { + /** `id` of the migration to start */ + migrationId: string; + /** The connector id to use for the migration */ + connectorId: string; + /** Optional LangSmithOptions to use for the for the migration */ + langSmithOptions?: LangSmithOptions; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Starts a new migration with the provided rules. */ +export const startRuleMigration = async ({ migrationId, + connectorId, + langSmithOptions, signal, -}: { - migrationId: string; - signal: AbortSignal | undefined; -}): Promise => { - return KibanaServices.get().http.fetch( - replaceParams(SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, { migration_id: migrationId }), - { - method: 'GET', - version: '1', - signal, - } +}: StartRuleMigrationParams): Promise => { + const body: StartRuleMigrationRequestBody = { connector_id: connectorId }; + if (langSmithOptions) { + body.langsmith_options = langSmithOptions; + } + return KibanaServices.get().http.put( + replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), + { body: JSON.stringify(body), version: '1', signal } ); }; -/** - * Retrieves all the migration rule documents of a specific migration. - * - * @param migrationId `id` of the migration to retrieve rule documents for - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ +export interface GetRuleMigrationParams { + /** `id` of the migration to get rules documents for */ + migrationId: string; + /** Optional page number to retrieve */ + page?: number; + /** Optional number of documents per page to retrieve */ + perPage?: number; + /** Optional search term to filter documents */ + searchTerm?: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Retrieves all the migration rule documents of a specific migration. */ export const getRuleMigrations = async ({ migrationId, page, perPage, searchTerm, signal, -}: { - migrationId: string; - page?: number; - perPage?: number; - searchTerm?: string; - signal: AbortSignal | undefined; -}): Promise => { - return KibanaServices.get().http.fetch( +}: GetRuleMigrationParams): Promise => { + return KibanaServices.get().http.get( replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }), - { - method: 'GET', - version: '1', - query: { - page, - per_page: perPage, - search_term: searchTerm, - }, - signal, - } + { version: '1', query: { page, per_page: perPage, search_term: searchTerm }, signal } ); }; -export const installMigrationRules = async ({ +export interface GetRuleMigrationTranslationStatsParams { + /** `id` of the migration to get translation stats for */ + migrationId: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** + * Retrieves the translation stats for the migration. + */ +export const getRuleMigrationTranslationStats = async ({ migrationId, - ids, signal, -}: { +}: GetRuleMigrationTranslationStatsParams): Promise => { + return KibanaServices.get().http.get( + replaceParams(SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH, { migration_id: migrationId }), + { version: '1', signal } + ); +}; + +export interface InstallRulesParams { + /** `id` of the migration to install rules for */ migrationId: string; + /** The rule ids to install */ ids: string[]; + /** Optional AbortSignal for cancelling request */ signal?: AbortSignal; -}): Promise => { - return KibanaServices.get().http.fetch( +} +/** Installs the provided rule ids for a specific migration. */ +export const installMigrationRules = async ({ + migrationId, + ids, + signal, +}: InstallRulesParams): Promise => { + return KibanaServices.get().http.post( replaceParams(SIEM_RULE_MIGRATION_INSTALL_PATH, { migration_id: migrationId }), - { - method: 'POST', - version: '1', - body: JSON.stringify(ids), - signal, - } + { version: '1', body: JSON.stringify(ids), signal } ); }; +export interface InstallTranslatedRulesParams { + /** `id` of the migration to install rules for */ + migrationId: string; + /** Optional AbortSignal for cancelling request */ + signal?: AbortSignal; +} +/** Installs all the translated rules for a specific migration. */ export const installTranslatedMigrationRules = async ({ migrationId, signal, -}: { - migrationId: string; - signal?: AbortSignal; -}): Promise => { - return KibanaServices.get().http.fetch( +}: InstallTranslatedRulesParams): Promise => { + return KibanaServices.get().http.post( replaceParams(SIEM_RULE_MIGRATION_INSTALL_TRANSLATED_PATH, { migration_id: migrationId }), - { - method: 'POST', - version: '1', - signal, - } + { version: '1', signal } ); }; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts new file mode 100644 index 0000000000000..aa331bf17c832 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/constants.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export enum DataInputStep { + rules = 'rules', + macros = 'macros', + lookups = 'lookups', +} + +export const SPL_RULES_COLUMNS = [ + 'id', + 'title', + 'search', + 'description', + 'action.escu.eli5', + 'action.correlationsearch.annotations', +] as const; + +export const RULES_SPLUNK_QUERY = `| rest /servicesNS/-/-/saved/searches +| search action.correlationsearch.enabled = "1" OR (eai:acl.app = "Splunk_Security_Essentials" AND is_scheduled=1) +| where disabled=0 +| table ${SPL_RULES_COLUMNS.join(', ')}`; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx new file mode 100644 index 0000000000000..6a4916a5e54b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { + EuiFlyoutResizable, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import { DataInputStep } from './constants'; +import { RulesDataInput } from './steps/rules/rules_data_input'; +import { useStartMigration } from '../../service/hooks/use_start_migration'; + +export interface MigrationDataInputFlyoutProps { + onClose: () => void; + migrationStats?: RuleMigrationTaskStats; +} +export const MigrationDataInputFlyout = React.memo( + ({ onClose, migrationStats: initialMigrationSats }) => { + const [migrationStats, setMigrationStats] = useState( + initialMigrationSats + ); + + const { startMigration, isLoading: isStartLoading } = useStartMigration(onClose); + const onStartMigration = useCallback(() => { + if (migrationStats?.id) { + startMigration(migrationStats.id); + } + }, [migrationStats, startMigration]); + + const [dataInputStep, setDataInputStep] = useState(() => { + if (migrationStats) { + return DataInputStep.macros; + } + return DataInputStep.rules; + }); + + const onMigrationCreated = useCallback( + (createdMigrationStats: RuleMigrationTaskStats) => { + if (createdMigrationStats) { + setMigrationStats(createdMigrationStats); + setDataInputStep(DataInputStep.macros); + } + }, + [setDataInputStep] + ); + + return ( + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + +
+ ); + } +); +MigrationDataInputFlyout.displayName = 'MigrationDataInputFlyout'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts new file mode 100644 index 0000000000000..709623f992f72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { MigrationDataInputFlyout, type MigrationDataInputFlyoutProps } from './data_input_flyout'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx new file mode 100644 index 0000000000000..438134b0ad99a --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/sub_step_wrapper.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +const style = css` + .euiStep__title { + font-size: 14px; + } +`; + +export const SubStepWrapper = React.memo>(({ children }) => { + return ( + + {children} + + ); +}); +SubStepWrapper.displayName = 'SubStepWrapper'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx new file mode 100644 index 0000000000000..2b20dcda0cea7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/rules_data_input.tsx @@ -0,0 +1,95 @@ +/* + * 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 { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiStepNumber, + EuiSteps, + EuiTitle, +} from '@elastic/eui'; +import React, { useMemo, useState } from 'react'; +import { SubStepWrapper } from '../common/sub_step_wrapper'; +import type { OnMigrationCreated } from '../../types'; +import { useCopyExportQueryStep } from './sub_steps/copy_export_query'; +import { useRulesFileUploadStep } from './sub_steps/rules_file_upload'; +import * as i18n from './translations'; +import { useCheckResourcesStep } from './sub_steps/check_resources'; + +type Step = 1 | 2 | 3 | 4; +const getStatus = (step: Step, currentStep: Step): EuiStepStatus => { + if (step === currentStep) { + return 'current'; + } + if (step < currentStep) { + return 'complete'; + } + return 'incomplete'; +}; + +interface RulesDataInputProps { + selected: boolean; + onMigrationCreated: OnMigrationCreated; +} + +export const RulesDataInput = React.memo( + ({ selected, onMigrationCreated }) => { + const [step, setStep] = useState(1); + + const copyStep = useCopyExportQueryStep({ + status: getStatus(1, step), + onCopied: () => setStep(2), + }); + + const uploadStep = useRulesFileUploadStep({ + status: getStatus(2, step), + onMigrationCreated: (stats) => { + onMigrationCreated(stats); + setStep(3); + }, + }); + + const resourcesStep = useCheckResourcesStep({ + status: getStatus(3, step), + onComplete: () => { + setStep(4); + }, + }); + + const steps = useMemo( + () => [copyStep, uploadStep, resourcesStep], + [copyStep, uploadStep, resourcesStep] + ); + + return ( + + + + + + + + + + {i18n.RULES_DATA_INPUT_TITLE} + + + + + + + + + + + + ); + } +); +RulesDataInput.displayName = 'RulesDataInput'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx new file mode 100644 index 0000000000000..3b081eb203267 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, type EuiStepProps, type EuiStepStatus } from '@elastic/eui'; +import * as i18n from './translations'; + +export interface CheckResourcesStepProps { + status: EuiStepStatus; + onComplete: () => void; +} +export const useCheckResourcesStep = ({ + status, + onComplete, +}: CheckResourcesStepProps): EuiStepProps => { + // onComplete(); // TODO: check the resources + return { + title: i18n.RULES_DATA_INPUT_CHECK_RESOURCES_TITLE, + status, + children: ( + + {i18n.RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION} + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts new file mode 100644 index 0000000000000..159b4033fafd6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/check_resources/translations.ts @@ -0,0 +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. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_CHECK_RESOURCES_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.title', + { defaultMessage: 'Check for macros and lookups' } +); + +export const RULES_DATA_INPUT_CHECK_RESOURCES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.checkResources.description', + { + defaultMessage: `For best translation results, we will automatically review your rules for macros and lookups and ask you to upload them. Once uploaded, we'll be able to deliver a more complete rule translation for all rules using those macros or lookups.`, + } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx new file mode 100644 index 0000000000000..11fb88a1cade2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/copy_export_query.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RULES_SPLUNK_QUERY } from '../../../../constants'; +import * as i18n from './translations'; + +interface CopyExportQueryProps { + onCopied: () => void; +} +export const CopyExportQuery = React.memo(({ onCopied }) => { + const onClick: React.MouseEventHandler = useCallback( + (ev) => { + // The only button inside the element is the "copy" button. + if ((ev.target as Element).tagName === 'BUTTON') { + onCopied(); + } + }, + [onCopied] + ); + + return ( + <> + {/* The click event is also dispatched when using the keyboard actions (space or enter) for "copy" button. + No need to use keyboard specific events, disabling the a11y lint rule:*/} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
+ {/* onCopy react event is dispatched when the user copies text manually */} + + {RULES_SPLUNK_QUERY} + +
+ + + {i18n.RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION}, + format: {'JSON'}, + }} + /> + + + ); +}); +CopyExportQuery.displayName = 'CopyExportQuery'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx new file mode 100644 index 0000000000000..3d2adcc78857b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import { CopyExportQuery } from './copy_export_query'; +import * as i18n from './translations'; + +export interface CopyExportQueryStepProps { + status: EuiStepStatus; + onCopied: () => void; +} +export const useCopyExportQueryStep = ({ + status, + onCopied, +}: CopyExportQueryStepProps): EuiStepProps => { + return { + title: i18n.RULES_DATA_INPUT_COPY_TITLE, + status, + children: , + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts new file mode 100644 index 0000000000000..d76eb71f2e378 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/copy_export_query/translations.ts @@ -0,0 +1,18 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_COPY_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.title', + { defaultMessage: 'Copy and export query' } +); + +export const RULES_DATA_INPUT_COPY_DESCRIPTION_SECTION = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.copyExportQuery.description.section', + { defaultMessage: 'Search and Reporting' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx new file mode 100644 index 0000000000000..ab7838b28908b --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import type { EuiStepProps, EuiStepStatus } from '@elastic/eui'; +import type { OnMigrationCreated } from '../../../../types'; +import { RulesFileUpload } from './rules_file_upload'; +import { + useCreateMigration, + type OnSuccess, +} from '../../../../../../service/hooks/use_create_migration'; +import * as i18n from './translations'; + +export interface RulesFileUploadStepProps { + status: EuiStepStatus; + onMigrationCreated: OnMigrationCreated; +} +export const useRulesFileUploadStep = ({ + status, + onMigrationCreated, +}: RulesFileUploadStepProps): EuiStepProps => { + const [isCreated, setIsCreated] = useState(false); + const onSuccess = useCallback( + (stats) => { + setIsCreated(true); + onMigrationCreated(stats); + }, + [onMigrationCreated] + ); + const { createMigration, isLoading, error } = useCreateMigration(onSuccess); + + const uploadStepStatus = useMemo(() => { + if (isLoading) { + return 'loading'; + } + if (error) { + return 'danger'; + } + return status; + }, [isLoading, error, status]); + + return { + title: i18n.RULES_DATA_INPUT_FILE_UPLOAD_TITLE, + status: uploadStepStatus, + children: ( + + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts new file mode 100644 index 0000000000000..3d5dbb32ccde8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/parse_rules_file.ts @@ -0,0 +1,79 @@ +/* + * 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 { isPlainObject } from 'lodash/fp'; +import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { SPL_RULES_COLUMNS } from '../../../../constants'; +import * as i18n from './translations'; + +type SplunkResult = Partial>; +interface SplunkRow { + result: SplunkResult; +} + +export const parseContent = (fileContent: string): OriginalRule[] => { + const trimmedContent = fileContent.trim(); + let arrayContent: SplunkRow[]; + if (trimmedContent.startsWith('[')) { + arrayContent = parseJSONArray(trimmedContent); + } else { + arrayContent = parseNDJSON(trimmedContent); + } + if (arrayContent.length === 0) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.EMPTY); + } + return arrayContent.map(convertFormat); +}; + +const parseNDJSON = (fileContent: string): SplunkRow[] => { + return fileContent + .split(/\n(?=\{)/) // split at newline followed by '{'. + .filter((entry) => entry.trim() !== '') // Remove empty entries. + .map(parseJSON); // Parse each entry as JSON. +}; + +const parseJSONArray = (fileContent: string): SplunkRow[] => { + const parsedContent = parseJSON(fileContent); + if (!Array.isArray(parsedContent)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_ARRAY); + } + return parsedContent; +}; + +const parseJSON = (fileContent: string) => { + try { + return JSON.parse(fileContent); + } catch (error) { + if (error instanceof RangeError) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + } + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_PARSE); + } +}; + +const convertFormat = (row: SplunkRow): OriginalRule => { + if (!isPlainObject(row) || !isPlainObject(row.result)) { + throw new Error(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.NOT_OBJECT); + } + const originalRule: Partial = { + id: row.result.id, + vendor: 'splunk', + title: row.result.title, + query: row.result.search, + query_language: 'spl', + description: row.result['action.escu.eli5']?.trim() || row.result.description, + }; + + if (row.result['action.correlationsearch.annotations']) { + try { + originalRule.annotations = JSON.parse(row.result['action.correlationsearch.annotations']); + } catch (error) { + delete originalRule.annotations; + } + } + return originalRule as OriginalRule; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx new file mode 100644 index 0000000000000..0f9787a4ddf68 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; +import { parseContent } from './parse_rules_file'; +import * as i18n from './translations'; + +export interface RulesFileUploadProps { + createMigration: CreateMigration; + apiError?: string; + isLoading?: boolean; + isCreated?: boolean; +} +export const RulesFileUpload = React.memo( + ({ createMigration, apiError, isLoading, isCreated }) => { + const [isParsing, setIsParsing] = useState(false); + const [fileError, setFileError] = useState(); + + const onChangeFile = useCallback( + (files: FileList | null) => { + if (!files) { + return; + } + + setFileError(undefined); + + const rulesFile = files[0]; + const reader = new FileReader(); + + reader.onloadstart = () => setIsParsing(true); + reader.onloadend = () => setIsParsing(false); + + reader.onload = function (e) { + // We can safely cast to string since we call `readAsText` to load the file. + const fileContent = e.target?.result as string | undefined; + + if (fileContent == null) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && e.loaded > 100000) { + // V8-based browsers can't handle large files and return an empty string + // instead of an error; see https://stackoverflow.com/a/61316641 + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + let data: OriginalRule[]; + try { + data = parseContent(fileContent); + createMigration(data); + } catch (err) { + setFileError(err.message); + } + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setFileError(i18n.RULES_DATA_INPUT_FILE_UPLOAD_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(rulesFile); + }, + [createMigration] + ); + + const error = useMemo(() => { + if (apiError) { + return apiError; + } + return fileError; + }, [apiError, fileError]); + + return ( + + {error} + + } + isInvalid={error != null} + fullWidth + > + + + {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + } + accept="application/json" + onChange={onChangeFile} + display="large" + aria-label="Upload logs sample file" + isLoading={isParsing || isLoading} + disabled={isLoading || isCreated} + data-test-subj="rulesFilePicker" + data-loading={isParsing} + /> + + ); + } +); +RulesFileUpload.displayName = 'RulesFileUpload'; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts new file mode 100644 index 0000000000000..675eed61f4973 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/translations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_FILE_UPLOAD_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.title', + { defaultMessage: 'Update your rule export' } +); +export const RULES_DATA_INPUT_FILE_UPLOAD_PROMPT = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.prompt', + { defaultMessage: 'Select or drag and drop the exported JSON file' } +); + +export const RULES_DATA_INPUT_FILE_UPLOAD_ERROR = { + CAN_NOT_READ: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotRead', + { defaultMessage: 'Failed to read the rules export file' } + ), + CAN_NOT_READ_WITH_REASON: (reason: string) => + i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotReadWithReason', + { + defaultMessage: 'An error occurred when reading rules export file: {reason}', + values: { reason }, + } + ), + CAN_NOT_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.canNotParse', + { defaultMessage: 'Cannot parse the rules export file as either a JSON file' } + ), + TOO_LARGE_TO_PARSE: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.tooLargeToParse', + { defaultMessage: 'This rules export file is too large to parse' } + ), + NOT_ARRAY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notArray', + { defaultMessage: 'The rules export file is not an array' } + ), + EMPTY: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.empty', + { defaultMessage: 'The rules export file is empty' } + ), + NOT_OBJECT: i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.notObject', + { defaultMessage: 'The rules export file contains non-object entries' } + ), + WRONG_FORMAT: (formatError: string) => { + return i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.error.wrongFormat', + { + defaultMessage: 'The rules export file has wrong format: {formatError}', + values: { formatError }, + } + ); + }, +}; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createError', + { defaultMessage: 'Failed to upload rules file' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts new file mode 100644 index 0000000000000..5446180d03a75 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/translations.ts @@ -0,0 +1,13 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_TITLE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.title', + { defaultMessage: 'Upload rule export and check for macros and lookups' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts new file mode 100644 index 0000000000000..16d8f60043bcb --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/types.ts @@ -0,0 +1,10 @@ +/* + * 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 { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; + +export type OnMigrationCreated = (migrationStats: RuleMigrationTaskStats) => void; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx index 728873f046d2e..3f255a49f87c2 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/components/header_buttons/index.tsx @@ -10,13 +10,13 @@ import React, { useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as i18n from './translations'; -import type { RuleMigrationTask } from '../../types'; +import type { RuleMigrationStats } from '../../types'; export interface HeaderButtonsProps { /** * Available rule migrations stats */ - ruleMigrationsStats: RuleMigrationTask[]; + ruleMigrationsStats: RuleMigrationStats[]; /** * Selected rule migration id diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index 018aa5d77559e..3877a6f46cbe7 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -21,7 +21,7 @@ import { NeedAdminForUpdateRulesCallOut } from '../../../detections/components/c import { MissingPrivilegesCallOut } from '../../../detections/components/callouts/missing_privileges_callout'; import { HeaderButtons } from '../components/header_buttons'; import { UnknownMigration } from '../components/unknown_migration'; -import { useLatestStats } from '../hooks/use_latest_stats'; +import { useLatestStats } from '../service/hooks/use_latest_stats'; type MigrationRulesPageProps = RouteComponentProps<{ migrationId?: string }>; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts new file mode 100644 index 0000000000000..a68432d48bf9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/common/api_request_reducer.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +export interface State { + loading: boolean; + error: Error | null; +} +export type Action = { type: 'start' } | { type: 'error'; error: Error } | { type: 'success' }; + +export const initialState: State = { loading: false, error: null }; +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'start': + return { loading: true, error: null }; + case 'error': + return { loading: false, error: action.error }; + case 'success': + return { loading: false, error: null }; + default: + return state; + } +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts new file mode 100644 index 0000000000000..936bc07e6576e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/translations.ts @@ -0,0 +1,17 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.rules.rulesFileUpload.createError', + { defaultMessage: 'Failed to upload rules file' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts new file mode 100644 index 0000000000000..94082cf59d359 --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_create_migration.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { RuleMigrationTaskStats } from '../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { CreateRuleMigrationRequestBody } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleSuccess', + { defaultMessage: 'Rules uploaded successfully' } +); +export const RULES_DATA_INPUT_CREATE_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.createRuleError', + { defaultMessage: 'Failed to upload rules file' } +); + +export type CreateMigration = (data: CreateRuleMigrationRequestBody) => void; +export type OnSuccess = (migrationStats: RuleMigrationTaskStats) => void; + +export const useCreateMigration = (onSuccess: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const createMigration = useCallback( + (data) => { + (async () => { + try { + dispatch({ type: 'start' }); + const migrationId = await siemMigrations.rules.createRuleMigration(data); + const stats = await siemMigrations.rules.getRuleMigrationStats(migrationId); + + notifications.toasts.addSuccess(RULES_DATA_INPUT_CREATE_MIGRATION_SUCCESS); + onSuccess(stats); + dispatch({ type: 'success' }); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_CREATE_MIGRATION_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, createMigration }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts similarity index 91% rename from x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts rename to x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts index c681af0d2a21c..8b692f07eb3cb 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/hooks/use_latest_stats.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_latest_stats.ts @@ -7,7 +7,7 @@ import useObservable from 'react-use/lib/useObservable'; import { useEffect, useMemo } from 'react'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; export const useLatestStats = () => { const { siemMigrations } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts new file mode 100644 index 0000000000000..6794439d1298e --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useReducer } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { reducer, initialState } from './common/api_request_reducer'; + +export const RULES_DATA_INPUT_START_MIGRATION_SUCCESS = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.startMigrationSuccess', + { defaultMessage: 'Migration started successfully.' } +); +export const RULES_DATA_INPUT_START_MIGRATION_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.service.startMigrationError', + { defaultMessage: 'Error starting migration.' } +); + +export type StartMigration = (migrationId: string) => void; +export type OnSuccess = () => void; + +export const useStartMigration = (onSuccess?: OnSuccess) => { + const { siemMigrations, notifications } = useKibana().services; + const [state, dispatch] = useReducer(reducer, initialState); + + const startMigration = useCallback( + (migrationId) => { + (async () => { + try { + dispatch({ type: 'start' }); + await siemMigrations.rules.startRuleMigration(migrationId); + + notifications.toasts.addSuccess(RULES_DATA_INPUT_START_MIGRATION_SUCCESS); + dispatch({ type: 'success' }); + onSuccess?.(); + } catch (err) { + const apiError = err.body ?? err; + notifications.toasts.addError(apiError, { + title: RULES_DATA_INPUT_START_MIGRATION_ERROR, + }); + dispatch({ type: 'error', error: apiError }); + } + })(); + }, + [siemMigrations.rules, notifications.toasts, onSuccess] + ); + + return { isLoading: state.loading, error: state.error, startMigration }; +}; diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 3162cc3d58e63..c13b0606d771d 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -7,28 +7,55 @@ import { BehaviorSubject, type Observable } from 'rxjs'; import type { CoreStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; +import type { TraceOptions } from '@kbn/elastic-assistant/impl/assistant/types'; +import { + DEFAULT_ASSISTANT_NAMESPACE, + TRACE_OPTIONS_SESSION_STORAGE_KEY, +} from '@kbn/elastic-assistant/impl/assistant_context/constants'; +import type { LangSmithOptions } from '../../../../common/siem_migrations/model/common.gen'; +import type { RuleMigrationTaskStats } from '../../../../common/siem_migrations/model/rule_migration.gen'; +import type { + CreateRuleMigrationRequestBody, + GetAllStatsRuleMigrationResponse, + GetRuleMigrationStatsResponse, +} from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SiemMigrationTaskStatus } from '../../../../common/siem_migrations/constants'; import type { StartPluginsDependencies } from '../../../types'; import { ExperimentalFeaturesService } from '../../../common/experimental_features_service'; import { licenseService } from '../../../common/hooks/use_license'; -import { getRuleMigrationsStatsAll, startRuleMigration } from '../api'; -import type { RuleMigrationTask } from '../types'; +import { + createRuleMigration, + getRuleMigrationStats, + getRuleMigrationsStatsAll, + startRuleMigration, + type GetRuleMigrationsStatsAllParams, +} from '../api'; +import type { RuleMigrationStats } from '../types'; import { getSuccessToast } from './success_notification'; import { RuleMigrationsStorage } from './storage'; +import * as i18n from './translations'; + +// use the default assistant namespace since it's the only one we use +const NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY = + `${DEFAULT_ASSISTANT_NAMESPACE}.${TRACE_OPTIONS_SESSION_STORAGE_KEY}` as const; + +const REQUEST_POLLING_INTERVAL_MS = 5000 as const; +const CREATE_MIGRATION_BODY_BATCH_SIZE = 50 as const; export class SiemRulesMigrationsService { - private readonly pollingInterval = 5000; - private readonly latestStats$: BehaviorSubject; - private readonly signal = new AbortController().signal; + private readonly latestStats$: BehaviorSubject; private isPolling = false; - public connectorIdStorage = new RuleMigrationsStorage('connectorId'); + public connectorIdStorage = new RuleMigrationsStorage('connectorId'); + public traceOptionsStorage = new RuleMigrationsStorage('traceOptions', { + customKey: NAMESPACE_TRACE_OPTIONS_SESSION_STORAGE_KEY, + storageType: 'session', + }); constructor( private readonly core: CoreStart, private readonly plugins: StartPluginsDependencies ) { - this.latestStats$ = new BehaviorSubject([]); + this.latestStats$ = new BehaviorSubject([]); this.plugins.spaces.getActiveSpace().then((space) => { this.connectorIdStorage.setSpaceId(space.id); @@ -36,7 +63,7 @@ export class SiemRulesMigrationsService { }); } - public getLatestStats$(): Observable { + public getLatestStats$(): Observable { return this.latestStats$.asObservable(); } @@ -48,27 +75,92 @@ export class SiemRulesMigrationsService { if (this.isPolling || !this.isAvailable()) { return; } - this.isPolling = true; - this.startStatsPolling() + this.startTaskStatsPolling() .catch((e) => { - this.core.notifications.toasts.addError(e, { - title: i18n.translate( - 'xpack.securitySolution.siemMigrations.rulesService.polling.errorTitle', - { defaultMessage: 'Error fetching rule migrations' } - ), - }); + this.core.notifications.toasts.addError(e, { title: i18n.POLLING_ERROR }); }) .finally(() => { this.isPolling = false; }); } - private async startStatsPolling(): Promise { + public async createRuleMigration(body: CreateRuleMigrationRequestBody): Promise { + if (body.length === 0) { + throw new Error(i18n.EMPTY_RULES_ERROR); + } + // Batching creation to avoid hitting the max payload size limit of the API + let migrationId: string | undefined; + for (let i = 0; i < body.length; i += CREATE_MIGRATION_BODY_BATCH_SIZE) { + const bodyBatch = body.slice(i, i + CREATE_MIGRATION_BODY_BATCH_SIZE); + const response = await createRuleMigration({ migrationId, body: bodyBatch }); + migrationId = response.migration_id; + } + return migrationId as string; + } + + public async startRuleMigration(migrationId: string): Promise { + const connectorId = this.connectorIdStorage.get(); + if (!connectorId) { + throw new Error(i18n.MISSING_CONNECTOR_ERROR); + } + + const langSmithSettings = this.traceOptionsStorage.get(); + let langSmithOptions: LangSmithOptions | undefined; + if (langSmithSettings) { + langSmithOptions = { + project_name: langSmithSettings.langSmithProject, + api_key: langSmithSettings.langSmithApiKey, + }; + } + + const result = await startRuleMigration({ migrationId, connectorId, langSmithOptions }); + this.startPolling(); + return result; + } + + public async getRuleMigrationStats(migrationId: string): Promise { + return getRuleMigrationStats({ migrationId }); + } + + public async getRuleMigrationsStats( + params: GetRuleMigrationsStatsAllParams = {} + ): Promise { + const allStats = await this.getRuleMigrationsStatsWithRetry(params); + const results = allStats.map( + // the array order (by creation) is guaranteed by the API + (stats, index) => ({ ...stats, number: index + 1 } as RuleMigrationStats) // needs cast because of the `status` enum override + ); + this.latestStats$.next(results); // Always update the latest stats + return results; + } + + private async getRuleMigrationsStatsWithRetry( + params: GetRuleMigrationsStatsAllParams = {}, + sleepSecs?: number + ): Promise { + if (sleepSecs) { + await new Promise((resolve) => setTimeout(resolve, sleepSecs * 1000)); + } + + return getRuleMigrationsStatsAll(params).catch((e) => { + // Retry only on network errors (no status) and 503s, otherwise throw + if (e.status && e.status !== 503) { + throw e; + } + const nextSleepSecs = sleepSecs ? sleepSecs * 2 : 1; // Exponential backoff + if (nextSleepSecs > 60) { + // Wait for a minutes max (two minutes total) for the API to be available again + throw e; + } + return this.getRuleMigrationsStatsWithRetry(params, nextSleepSecs); + }); + } + + private async startTaskStatsPolling(): Promise { let pendingMigrationIds: string[] = []; do { - const results = await this.fetchRuleMigrationTasksStats(); - this.latestStats$.next(results); + const results = await this.getRuleMigrationsStats(); if (pendingMigrationIds.length > 0) { // send notifications for finished migrations @@ -91,22 +183,13 @@ export class SiemRulesMigrationsService { const connectorId = this.connectorIdStorage.get(); if (connectorId) { // automatically resume stopped migrations when connector is available - await startRuleMigration({ - migrationId: result.id, - body: { connector_id: connectorId }, - signal: this.signal, - }); + await startRuleMigration({ migrationId: result.id, connectorId }); pendingMigrationIds.push(result.id); } } } - await new Promise((resolve) => setTimeout(resolve, this.pollingInterval)); + await new Promise((resolve) => setTimeout(resolve, REQUEST_POLLING_INTERVAL_MS)); } while (pendingMigrationIds.length > 0); } - - private async fetchRuleMigrationTasksStats(): Promise { - const stats = await getRuleMigrationsStatsAll({ signal: this.signal }); - return stats.map((stat, index) => ({ ...stat, number: index + 1 })); // the array order (by creation) is guaranteed by the API - } } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts index bbf53ec3a5404..874f1b05dfab6 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/storage.ts @@ -7,23 +7,37 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; -export class RuleMigrationsStorage { - private readonly storage = new Storage(localStorage); +const storages = { + local: new Storage(localStorage), + session: new Storage(sessionStorage), +} as const; + +interface Options { + customKey?: string; + storageType?: keyof typeof storages; +} + +export class RuleMigrationsStorage { + private readonly storage: Storage; public key: string; - constructor(private readonly objectName: string, spaceId?: string) { - this.key = this.getStorageKey(spaceId); + constructor(private readonly objectName: string, private readonly options?: Options) { + this.storage = storages[this.options?.storageType ?? 'local']; + this.key = this.getKey(); } - private getStorageKey(spaceId: string = 'default') { + private getKey(spaceId: string = 'default'): string { + if (this.options?.customKey) { + return this.options.customKey; + } return `siem_migrations.rules.${this.objectName}.${spaceId}`; } public setSpaceId(spaceId: string) { - this.key = this.getStorageKey(spaceId); + this.key = this.getKey(spaceId); } - public get = () => this.storage.get(this.key); - public set = (value: string) => this.storage.set(this.key, value); + public get = (): T | undefined => this.storage.get(this.key); + public set = (value: T) => this.storage.set(this.key, value); public remove = () => this.storage.remove(this.key); } diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx index 830e3c5f4a531..f87755943f830 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/success_notification.tsx @@ -17,9 +17,9 @@ import type { ToastInput } from '@kbn/core-notifications-browser'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { RuleMigrationTask } from '../types'; +import type { RuleMigrationStats } from '../types'; -export const getSuccessToast = (migration: RuleMigrationTask, core: CoreStart): ToastInput => ({ +export const getSuccessToast = (migration: RuleMigrationStats, core: CoreStart): ToastInput => ({ color: 'success', iconType: 'check', toastLifeTimeMs: 1000 * 60 * 30, // 30 minutes @@ -34,7 +34,7 @@ export const getSuccessToast = (migration: RuleMigrationTask, core: CoreStart): ), }); -const SuccessToastContent: React.FC<{ migration: RuleMigrationTask }> = ({ migration }) => { +const SuccessToastContent: React.FC<{ migration: RuleMigrationStats }> = ({ migration }) => { const navigation = { deepLinkId: SecurityPageName.siemMigrationsRules, path: migration.id }; const { navigateTo, getAppUrl } = useNavigation(); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts new file mode 100644 index 0000000000000..41a897a56e9df --- /dev/null +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/service/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const POLLING_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.pollingError', + { defaultMessage: 'Error fetching rule migrations' } +); + +export const MISSING_CONNECTOR_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.create.missingConnectorError', + { defaultMessage: 'Connector not defined. Please set a connector ID first.' } +); + +export const EMPTY_RULES_ERROR = i18n.translate( + 'xpack.securitySolution.siemMigrations.rulesService.create.emptyRulesError', + { defaultMessage: 'Can not create a migration without rules' } +); diff --git a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts index 4c704e97179c0..bcc11327d1051 100644 --- a/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts +++ b/x-pack/plugins/security_solution/public/siem_migrations/rules/types.ts @@ -5,9 +5,11 @@ * 2.0. */ +import type { SiemMigrationTaskStatus } from '../../../common/siem_migrations/constants'; import type { RuleMigrationTaskStats } from '../../../common/siem_migrations/model/rule_migration.gen'; -export interface RuleMigrationTask extends RuleMigrationTaskStats { +export interface RuleMigrationStats extends RuleMigrationTaskStats { + status: SiemMigrationTaskStatus; /** The sequential number of the migration */ number: number; } diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts index 21a17bb7834a1..7c94631be6b65 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/create.ts @@ -8,9 +8,10 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { v4 as uuidV4 } from 'uuid'; -import { SIEM_RULE_MIGRATIONS_PATH } from '../../../../../common/siem_migrations/constants'; +import { SIEM_RULE_MIGRATION_CREATE_PATH } from '../../../../../common/siem_migrations/constants'; import { CreateRuleMigrationRequestBody, + CreateRuleMigrationRequestParams, type CreateRuleMigrationResponse, } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -23,7 +24,7 @@ export const registerSiemRuleMigrationsCreateRoute = ( ) => { router.versioned .post({ - path: SIEM_RULE_MIGRATIONS_PATH, + path: SIEM_RULE_MIGRATION_CREATE_PATH, access: 'internal', security: { authz: { requiredPrivileges: ['securitySolution'] } }, }) @@ -31,18 +32,20 @@ export const registerSiemRuleMigrationsCreateRoute = ( { version: '1', validate: { - request: { body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody) }, + request: { + body: buildRouteValidationWithZod(CreateRuleMigrationRequestBody), + params: buildRouteValidationWithZod(CreateRuleMigrationRequestParams), + }, }, }, withLicense( async (context, req, res): Promise> => { const originalRules = req.body; + const migrationId = req.params.migration_id ?? uuidV4(); try { const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const migrationId = uuidV4(); - const ruleMigrations = originalRules.map((originalRule) => ({ migration_id: migrationId, original_rule: originalRule, diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts index b9645a3de374e..dd13a75cdf83a 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/get.ts @@ -14,6 +14,7 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import { SIEM_RULE_MIGRATION_PATH } from '../../../../../common/siem_migrations/constants'; import type { SecuritySolutionPluginRouter } from '../../../../types'; +import type { RuleMigrationGetOptions } from '../data/rule_migrations_data_rules_client'; import { withLicense } from './util/with_license'; export const registerSiemRuleMigrationsGetRoute = ( @@ -43,20 +44,13 @@ export const registerSiemRuleMigrationsGetRoute = ( const ctx = await context.resolve(['securitySolution']); const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - let from = 0; - if (page && perPage) { - from = page * perPage; - } - const size = perPage; + const options: RuleMigrationGetOptions = { + filters: { searchTerm }, + size: perPage, + from: page && perPage ? page * perPage : 0, + }; - const result = await ruleMigrationsClient.data.rules.get( - { - migrationId, - searchTerm, - }, - from, - size - ); + const result = await ruleMigrationsClient.data.rules.get(migrationId, options); return res.ok({ body: result }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts index df86a1f953656..2fce95be9dafe 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/api/util/installation.ts @@ -177,10 +177,8 @@ export const installTranslated = async ({ const detectionRulesClient = securitySolutionContext.getDetectionRulesClient(); const ruleMigrationsClient = securitySolutionContext.getSiemRuleMigrationsClient(); - const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get({ - migrationId, - ids, - installable: true, + const { data: rulesToInstall } = await ruleMigrationsClient.data.rules.get(migrationId, { + filters: { ids, installable: true }, }); const { customRulesToInstall, prebuiltRulesToInstall } = rulesToInstall.reduce( diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts index 209f2e4416e16..716d19ce16cdf 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_data_rules_client.ts @@ -38,32 +38,23 @@ export type UpdateRuleMigrationInput = { elastic_rule?: Partial } & export type RuleMigrationDataStats = Omit; export type RuleMigrationAllDataStats = RuleMigrationDataStats[]; -export interface RuleMigrationFilterOptions { - migrationId: string; +export interface RuleMigrationFilters { status?: SiemMigrationStatus | SiemMigrationStatus[]; ids?: string[]; installable?: boolean; searchTerm?: string; } +export interface RuleMigrationGetOptions { + filters?: RuleMigrationFilters; + from?: number; + size?: number; +} /* BULK_MAX_SIZE defines the number to break down the bulk operations by. * The 500 number was chosen as a reasonable number to avoid large payloads. It can be adjusted if needed. */ const BULK_MAX_SIZE = 500 as const; - -const getInstallableConditions = (): QueryDslQueryContainer[] => { - return [ - { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }, - { - nested: { - path: 'elastic_rule', - query: { - bool: { must_not: { exists: { field: 'elastic_rule.id' } } }, - }, - }, - }, - ]; -}; +/* The default number of rule migrations to retrieve in a single GET request. */ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { /** Indexes an array of rule migrations to be processed */ @@ -128,12 +119,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves an array of rule documents of a specific migrations */ async get( - filters: RuleMigrationFilterOptions, - from?: number, - size?: number + migrationId: string, + { filters = {}, from, size }: RuleMigrationGetOptions = {} ): Promise<{ total: number; data: StoredRuleMigration[] }> { const index = await this.getIndexName(); - const query = this.getFilterQuery(filters); + const query = this.getFilterQuery(migrationId, { ...filters }); const result = await this.esClient .search({ index, query, sort: '_doc', from, size }) @@ -155,7 +145,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient */ async takePending(migrationId: string, size: number): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId, status: SiemMigrationStatus.PENDING }); + const query = this.getFilterQuery(migrationId, { status: SiemMigrationStatus.PENDING }); const storedRuleMigrations = await this.esClient .search({ index, query, sort: '_doc', size }) @@ -234,7 +224,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient { refresh = false }: { refresh?: boolean } = {} ): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId, status: statusToQuery }); + const query = this.getFilterQuery(migrationId, { status: statusToQuery }); const script = { source: `ctx._source['status'] = '${statusToUpdate}'` }; await this.esClient.updateByQuery({ index, query, script, refresh }).catch((error) => { this.logger.error(`Error updating rule migrations status: ${error.message}`); @@ -245,24 +235,11 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves the translation stats for the rule migrations with the provided id */ async getTranslationStats(migrationId: string): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId }); + const query = this.getFilterQuery(migrationId); const aggregations = { - prebuilt: { - filter: { - nested: { - path: 'elastic_rule', - query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } }, - }, - }, - }, - installable: { - filter: { - bool: { - must: getInstallableConditions(), - }, - }, - }, + prebuilt: { filter: conditions.isPrebuilt() }, + installable: { filter: { bool: { must: conditions.isInstallable() } } }, }; const result = await this.esClient .search({ index, query, aggregations, _source: false }) @@ -288,7 +265,7 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient /** Retrieves the stats for the rule migrations with the provided id */ async getStats(migrationId: string): Promise { const index = await this.getIndexName(); - const query = this.getFilterQuery({ migrationId }); + const query = this.getFilterQuery(migrationId); const aggregations = { pending: { filter: { term: { status: SiemMigrationStatus.PENDING } } }, processing: { filter: { term: { status: SiemMigrationStatus.PROCESSING } } }, @@ -358,13 +335,10 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient })); } - private getFilterQuery({ - migrationId, - status, - ids, - installable, - searchTerm, - }: RuleMigrationFilterOptions): QueryDslQueryContainer { + private getFilterQuery( + migrationId: string, + { status, ids, installable, searchTerm }: RuleMigrationFilters = {} + ): QueryDslQueryContainer { const filter: QueryDslQueryContainer[] = [{ term: { migration_id: migrationId } }]; if (status) { if (Array.isArray(status)) { @@ -377,16 +351,44 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient filter.push({ terms: { _id: ids } }); } if (installable) { - filter.push(...getInstallableConditions()); + filter.push(...conditions.isInstallable()); } if (searchTerm?.length) { - filter.push({ - nested: { - path: 'elastic_rule', - query: { match: { 'elastic_rule.title': searchTerm } }, - }, - }); + filter.push(conditions.matchTitle(searchTerm)); } return { bool: { filter } }; } } + +const conditions = { + isFullyTranslated(): QueryDslQueryContainer { + return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } }; + }, + isNotInstalled(): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } }, + }, + }; + }, + isPrebuilt(): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } }, + }, + }; + }, + matchTitle(title: string): QueryDslQueryContainer { + return { + nested: { + path: 'elastic_rule', + query: { match: { 'elastic_rule.title': title } }, + }, + }; + }, + isInstallable(): QueryDslQueryContainer[] { + return [this.isFullyTranslated(), this.isNotInstalled()]; + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts index 09a4bef34c279..f63953192844b 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/data/rule_migrations_field_maps.ts @@ -23,7 +23,8 @@ export const ruleMigrationsFieldMap: FieldMap async (state) => { - const mitreAttackIds = state.original_rule.mitre_attack_ids; + const mitreAttackIds = state.original_rule.annotations?.mitre_attack; if (!mitreAttackIds?.length) { return {}; } diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 17163bcdc38f2..a67ba14c2a8f2 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -32,7 +32,10 @@ import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/comm import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; -import { CreateRuleMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; +import { + CreateRuleMigrationRequestParamsInput, + CreateRuleMigrationRequestBodyInput, +} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { CreateTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/create_timelines/create_timelines_route.gen'; import { CreateUpdateProtectionUpdatesNoteRequestParamsInput, @@ -369,7 +372,12 @@ Migrations are initiated per index. While the process is neither destructive nor */ createRuleMigration(props: CreateRuleMigrationProps, kibanaSpace: string = 'default') { return supertest - .post(routeWithNamespace('/internal/siem_migrations/rules', kibanaSpace)) + .post( + routeWithNamespace( + replaceParams('/internal/siem_migrations/rules/{migration_id}', props.params), + kibanaSpace + ) + ) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') @@ -1590,6 +1598,7 @@ export interface CreateRuleProps { body: CreateRuleRequestBodyInput; } export interface CreateRuleMigrationProps { + params: CreateRuleMigrationRequestParamsInput; body: CreateRuleMigrationRequestBodyInput; } export interface CreateTimelinesProps { From 62f28c44e8497144693531223195b0750790131e Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:31:05 +0100 Subject: [PATCH 09/10] [8.x] [React18] Migrate test suites to account for testing library upgrades security-threat-hunting-investigations (#201145) (#201859) # Backport This will backport the following commits from `main` to `8.x`: - [[React18] Migrate test suites to account for testing library upgrades security-threat-hunting-investigations (#201145)](https://github.com/elastic/kibana/pull/201145) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../use_initialize_from_local_storage.test.ts | 2 +- .../src/hooks/use_sections.test.tsx | 5 +- .../src/hooks/use_window_width.test.ts | 2 +- .../use_insight_data_providers.test.ts | 20 +- .../plugins/insight/use_insight_query.test.ts | 6 +- .../use_summary_chart_data.test.tsx | 7 +- .../alerts_local_storage/index.test.tsx | 2 +- .../alerts_kpis/common/hooks.test.tsx | 8 +- .../investigate_in_resolver.test.tsx | 2 +- .../use_add_to_case_actions.test.tsx | 3 +- .../use_alert_assignees_actions.test.tsx | 3 +- .../use_alert_tags_actions.test.tsx | 3 +- .../use_investigate_in_timeline.test.tsx | 4 +- .../left/hooks/use_fetch_alerts.test.ts | 11 +- .../hooks/use_response_actions_view.test.ts | 2 +- .../use_threat_intelligence_details.test.ts | 2 +- .../right/hooks/use_accordion_state.test.ts | 6 +- .../right/hooks/use_assistant.test.tsx | 6 +- .../right/hooks/use_expand_section.test.ts | 6 +- .../use_fetch_threat_intelligence.test.tsx | 6 +- .../hooks/use_flyout_is_expandable.test.tsx | 2 +- .../right/hooks/use_get_flyout_link.test.tsx | 2 +- .../right/hooks/use_graph_preview.test.tsx | 6 +- .../right/hooks/use_process_data.test.tsx | 2 +- .../right/hooks/use_session_preview.test.tsx | 6 +- .../right/hooks/use_tabs.test.tsx | 6 +- ...se_alert_document_analyzer_schema.test.tsx | 8 +- .../hooks/use_alert_prevalence.test.tsx | 6 +- ...lert_prevalence_from_process_tree.test.tsx | 8 +- .../use_basic_data_from_details_data.test.tsx | 2 +- .../shared/hooks/use_event_details.test.tsx | 6 +- ..._fetch_related_alerts_by_ancestry.test.tsx | 8 +- ...lated_alerts_by_same_source_event.test.tsx | 8 +- ...e_fetch_related_alerts_by_session.test.tsx | 8 +- .../hooks/use_fetch_related_cases.test.ts | 2 +- .../shared/hooks/use_get_fields_data.test.tsx | 6 +- .../hooks/use_highlighted_fields.test.tsx | 2 +- .../use_investigation_enrichment.test.ts | 2 +- .../hooks/use_investigation_guide.test.ts | 6 +- .../hooks/use_navigate_to_analyzer.test.tsx | 2 +- .../use_navigate_to_session_view.test.tsx | 2 +- .../shared/hooks/use_prevalence.test.tsx | 2 +- .../hooks/use_rule_details_link.test.ts | 6 +- ...e_show_related_alerts_by_ancestry.test.tsx | 8 +- ...lated_alerts_by_same_source_event.test.tsx | 8 +- ...se_show_related_alerts_by_session.test.tsx | 8 +- .../hooks/use_show_related_cases.test.tsx | 2 +- .../hooks/use_show_suppressed_alerts.test.tsx | 6 +- .../shared/hooks/use_which_flyout.test.tsx | 6 +- .../use_on_expandable_flyout_close.test.tsx | 2 +- .../notes/hooks/use_fetch_notes.test.ts | 2 +- .../use_get_sourcerer_data_view.test.ts | 15 +- .../components/use_update_data_view.test.tsx | 2 +- .../sourcerer/containers/hooks.test.tsx | 492 ++++++++---------- .../containers/use_signal_helpers.test.tsx | 84 ++- .../create_field_button/index.test.tsx | 7 +- .../field_table_columns/index.test.tsx | 7 +- .../components/fields_browser/index.test.tsx | 24 +- .../components/open_timeline/helpers.test.ts | 3 +- .../components/open_timeline/index.test.tsx | 40 +- .../hooks/use_delete_note.test.ts | 3 +- .../open_timeline/use_timeline_types.test.tsx | 229 ++++---- .../use_update_timeline.test.tsx | 247 ++++----- .../properties/use_notes_in_flyout.test.tsx | 11 +- .../use_histogram_customizations.test.ts | 2 +- .../esql/use_get_stateful_query_bar.test.tsx | 2 +- .../tabs/session/use_session_view.test.tsx | 3 +- .../tabs/shared/use_timeline_columns.test.ts | 2 +- .../use_timeline_control_columns.test.tsx | 2 +- .../timelines/containers/index.test.tsx | 364 ++++++------- .../use_timeline_data_filters.test.tsx | 2 +- .../hooks/use_create_timeline.test.tsx | 2 +- .../public/hooks/use_integrations.test.tsx | 10 +- .../block_list/hooks/use_policies.test.tsx | 10 +- .../cases/hooks/use_case_permission.test.tsx | 4 +- .../cases/hooks/use_indicator_by_id.test.tsx | 18 +- .../hooks/use_aggregated_indicators.test.tsx | 6 +- .../hooks/use_column_settings.test.ts | 2 +- .../indicators/hooks/use_indicators.test.tsx | 12 +- .../hooks/use_toolbar_options.test.tsx | 2 +- .../indicators/hooks/use_total_count.test.tsx | 2 +- .../query_bar/hooks/use_filter_in_out.test.ts | 4 +- .../hooks/use_add_to_timeline.test.tsx | 4 +- .../use_investigate_in_timeline.test.tsx | 4 +- 84 files changed, 878 insertions(+), 999 deletions(-) diff --git a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts index 26b3daf8161db..cc3623f9238c6 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useInitializeFromLocalStorage } from './use_initialize_from_local_storage'; import { localStorageMock } from '../../__mocks__'; import { diff --git a/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx b/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx index 4526f128affd3..8dc4aacaefbcf 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx +++ b/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx @@ -8,8 +8,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react'; import type { UseSectionsParams, UseSectionsResult } from './use_sections'; import { useSections } from './use_sections'; import { useExpandableFlyoutState } from '../..'; @@ -17,7 +16,7 @@ import { useExpandableFlyoutState } from '../..'; jest.mock('../..'); describe('useSections', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return undefined for all values if no registeredPanels', () => { (useExpandableFlyoutState as jest.Mock).mockReturnValue({ diff --git a/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts index 72ab9148743db..696191e7fbdf6 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { FULL_WIDTH_PADDING, MAX_RESOLUTION_BREAKPOINT, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts index 4d1807b91b718..f8b0e1bff9441 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import type { UseInsightDataProvidersProps, Provider } from './use_insight_data_providers'; + +import { renderHook } from '@testing-library/react'; +import type { Provider } from './use_insight_data_providers'; import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; -import { - useInsightDataProviders, - type UseInsightDataProvidersResult, -} from './use_insight_data_providers'; +import { useInsightDataProviders } from './use_insight_data_providers'; import { mockAlertDetailsData } from '../../../event_details/mocks'; const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { @@ -103,7 +101,7 @@ const providerWithRange: Provider[][] = [ describe('useInsightDataProviders', () => { it('should return 2 data providers, 1 with a nested provider ANDed to it', () => { - const { result } = renderHook(() => + const { result } = renderHook(() => useInsightDataProviders({ providers: nestedAndProvider, alertData: mockAlertDetailsDataWithIsObject, @@ -117,7 +115,7 @@ describe('useInsightDataProviders', () => { }); it('should return 3 data providers without any containing nested ANDs', () => { - const { result } = renderHook(() => + const { result } = renderHook(() => useInsightDataProviders({ providers: topLevelOnly, alertData: mockAlertDetailsDataWithIsObject, @@ -130,7 +128,7 @@ describe('useInsightDataProviders', () => { }); it('should use the string literal if no field in the alert matches a bracketed value', () => { - const { result } = renderHook(() => + const { result } = renderHook(() => useInsightDataProviders({ providers: nonExistantField, alertData: mockAlertDetailsDataWithIsObject, @@ -145,7 +143,7 @@ describe('useInsightDataProviders', () => { }); it('should use template data providers when called without alertData', () => { - const { result } = renderHook(() => + const { result } = renderHook(() => useInsightDataProviders({ providers: nestedAndProvider, }) @@ -159,7 +157,7 @@ describe('useInsightDataProviders', () => { }); it('should return an empty array of dataProviders and populated filters if a provider contains a range type', () => { - const { result } = renderHook(() => + const { result } = renderHook(() => useInsightDataProviders({ providers: providerWithRange, }) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts index a162c625c7adc..98d5e2f3f7df4 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { QueryOperator } from '@kbn/timelines-plugin/common'; import { DataProviderTypeEnum } from '../../../../../../common/api/timeline'; import { useInsightQuery } from './use_insight_query'; import { TestProviders } from '../../../../mock'; -import type { UseInsightQuery, UseInsightQueryResult } from './use_insight_query'; import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; const mockProvider = { @@ -30,7 +28,7 @@ const mockProvider = { describe('useInsightQuery', () => { it('should return renderable defaults', () => { - const { result } = renderHook, UseInsightQueryResult>( + const { result } = renderHook( () => useInsightQuery({ dataProviders: [mockProvider], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.test.tsx index 74e04a61ae5bd..e34d8be795ba8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.test.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import type { PropsWithChildren } from 'react'; +import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants'; -import type { UseAlerts, UseAlertsQueryProps } from './use_summary_chart_data'; +import type { UseAlertsQueryProps } from './use_summary_chart_data'; import { useSummaryChartData, getAlertsQuery } from './use_summary_chart_data'; import * as aggregations from './aggregations'; import * as severityMock from '../severity_level_panel/mock_data'; @@ -76,7 +75,7 @@ describe('getAlertsQuery', () => { // helper function to render the hook const renderUseSummaryChartData = (props: Partial = {}) => - renderHook, ReturnType>( + renderHook( () => useSummaryChartData({ aggregations: aggregations.severityAggregations, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/chart_panels/alerts_local_storage/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/chart_panels/alerts_local_storage/index.test.tsx index 303b85a40e6ee..82e8456e28571 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/chart_panels/alerts_local_storage/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/chart_panels/alerts_local_storage/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import React from 'react'; import { useAlertsLocalStorage } from '.'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx index c89626b9edcec..f7fd2eced3d3d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/hooks.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { FieldSpec } from '@kbn/data-plugin/common'; import type { GetAggregatableFields, UseInspectButtonParams } from './hooks'; @@ -120,7 +120,7 @@ describe('hooks', () => { jest.clearAllMocks(); }); it('returns only aggregateable fields', () => { - const wrapper = ({ children }: { children: JSX.Element }) => ( + const wrapper = ({ children }: React.PropsWithChildren) => ( {children} ); const { result, unmount } = renderHook(() => useStackByFields(), { wrapper }); @@ -137,7 +137,7 @@ describe('hooks', () => { browserFields: { base: mockBrowserFields.base }, }); - const wrapper = ({ children }: { children: JSX.Element }) => ( + const wrapper = ({ children }: React.PropsWithChildren) => ( {children} ); const useLensCompatibleFields = true; @@ -155,7 +155,7 @@ describe('hooks', () => { browserFields: { nestedField: mockBrowserFields.nestedField }, }); - const wrapper = ({ children }: { children: JSX.Element }) => ( + const wrapper = ({ children }: React.PropsWithChildren) => ( {children} ); const useLensCompatibleFields = true; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx index cb1fa0b4ef86e..fe3648c426bc0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx @@ -7,7 +7,7 @@ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { useIsInvestigateInResolverActionEnabled } from './investigate_in_resolver'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; describe('InvestigateInResolverAction', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx index b60a7a5644b52..d22520f9f9553 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { act, renderHook } from '@testing-library/react-hooks'; -import { render, screen } from '@testing-library/react'; +import { render, screen, renderHook, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useAddToCaseActions } from './use_add_to_case_actions'; import { TestProviders } from '../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_assignees_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_assignees_actions.test.tsx index a7bde416b7ca0..b3173bcff3770 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_assignees_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_assignees_actions.test.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import type { UseAlertAssigneesActionsProps } from './use_alert_assignees_actions'; import { useAlertAssigneesActions } from './use_alert_assignees_actions'; import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges'; import type { AlertTableContextMenuItem } from '../types'; -import { render } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import React from 'react'; import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiPopover, EuiContextMenu } from '@elastic/eui'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_tags_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_tags_actions.test.tsx index 4e037467597c9..64461725b8622 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_tags_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alert_tags_actions.test.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; import type { UseAlertTagsActionsProps } from './use_alert_tags_actions'; import { useAlertTagsActions } from './use_alert_tags_actions'; import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges'; import type { AlertTableContextMenuItem } from '../types'; -import { render } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import React from 'react'; import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiPopover, EuiContextMenu } from '@elastic/eui'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx index 6d20ef3973337..9c688816f59ea 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import { fireEvent, render, waitFor } from '@testing-library/react'; + +import { fireEvent, render, waitFor, renderHook, act } from '@testing-library/react'; import { of } from 'rxjs'; import { TestProviders } from '../../../../common/mock'; import { useKibana } from '../../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts index c4910f5daa42a..9f9d031cc0bf2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_fetch_alerts.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, waitFor } from '@testing-library/react'; import { useKibana } from '../../../../common/lib/kibana'; import { createFindAlerts } from '../services/find_alerts'; import { useFetchAlerts, type UseAlertsQueryParams } from './use_fetch_alerts'; @@ -41,15 +41,14 @@ describe('useFetchAlerts', () => { sort: [{ '@timestamp': 'desc' }], }; - const { result, waitFor } = renderHook(() => useFetchAlerts(params), { + const { result } = renderHook(() => useFetchAlerts(params), { wrapper: createReactQueryWrapper(), }); expect(result.current.loading).toBe(true); - await waitFor(() => !result.current.loading); + await waitFor(() => expect(result.current.loading).toBe(false)); - expect(result.current.loading).toBe(false); expect(result.current.error).toBe(false); expect(result.current.totalItemCount).toBe(10); expect(result.current.data).toEqual(['alert1', 'alert2', 'alert3']); @@ -70,13 +69,13 @@ describe('useFetchAlerts', () => { sort: [{ '@timestamp': 'desc' }], }; - const { result, waitFor } = renderHook(() => useFetchAlerts(params), { + const { result } = renderHook(() => useFetchAlerts(params), { wrapper: createReactQueryWrapper(), }); expect(result.current.loading).toBe(true); - await waitFor(() => !result.current.loading); + await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(true); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts index cafac9f3a0b9f..5e6469e44a048 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useResponseActionsView } from './use_response_actions_view'; import { mockSearchHit } from '../../shared/mocks/mock_search_hit'; import { mockDataAsNestedObject } from '../../shared/mocks/mock_data_as_nested_object'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts index e40cd74709cfd..522fd74765fd8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts @@ -6,7 +6,7 @@ */ import { useThreatIntelligenceDetails } from './use_threat_intelligence_details'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { SecurityPageName } from '@kbn/deeplinks-security'; import { useTimelineEventsDetails } from '../../../../timelines/containers/details'; import { useSourcererDataView } from '../../../../sourcerer/containers'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_accordion_state.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_accordion_state.test.ts index ac3c9c8a6be0d..80ac41b539fa1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_accordion_state.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_accordion_state.test.ts @@ -7,14 +7,14 @@ import type { ToggleReducerAction, UseAccordionStateValue } from './use_accordion_state'; import { useAccordionState, toggleReducer } from './use_accordion_state'; -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { FLYOUT_STORAGE_KEYS } from '../../shared/constants/local_storage'; const mockSet = jest.fn(); describe('useAccordionState', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return initial value', () => { hookResult = renderHook((props: boolean) => useAccordionState(props), { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 9ca0d9fd18e7d..1871c4aad5b8d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseAssistantParams, UseAssistantResult } from './use_assistant'; import { useAssistant } from './use_assistant'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; @@ -25,7 +25,7 @@ const renderUseAssistant = () => }); describe('useAssistant', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it(`should return showAssistant true and a value for promptContextId`, () => { jest.mocked(useAssistantAvailability).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_expand_section.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_expand_section.test.ts index 998f56312b0f0..26c9b36a728aa 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_expand_section.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_expand_section.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseExpandSectionParams } from './use_expand_section'; import { useExpandSection } from './use_expand_section'; import { useKibana } from '../../../../common/lib/kibana'; @@ -14,7 +14,7 @@ import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); describe('useExpandSection', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return default value if nothing in localStorage', () => { (useKibana as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.test.tsx index e778552dff613..74672356f0762 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseThreatIntelligenceParams, UseThreatIntelligenceResult, @@ -41,7 +41,7 @@ const dataFormattedForFieldBrowser = [ ]; describe('useFetchThreatIntelligence', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('return render 1 match detected and 1 field enriched', () => { (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx index a67bb675a373a..3611ab6282644 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.test.tsx @@ -6,7 +6,7 @@ */ import { useFlyoutIsExpandable } from './use_flyout_is_expandable'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx index 2db21334e59f3..71b24ab891e78 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useGetFlyoutLink } from './use_get_flyout_link'; import { useGetAppUrl } from '@kbn/security-solution-navigation'; import { ALERT_DETAILS_REDIRECT_PATH } from '../../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx index d12154a390abf..7fa0741a85118 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseGraphPreviewParams, UseGraphPreviewResult } from './use_graph_preview'; import { useGraphPreview } from './use_graph_preview'; import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { mockFieldData } from '../../shared/mocks/mock_get_fields_data'; describe('useGraphPreview', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.test.tsx index 31eb78975d195..6f6085c13ee6f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.test.tsx @@ -6,7 +6,7 @@ */ import { getUserDisplayName, useProcessData } from './use_process_data'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { FC, PropsWithChildren } from 'react'; import { DocumentDetailsContext } from '../../shared/context'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx index 0f6f233772626..4e2e19c6b54fa 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseSessionPreviewParams } from './use_session_preview'; import { useSessionPreview } from './use_session_preview'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; @@ -15,7 +15,7 @@ import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_f import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; describe('useSessionPreview', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it(`should return a session view config object if alert ancestor index is available`, () => { const getFieldsData: GetFieldsData = (field: string) => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_tabs.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_tabs.test.tsx index 87a23a06b7ce1..a8a7f61bac26c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_tabs.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseTabsParams, UseTabsResult } from './use_tabs'; import { allThreeTabs, twoTabs, useTabs } from './use_tabs'; @@ -26,7 +26,7 @@ jest.mock('../../../../common/lib/kibana', () => { }); describe('useTabs', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return 3 tabs to render and the one from path as selected', () => { const initialProps: UseTabsParams = { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx index 3c31720b53f99..8d9cbd958e432 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { useQuery } from '@tanstack/react-query'; import type { UseAlertDocumentAnalyzerSchemaParams, @@ -20,8 +20,8 @@ jest.mock('@tanstack/react-query'); describe('useAlertPrevalenceFromProcessTree', () => { let hookResult: RenderHookResult< - UseAlertDocumentAnalyzerSchemaParams, - UseAlertDocumentAnalyzerSchemaResult + UseAlertDocumentAnalyzerSchemaResult, + UseAlertDocumentAnalyzerSchemaParams >; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx index 231e0e5419441..27b83255443b5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { ALERT_PREVALENCE_AGG, useAlertPrevalence } from './use_alert_prevalence'; import type { UseAlertPrevalenceParams, UserAlertPrevalenceResult } from './use_alert_prevalence'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; @@ -18,7 +18,7 @@ jest.mock('../../../../common/hooks/use_selector'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_query'); describe('useAlertPrevalence', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; beforeEach(() => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx index 94b7cfa623507..668f233e65710 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseAlertPrevalenceFromProcessTreeParams, UserAlertPrevalenceFromProcessTreeResult, @@ -25,8 +25,8 @@ jest.mock('@tanstack/react-query'); describe('useAlertPrevalenceFromProcessTree', () => { let hookResult: RenderHookResult< - UseAlertPrevalenceFromProcessTreeParams, - UserAlertPrevalenceFromProcessTreeResult + UserAlertPrevalenceFromProcessTreeResult, + UseAlertPrevalenceFromProcessTreeParams >; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx index b4cd7c35824a1..2894cb0d21276 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx index de1020bac4d00..6e4cab20f013c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseEventDetailsParams, UseEventDetailsResult } from './use_event_details'; import { getAlertIndexAlias, useEventDetails } from './use_event_details'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; @@ -45,7 +45,7 @@ describe('getAlertIndexAlias', () => { }); describe('useEventDetails', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return all properties', () => { jest.mocked(useSpaceId).mockReturnValue('default'); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx index 4d65339c6b41a..7630c260f4c21 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseFetchRelatedAlertsByAncestryParams, UseFetchRelatedAlertsByAncestryResult, @@ -22,8 +22,8 @@ const scopeId = 'scopeId'; describe('useFetchRelatedAlertsByAncestry', () => { let hookResult: RenderHookResult< - UseFetchRelatedAlertsByAncestryParams, - UseFetchRelatedAlertsByAncestryResult + UseFetchRelatedAlertsByAncestryResult, + UseFetchRelatedAlertsByAncestryParams >; it('should return loading true while data is loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx index ff74774068adf..9da01a94d51dc 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseFetchRelatedAlertsBySameSourceEventParams, UseFetchRelatedAlertsBySameSourceEventResult, @@ -21,8 +21,8 @@ const scopeId = 'scopeId'; describe('useFetchRelatedAlertsBySameSourceEvent', () => { let hookResult: RenderHookResult< - UseFetchRelatedAlertsBySameSourceEventParams, - UseFetchRelatedAlertsBySameSourceEventResult + UseFetchRelatedAlertsBySameSourceEventResult, + UseFetchRelatedAlertsBySameSourceEventParams >; it('should return loading true while data is loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx index b38ef44178f9f..27f030cc11e2d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseFetchRelatedAlertsBySessionParams, @@ -22,8 +22,8 @@ const scopeId = 'scopeId'; describe('useFetchRelatedAlertsBySession', () => { let hookResult: RenderHookResult< - UseFetchRelatedAlertsBySessionParams, - UseFetchRelatedAlertsBySessionResult + UseFetchRelatedAlertsBySessionResult, + UseFetchRelatedAlertsBySessionParams >; it('should return loading true while data is loading', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts index 6ebdc2bc4b7c7..9e494ef8e05d0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_cases.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { createReactQueryWrapper } from '../../../../common/mock'; import { useFetchRelatedCases } from './use_fetch_related_cases'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx index fcf370b4bca1a..a91d14c7a84ee 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { mockSearchHit } from '../mocks/mock_search_hit'; import type { UseGetFieldsDataParams, UseGetFieldsDataResult } from './use_get_fields_data'; import { useGetFieldsData } from './use_get_fields_data'; @@ -17,7 +17,7 @@ const fieldsData = { }; describe('useGetFieldsData', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return the value for a field', () => { hookResult = renderHook(() => useGetFieldsData({ fieldsData })); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx index 6eb8c242c79fe..6bffb7c58ae3f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { mockDataFormattedForFieldBrowser, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_enrichment.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_enrichment.test.ts index 0e1cdbc845b38..c79e1ec07ce15 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_enrichment.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_enrichment.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useInvestigationTimeEnrichment } from './use_investigation_enrichment'; import { DEFAULT_EVENT_ENRICHMENT_FROM, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts index f7e3a40e60c40..39522df675486 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseInvestigationGuideParams, UseInvestigationGuideResult, @@ -22,7 +22,7 @@ jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fall const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; describe('useInvestigationGuide', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return loading true', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ ruleId: 'ruleId' }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.test.tsx index 6f578c7cdb95c..334bf5f08721e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useNavigateToAnalyzer } from './use_navigate_to_analyzer'; import { mockFlyoutApi } from '../mocks/mock_flyout_context'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.test.tsx index c0f85e07377df..ac624ce11ce56 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useNavigateToSessionView } from './use_navigate_to_session_view'; import { mockFlyoutApi } from '../mocks/mock_flyout_context'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_prevalence.test.tsx index a9c12adfc84ca..d4acc0eb0c7f6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_prevalence.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_prevalence.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { usePrevalence } from './use_prevalence'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_rule_details_link.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_rule_details_link.test.ts index f77b82af80065..ca748433c1931 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_rule_details_link.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_rule_details_link.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseRuleDetailsLinkParams } from './use_rule_details_link'; import { useRuleDetailsLink } from './use_rule_details_link'; @@ -24,7 +24,7 @@ jest.mock('../../../../common/components/link_to', () => ({ })); describe('useRuleDetailsLink', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return null if the ruleId prop is null', () => { const initialProps: UseRuleDetailsLinkParams = { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.test.tsx index f4be536d5419c..938930496d803 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseShowRelatedAlertsByAncestryParams, UseShowRelatedAlertsByAncestryResult, @@ -36,8 +36,8 @@ const dataAsNestedObject = mockDataAsNestedObject; describe('useShowRelatedAlertsByAncestry', () => { let hookResult: RenderHookResult< - UseShowRelatedAlertsByAncestryParams, - UseShowRelatedAlertsByAncestryResult + UseShowRelatedAlertsByAncestryResult, + UseShowRelatedAlertsByAncestryParams >; it('should return false if Process Entity Info is not available', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.test.tsx index dfbfeeccc655a..71f681c64e802 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { ShowRelatedAlertsBySameSourceEventParams, @@ -18,8 +18,8 @@ const eventId = 'eventId'; describe('useShowRelatedAlertsBySameSourceEvent', () => { let hookResult: RenderHookResult< - ShowRelatedAlertsBySameSourceEventParams, - ShowRelatedAlertsBySameSourceEventResult + ShowRelatedAlertsBySameSourceEventResult, + ShowRelatedAlertsBySameSourceEventParams >; it('should return eventId if getFieldsData returns null', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.test.tsx index 32595a4c27c6d..92d9c076ab64f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { UseShowRelatedAlertsBySessionParams, @@ -16,8 +16,8 @@ import { useShowRelatedAlertsBySession } from './use_show_related_alerts_by_sess describe('useShowRelatedAlertsBySession', () => { let hookResult: RenderHookResult< - UseShowRelatedAlertsBySessionParams, - UseShowRelatedAlertsBySessionResult + UseShowRelatedAlertsBySessionResult, + UseShowRelatedAlertsBySessionParams >; it('should return false if getFieldsData returns null', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx index cfa9b87d8fcfc..6245e480a35db 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useShowRelatedCases } from './use_show_related_cases'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.test.tsx index 622f37a0997cf..b10e154fed3d5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import type { ShowSuppressedAlertsParams, ShowSuppressedAlertsResult, @@ -14,7 +14,7 @@ import type { import { useShowSuppressedAlerts } from './use_show_suppressed_alerts'; describe('useShowSuppressedAlerts', () => { - let hookResult: RenderHookResult; + let hookResult: RenderHookResult; it('should return false if getFieldsData returns null', () => { const getFieldsData = () => null; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_which_flyout.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_which_flyout.test.tsx index 76277b8da889b..033fb53a5c029 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_which_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_which_flyout.test.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { useWhichFlyout } from './use_which_flyout'; import { Flyouts } from '../constants/flyouts'; describe('useWhichFlyout', () => { - let hookResult: RenderHookResult<{}, string | null>; + let hookResult: RenderHookResult; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_on_expandable_flyout_close.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_on_expandable_flyout_close.test.tsx index 308c1bcfc6cfc..2a4e5576e24bc 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_on_expandable_flyout_close.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_on_expandable_flyout_close.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useWhichFlyout } from '../../document_details/shared/hooks/use_which_flyout'; import { useOnExpandableFlyoutClose } from './use_on_expandable_flyout_close'; import { Flyouts } from '../../document_details/shared/constants/flyouts'; diff --git a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts index 7498a6f40c6b6..b7fa3ab3fc519 100644 --- a/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts +++ b/x-pack/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useDispatch } from 'react-redux'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { fetchNotesByDocumentIds } from '../store/notes.slice'; diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts b/x-pack/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts index 18e34ba2067a1..977ee35310031 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts +++ b/x-pack/plugins/security_solution/public/sourcerer/components/use_get_sourcerer_data_view.test.ts @@ -6,7 +6,7 @@ */ import { DataView } from '@kbn/data-views-plugin/common'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useSourcererDataView } from '../containers'; import { mockSourcererScope } from '../containers/mocks'; import { SourcererScopeName } from '../store/model'; @@ -14,14 +14,11 @@ import type { UseGetScopedSourcererDataViewArgs } from './use_get_sourcerer_data import { useGetScopedSourcererDataView } from './use_get_sourcerer_data_view'; const renderHookCustom = (args: UseGetScopedSourcererDataViewArgs) => { - return renderHook( - ({ sourcererScope }) => useGetScopedSourcererDataView({ sourcererScope }), - { - initialProps: { - ...args, - }, - } - ); + return renderHook(({ sourcererScope }) => useGetScopedSourcererDataView({ sourcererScope }), { + initialProps: { + ...args, + }, + }); }; jest.mock('../containers'); diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx index b37565f3eb912..bb4d935dbdc99 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/components/use_update_data_view.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useUpdateDataView } from './use_update_data_view'; import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx index 8b0150efa6126..230a7d2fee1e0 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { Provider } from 'react-redux'; import { useSourcererDataView } from '.'; @@ -137,8 +136,12 @@ jest.mock('../../common/lib/kibana', () => ({ type: 'keyword', }, }), + toSpec: () => ({ + id: dataViewId, + }), }) ), + getExistingIndices: jest.fn(() => [] as string[]), }, indexPatterns: { getTitles: jest.fn().mockImplementation(() => Promise.resolve(mockPatterns)), @@ -153,34 +156,35 @@ jest.mock('../../common/lib/kibana', () => ({ describe('Sourcerer Hooks', () => { let store = createMockStore(); + const StoreProvider: React.FC = ({ children }) => ( + {children} + ); + const Wrapper: React.FC = ({ children }) => ( + {children} + ); + beforeEach(() => { jest.clearAllMocks(); store = createMockStore(); mockUseUserInfo.mockImplementation(() => userInfoState); }); it('initializes loading default and timeline index patterns', async () => { - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - expect(mockDispatch).toBeCalledTimes(3); - expect(mockDispatch.mock.calls[0][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING', - payload: { id: 'security-solution', loading: true }, - }); - expect(mockDispatch.mock.calls[1][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', - payload: { - id: 'timeline', - selectedDataViewId: 'security-solution', - selectedPatterns: ['.siem-signals-spacename', ...DEFAULT_INDEX_PATTERN], - }, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING', + payload: { id: 'security-solution', loading: true }, + }); + expect(mockDispatch.mock.calls[1][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', + payload: { + id: 'timeline', + selectedDataViewId: 'security-solution', + selectedPatterns: ['.siem-signals-spacename', ...DEFAULT_INDEX_PATTERN], + }, }); }); it('sets signal index name', async () => { @@ -202,47 +206,42 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - mockUseUserInfo.mockImplementation(() => ({ - ...userInfoState, - loading: false, - signalIndexName: mockSourcererState.signalIndexName, - })); - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockDispatch.mock.calls[3][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', - payload: { loading: true }, - }); - expect(mockDispatch.mock.calls[4][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME', - payload: { signalIndexName: mockSourcererState.signalIndexName }, - }); - expect(mockDispatch.mock.calls[5][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING', - payload: { - id: mockSourcererState.defaultDataView.id, - loading: true, - }, - }); - expect(mockDispatch.mock.calls[6][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_DATA_VIEWS', - payload: mockNewDataViews, - }); - expect(mockDispatch.mock.calls[7][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', - payload: { loading: false }, - }); - expect(mockDispatch).toHaveBeenCalledTimes(8); - expect(mockSearch).toHaveBeenCalledTimes(2); + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + loading: false, + signalIndexName: mockSourcererState.signalIndexName, + })); + const { rerender } = renderHook(useInitSourcerer, { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockDispatch.mock.calls[3][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { loading: true }, + }); + expect(mockDispatch.mock.calls[4][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME', + payload: { signalIndexName: mockSourcererState.signalIndexName }, + }); + expect(mockDispatch.mock.calls[5][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING', + payload: { + id: mockSourcererState.defaultDataView.id, + loading: true, + }, + }); + expect(mockDispatch.mock.calls[6][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_DATA_VIEWS', + payload: mockNewDataViews, }); + expect(mockDispatch.mock.calls[7][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { loading: false }, + }); + + expect(mockSearch).toHaveBeenCalledTimes(2); }); }); @@ -258,8 +257,8 @@ describe('Sourcerer Hooks', () => { }) ); - renderHook, void>(() => useInitSourcerer(), { - wrapper: ({ children }) => {children}, + renderHook(() => useInitSourcerer(), { + wrapper: Wrapper, }); expect(mockDispatch).toHaveBeenCalledWith( @@ -278,8 +277,8 @@ describe('Sourcerer Hooks', () => { onInitialize(null) ); - renderHook, void>(() => useInitSourcerer(), { - wrapper: ({ children }) => {children}, + renderHook(() => useInitSourcerer(), { + wrapper: Wrapper, }); expect(updateUrlParam).toHaveBeenCalledWith({ @@ -302,16 +301,14 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - renderHook, void>(() => useInitSourcerer(), { - wrapper: ({ children }) => {children}, - }); + renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); - await waitFor(() => { - expect(mockAddWarning).toHaveBeenNthCalledWith(1, { - text: 'Users with write permission need to access the Elastic Security app to initialize the app source data.', - title: 'Write role required to generate data', - }); + await waitFor(() => { + expect(mockAddWarning).toHaveBeenNthCalledWith(1, { + text: 'Users with write permission need to access the Elastic Security app to initialize the app source data.', + title: 'Write role required to generate data', }); }); }); @@ -333,27 +330,25 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - mockUseUserInfo.mockImplementation(() => ({ - ...userInfoState, - loading: false, - signalIndexName: mockSourcererState.signalIndexName, - })); - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + loading: false, + signalIndexName: mockSourcererState.signalIndexName, + })); - await waitFor(() => { - expect(mockCreateSourcererDataView).toHaveBeenCalled(); - expect(mockAddError).not.toHaveBeenCalled(); - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, }); + + await waitFor(() => new Promise((resolve) => resolve(null))); + + rerender(); + + await waitFor(() => new Promise((resolve) => resolve(null))); + + expect(mockCreateSourcererDataView).toHaveBeenCalled(); + expect(mockAddError).not.toHaveBeenCalled(); }); it('does call addError if updateSourcererDataView receives a non-abort error', async () => { @@ -375,66 +370,51 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - mockUseUserInfo.mockImplementation(() => ({ - ...userInfoState, - loading: false, - signalIndexName: mockSourcererState.signalIndexName, - })); - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + loading: false, + signalIndexName: mockSourcererState.signalIndexName, + })); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); - await waitForNextUpdate(); - rerender(); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); - await waitFor(() => { - expect(mockAddError).toHaveBeenCalled(); - }); + await waitFor(() => { + expect(mockAddError).toHaveBeenCalled(); }); }); it('handles detections page', async () => { - await act(async () => { - mockUseUserInfo.mockImplementation(() => ({ - ...userInfoState, - signalIndexName: mockSourcererState.signalIndexName, - isSignalIndexExists: true, - })); - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(SourcererScopeName.detections), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - expect(mockDispatch.mock.calls[3][0]).toEqual({ - type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', - payload: { - id: 'detections', - selectedDataViewId: mockSourcererState.defaultDataView.id, - selectedPatterns: [mockSourcererState.signalIndexName], - }, - }); + mockUseUserInfo.mockImplementation(() => ({ + ...userInfoState, + signalIndexName: mockSourcererState.signalIndexName, + isSignalIndexExists: true, + })); + const { rerender } = renderHook(() => useInitSourcerer(SourcererScopeName.detections), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + expect(mockDispatch.mock.calls[3][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', + payload: { + id: 'detections', + selectedDataViewId: mockSourcererState.defaultDataView.id, + selectedPatterns: [mockSourcererState.signalIndexName], + }, }); }); it('index field search is not repeated when default and timeline have same dataViewId', async () => { - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(1); - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockSearch).toHaveBeenCalledTimes(1); }); }); it('index field search called twice when default and timeline have different dataViewId', async () => { @@ -451,18 +431,13 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(2); - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockSearch).toHaveBeenCalledTimes(2); }); }); describe('initialization settings', () => { @@ -476,21 +451,16 @@ describe('Sourcerer Hooks', () => { })); }); it('does not needToBeInit if scope is default and selectedPatterns/missingPatterns have values', async () => { - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ - dataViewId: mockSourcererState.defaultDataView.id, - needToBeInit: false, - scopeId: SourcererScopeName.default, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ + dataViewId: mockSourcererState.defaultDataView.id, + needToBeInit: false, + scopeId: SourcererScopeName.default, }); }); }); @@ -510,21 +480,16 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ - dataViewId: mockSourcererState.defaultDataView.id, - needToBeInit: true, - scopeId: SourcererScopeName.default, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenCalledWith({ + dataViewId: mockSourcererState.defaultDataView.id, + needToBeInit: true, + scopeId: SourcererScopeName.default, }); }); }); @@ -549,22 +514,17 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { - dataViewId: 'something-weird', - needToBeInit: true, - scopeId: SourcererScopeName.timeline, - skipScopeUpdate: false, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: true, + scopeId: SourcererScopeName.timeline, + skipScopeUpdate: false, }); }); }); @@ -589,22 +549,17 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { - dataViewId: 'something-weird', - needToBeInit: true, - scopeId: SourcererScopeName.timeline, - skipScopeUpdate: true, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: true, + scopeId: SourcererScopeName.timeline, + skipScopeUpdate: true, }); }); }); @@ -633,21 +588,16 @@ describe('Sourcerer Hooks', () => { }, }, }); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook, void>( - () => useInitSourcerer(), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - rerender(); - await waitFor(() => { - expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { - dataViewId: 'something-weird', - needToBeInit: false, - scopeId: SourcererScopeName.timeline, - }); + const { rerender } = renderHook(() => useInitSourcerer(), { + wrapper: StoreProvider, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { + expect(mockIndexFieldsSearch).toHaveBeenNthCalledWith(2, { + dataViewId: 'something-weird', + needToBeInit: false, + scopeId: SourcererScopeName.timeline, }); }); }); @@ -655,38 +605,39 @@ describe('Sourcerer Hooks', () => { describe('useSourcererDataView', () => { it('Should put any excludes in the index pattern at the end of the pattern list, and sort both the includes and excludes', async () => { - await act(async () => { - store = createMockStore({ - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.default]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - selectedPatterns: [ - '-packetbeat-*', - 'endgame-*', - 'auditbeat-*', - 'filebeat-*', - 'winlogbeat-*', - '-filebeat-*', - 'packetbeat-*', - 'traces-apm*', - 'apm-*-transaction*', - ], - }, + store = createMockStore({ + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + selectedPatterns: [ + '-packetbeat-*', + 'endgame-*', + 'auditbeat-*', + 'filebeat-*', + 'winlogbeat-*', + '-filebeat-*', + 'packetbeat-*', + 'traces-apm*', + 'apm-*-transaction*', + ], }, }, - }); - const { result, rerender, waitForNextUpdate } = renderHook< - React.PropsWithChildren<{}>, - SelectedDataView - >(() => useSourcererDataView(), { - wrapper: ({ children }) => {children}, - }); - await waitForNextUpdate(); - rerender(); + }, + }); + + const { result, rerender } = renderHook( + useSourcererDataView, + { + wrapper: StoreProvider, + } + ); + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender(); + await waitFor(() => { expect(result.current.selectedPatterns).toEqual([ 'apm-*-transaction*', 'auditbeat-*', @@ -703,12 +654,9 @@ describe('Sourcerer Hooks', () => { }); it('should update the title and name of the data view according to the selected patterns', async () => { - const { result, rerender } = renderHook, SelectedDataView>( - () => useSourcererDataView(), - { - wrapper: ({ children }) => {children}, - } - ); + const { result, rerender } = renderHook(() => useSourcererDataView(), { + wrapper: StoreProvider, + }); expect(result.current.sourcererDataView?.title).toBe( 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*' @@ -725,7 +673,7 @@ describe('Sourcerer Hooks', () => { ); }); - await rerender(); + rerender(); expect(result.current.sourcererDataView?.title).toBe(testPatterns.join(',')); expect(result.current.sourcererDataView?.name).toBe(testPatterns.join(',')); diff --git a/x-pack/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.test.tsx index 5bb0cc11ebffb..43f6f5d8a0a1f 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/containers/use_signal_helpers.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { createMockStore, mockGlobalState, TestProviders } from '../../common/mock'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { useSignalHelpers } from './use_signal_helpers'; import type { State } from '../../common/store'; import { createSourcererDataView } from './create_sourcerer_data_view'; @@ -41,14 +41,12 @@ describe('useSignalHelpers', () => { ); test('Default state, does not need init and does not need poll', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), { - wrapper: wrapperContainer, - }); - await waitForNextUpdate(); - expect(result.current.signalIndexNeedsInit).toEqual(false); - expect(result.current.pollForSignalIndex).toEqual(undefined); + const { result } = renderHook(() => useSignalHelpers(), { + wrapper: wrapperContainer, }); + await waitFor(() => new Promise((resolve) => resolve(null))); + expect(result.current.signalIndexNeedsInit).toEqual(false); + expect(result.current.pollForSignalIndex).toEqual(undefined); }); test('Needs init and does not need poll when signal index is not yet in default data view', async () => { const state: State = { @@ -70,16 +68,14 @@ describe('useSignalHelpers', () => { }, }; const store = createMockStore(state); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), { - wrapper: ({ children }: React.PropsWithChildren<{}>) => ( - {children} - ), - }); - await waitForNextUpdate(); - expect(result.current.signalIndexNeedsInit).toEqual(true); - expect(result.current.pollForSignalIndex).toEqual(undefined); + const { result } = renderHook(() => useSignalHelpers(), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + {children} + ), }); + await waitFor(() => new Promise((resolve) => resolve(null))); + expect(result.current.signalIndexNeedsInit).toEqual(true); + expect(result.current.pollForSignalIndex).toEqual(undefined); }); test('Init happened and signal index does not have data yet, poll function becomes available', async () => { const state: State = { @@ -101,16 +97,14 @@ describe('useSignalHelpers', () => { }, }; const store = createMockStore(state); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), { - wrapper: ({ children }: React.PropsWithChildren<{}>) => ( - {children} - ), - }); - await waitForNextUpdate(); - expect(result.current.signalIndexNeedsInit).toEqual(false); - expect(result.current.pollForSignalIndex).not.toEqual(undefined); + const { result } = renderHook(() => useSignalHelpers(), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + {children} + ), }); + await waitFor(() => new Promise((resolve) => resolve(null))); + expect(result.current.signalIndexNeedsInit).toEqual(false); + expect(result.current.pollForSignalIndex).not.toEqual(undefined); }); test('Init happened and signal index does not have data yet, poll function becomes available but createSourcererDataView throws an abort error', async () => { @@ -134,17 +128,15 @@ describe('useSignalHelpers', () => { }, }; const store = createMockStore(state); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), { - wrapper: ({ children }: React.PropsWithChildren<{}>) => ( - {children} - ), - }); - await waitForNextUpdate(); - expect(result.current.signalIndexNeedsInit).toEqual(false); - expect(result.current.pollForSignalIndex).not.toEqual(undefined); - expect(mockAddError).not.toHaveBeenCalled(); + const { result } = renderHook(() => useSignalHelpers(), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + {children} + ), }); + await waitFor(() => new Promise((resolve) => resolve(null))); + expect(result.current.signalIndexNeedsInit).toEqual(false); + expect(result.current.pollForSignalIndex).not.toEqual(undefined); + expect(mockAddError).not.toHaveBeenCalled(); }); test('Init happened and signal index does not have data yet, poll function becomes available but createSourcererDataView throws a non-abort error', async () => { @@ -170,17 +162,15 @@ describe('useSignalHelpers', () => { }, }; const store = createMockStore(state); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), { - wrapper: ({ children }: React.PropsWithChildren<{}>) => ( - {children} - ), - }); - await waitForNextUpdate(); - expect(result.current.signalIndexNeedsInit).toEqual(false); - expect(result.current.pollForSignalIndex).not.toEqual(undefined); - result.current.pollForSignalIndex?.(); - expect(mockAddError).toHaveBeenCalled(); + const { result } = renderHook(() => useSignalHelpers(), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + {children} + ), }); + await waitFor(() => new Promise((resolve) => resolve(null))); + expect(result.current.signalIndexNeedsInit).toEqual(false); + expect(result.current.pollForSignalIndex).not.toEqual(undefined); + result.current.pollForSignalIndex?.(); + expect(mockAddError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx index 4c1c72500ae8b..01c9dd701292a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx @@ -5,19 +5,18 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import React from 'react'; -import type { UseCreateFieldButton, UseCreateFieldButtonProps } from '.'; +import type { UseCreateFieldButtonProps } from '.'; import { useCreateFieldButton } from '.'; import { TestProviders } from '../../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; const mockOpenFieldEditor = jest.fn(); const mockOnHide = jest.fn(); const renderUseCreateFieldButton = (props: Partial = {}) => - renderHook, ReturnType>( + renderHook( () => useCreateFieldButton({ isAllowed: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.test.tsx index 8d82c3402af7c..a565a31ef67a7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.test.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; -import type { UseFieldTableColumnsProps, UseFieldTableColumns } from '.'; +import { render, renderHook } from '@testing-library/react'; +import type { UseFieldTableColumnsProps } from '.'; import { useFieldTableColumns } from '.'; import { TestProviders } from '../../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; import { EuiInMemoryTable } from '@elastic/eui'; import type { BrowserFieldItem } from '@kbn/triggers-actions-ui-plugin/public/types'; @@ -21,7 +20,7 @@ const mockOpenDeleteFieldModal = jest.fn(); // helper function to render the hook const renderUseFieldTableColumns = (props: Partial = {}) => - renderHook, ReturnType>( + renderHook( () => useFieldTableColumns({ hasFieldEditPermission: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 67094a18cf327..7b67fb6614adf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { render, act } from '@testing-library/react'; +import type { RenderHookResult } from '@testing-library/react'; +import { render, act, waitFor, renderHook } from '@testing-library/react'; import type { Store } from 'redux'; import type { UseFieldBrowserOptionsProps, UseFieldBrowserOptions, FieldEditorActionsRef } from '.'; import { useFieldBrowserOptions } from '.'; @@ -16,8 +17,6 @@ import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-p import { TestProviders } from '../../../common/mock'; import { useKibana } from '../../../common/lib/kibana'; import type { DataView, DataViewField } from '@kbn/data-plugin/common'; -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook } from '@testing-library/react-hooks'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; @@ -42,12 +41,13 @@ const mockOnHide = jest.fn(); const runAllPromises = () => new Promise(setImmediate); // helper function to render the hook -const renderUseFieldBrowserOptions = ( - props: Partial = {} -) => +const renderUseFieldBrowserOptions = ({ + store, + ...props +}: Partial = {}) => renderHook< - React.PropsWithChildren, - ReturnType + ReturnType, + React.PropsWithChildren >( () => useFieldBrowserOptions({ @@ -57,7 +57,7 @@ const renderUseFieldBrowserOptions = ( ...props, }), { - wrapper: ({ children, store }) => { + wrapper: ({ children }) => { if (store) { return {children}; } @@ -71,12 +71,12 @@ const renderUpdatedUseFieldBrowserOptions = async ( props: Partial = {} ) => { let renderHookResult: RenderHookResult< - UseFieldBrowserOptionsProps, - ReturnType + ReturnType, + UseFieldBrowserOptionsProps > | null = null; await act(async () => { renderHookResult = renderUseFieldBrowserOptions(props); - await renderHookResult.waitForNextUpdate(); + await waitFor(() => new Promise((resolve) => resolve(null))); }); return renderHookResult!; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 525d8bba3d909..14fed472e0e1f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -6,8 +6,7 @@ */ import { cloneDeep, getOr, omit } from 'lodash/fp'; -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; +import { waitFor, renderHook } from '@testing-library/react'; import { mockTimelineResults, mockGetOneTimelineResult } from '../../../common/mock'; import { timelineDefaults } from '../../store/defaults'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index da56cb12b4a00..5a3ba28a82767 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; import { mount } from 'enzyme'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor, renderHook } from '@testing-library/react'; import { useHistory, useParams } from 'react-router-dom'; import '../../../common/mock/formatted_relative'; @@ -30,7 +29,6 @@ import { NotePreviews } from './note_previews'; import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { StatefulOpenTimeline } from '.'; import { TimelineTabsStyle } from './types'; -import type { UseTimelineTypesArgs, UseTimelineTypesResult } from './use_timeline_types'; import { useTimelineTypes } from './use_timeline_types'; import { deleteTimelinesByIds } from '../../containers/api'; import { useUserPrivileges } from '../../../common/components/user_privileges'; @@ -165,12 +163,12 @@ describe('StatefulOpenTimeline', () => { describe("Template timelines' tab", () => { test("should land on correct timelines' tab with url timelines/default", () => { - const { result } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), { - wrapper: ({ children }) => {children}, - }); + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); expect(result.current.timelineType).toBe(TimelineTypeEnum.default); }); @@ -181,12 +179,12 @@ describe('StatefulOpenTimeline', () => { pageName: SecurityPageName.timelines, }); - const { result } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), { - wrapper: ({ children }) => {children}, - }); + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); expect(result.current.timelineType).toBe(TimelineTypeEnum.template); }); @@ -223,12 +221,12 @@ describe('StatefulOpenTimeline', () => { pageName: SecurityPageName.case, }); - const { result } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), { - wrapper: ({ children }) => {children}, - }); + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 0 }), + { + wrapper: ({ children }) => {children}, + } + ); expect(result.current.timelineType).toBe(TimelineTypeEnum.default); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/hooks/use_delete_note.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/hooks/use_delete_note.test.ts index b2c88364a8209..d9dd243357d5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/hooks/use_delete_note.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/hooks/use_delete_note.test.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; + +import { renderHook, act } from '@testing-library/react'; import { useKibana } from '../../../../../common/lib/kibana'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { appActions } from '../../../../../common/store/app'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx index 66d3a41bc0d48..22e03955afbc2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import type { UseTimelineTypesArgs, UseTimelineTypesResult } from './use_timeline_types'; +import { fireEvent, render, waitFor, screen, renderHook } from '@testing-library/react'; import { useTimelineTypes } from './use_timeline_types'; import { TestProviders } from '../../../common/mock'; @@ -31,12 +29,14 @@ jest.mock('../../../common/components/link_to', () => { }; }); +const mockNavigateToUrl = jest.fn(); + jest.mock('@kbn/kibana-react-plugin/public', () => { const originalModule = jest.requireActual('@kbn/kibana-react-plugin/public'); const useKibana = jest.fn().mockImplementation(() => ({ services: { application: { - navigateToUrl: jest.fn(), + navigateToUrl: mockNavigateToUrl, }, }, })); @@ -48,92 +48,83 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }); describe('useTimelineTypes', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); - expect(result.current).toEqual({ - timelineType: 'default', - timelineTabs: result.current.timelineTabs, - timelineFilters: result.current.timelineFilters, - }); + } + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, }); }); describe('timelineTabs', () => { it('render timelineTabs', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); - - const { container } = render(result.current.timelineTabs); - expect( - container.querySelector('[data-test-subj="timeline-tab-default"]') - ).toHaveTextContent('Timelines'); - expect( - container.querySelector('[data-test-subj="timeline-tab-template"]') - ).toHaveTextContent('Templates'); - }); + } + ); + await waitFor(() => new Promise((resolve) => resolve(null))); + + render(result.current.timelineTabs); + expect(screen.getByTestId('timeline-tab-default')).toHaveTextContent('Timelines'); + expect(screen.getByTestId('timeline-tab-template')).toHaveTextContent('Templates'); }); it('set timelineTypes correctly', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); + } + ); - const { container } = render(result.current.timelineTabs); + await waitFor(() => expect(result.current.timelineTabs).toBeDefined()); - fireEvent( - container.querySelector('[data-test-subj="timeline-tab-template"]')!, - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); + const { container } = render(result.current.timelineTabs); - expect(result.current).toEqual({ - timelineType: 'template', - timelineTabs: result.current.timelineTabs, - timelineFilters: result.current.timelineFilters, - }); - }); + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(mockNavigateToUrl).toHaveBeenCalled(); }); it('stays in the same tab if clicking again on current tab', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); + } + ); + await waitFor(() => new Promise((resolve) => resolve(null))); - const { container } = render(result.current.timelineTabs); + render(result.current.timelineTabs); - fireEvent( - container.querySelector('[data-test-subj="timeline-tab-default"]')!, - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); + fireEvent( + screen.getByTestId('timeline-tab-default'), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + await waitFor(() => { expect(result.current).toEqual({ timelineType: 'default', timelineTabs: result.current.timelineTabs, @@ -145,79 +136,77 @@ describe('useTimelineTypes', () => { describe('timelineFilters', () => { it('render timelineFilters', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); - - const { container } = render(<>{result.current.timelineFilters}); - expect( - container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]') - ).toHaveTextContent('Timelines'); - expect( - container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]') - ).toHaveTextContent('Templates'); - }); + } + ); + await waitFor(() => new Promise((resolve) => resolve(null))); + + const { container } = render(<>{result.current.timelineFilters}); + + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]') + ).toHaveTextContent('Templates'); }); it('set timelineTypes correctly', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); + } + ); - const { container } = render(<>{result.current.timelineFilters}); + await waitFor(() => expect(result.current.timelineFilters).toBeDefined()); - fireEvent( - container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')!, - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); + render(<>{result.current.timelineFilters}); - expect(result.current).toEqual({ - timelineType: 'template', + await waitFor(() => new Promise((resolve) => resolve(null))); + + fireEvent.click(screen.getByTestId('open-timeline-modal-body-filter-template')); + + await waitFor(() => expect(result.current.timelineType).toEqual('template')); + + expect(result.current).toEqual( + expect.objectContaining({ timelineTabs: result.current.timelineTabs, timelineFilters: result.current.timelineFilters, - }); - }); + }) + ); }); it('stays in the same tab if clicking again on current tab', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - React.PropsWithChildren, - UseTimelineTypesResult - >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), { + const { result } = renderHook( + () => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }), + { wrapper: TestProviders, - }); - await waitForNextUpdate(); + } + ); - const { container } = render(<>{result.current.timelineFilters}); + await waitFor(() => new Promise((resolve) => resolve(null))); - fireEvent( - container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!, - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); + const { container } = render(<>{result.current.timelineFilters}); + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + await waitFor(() => expect(result.current).toEqual({ timelineType: 'default', timelineTabs: result.current.timelineTabs, timelineFilters: result.current.timelineFilters, - }); - }); + }) + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx index b2c990b12eced..6d052a3f8b2d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_update_timeline.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { @@ -79,7 +79,10 @@ describe('dispatchUpdateTimeline', () => { beforeEach(() => { jest.clearAllMocks(); - clock = sinon.useFakeTimers(unix); + clock = sinon.useFakeTimers({ + now: unix, + toFake: ['Date'], + }); }); afterEach(function () { @@ -87,128 +90,134 @@ describe('dispatchUpdateTimeline', () => { }); it('it invokes date range picker dispatch', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + + act(() => { result.current(defaultArgs); + }); - expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ - from: '2020-03-26T14:35:56.356Z', - to: '2020-03-26T14:41:56.356Z', - }); + expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ + from: '2020-03-26T14:35:56.356Z', + to: '2020-03-26T14:41:56.356Z', }); }); it('it invokes add timeline dispatch', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + + act(() => { result.current(defaultArgs); + }); - expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: TimelineId.active, - savedTimeline: true, - timeline: { - ...mockTimelineModel, - version: null, - updated: undefined, - changed: true, - }, - }); + expect(dispatchAddTimeline).toHaveBeenCalledWith({ + id: TimelineId.active, + savedTimeline: true, + timeline: { + ...mockTimelineModel, + version: null, + updated: undefined, + changed: true, + }, }); }); it('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); - result.current(defaultArgs); + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + act(() => { + result.current(defaultArgs); }); + + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); }); it('it does not invoke notes dispatch if duplicate is true', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + act(() => { result.current(defaultArgs); - - expect(dispatchAddNotes).not.toHaveBeenCalled(); }); + + expect(dispatchAddNotes).not.toHaveBeenCalled(); }); it('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: null, - serializedQuery: 'some-serialized-query', - }, + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: null, + serializedQuery: 'some-serialized-query', }, - }; + }, + }; + + act(() => { result.current({ ...defaultArgs, timeline: mockTimeline, }); - - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); }); + + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); }); it('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, - serializedQuery: 'some-serialized-query', - }, + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, + serializedQuery: 'some-serialized-query', }, - }; + }, + }; + + act(() => { result.current({ ...defaultArgs, timeline: mockTimeline, }); + }); - expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: TimelineId.active, - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'expression', - }, - serializedQuery: 'some-serialized-query', + expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ + id: TimelineId.active, + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'expression', }, - }); + serializedQuery: 'some-serialized-query', + }, }); }); it('it invokes dispatchAddNotes if duplicate is false', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + + act(() => { result.current({ ...defaultArgs, duplicate: false, @@ -223,53 +232,55 @@ describe('dispatchUpdateTimeline', () => { }, ], }); + }); - expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).not.toHaveBeenCalled(); - expect(dispatchAddNotes).toHaveBeenCalledWith({ - notes: [ - { - created: new Date('2020-03-26T14:35:56.356Z'), - eventId: null, - id: 'note-id', - lastEdit: new Date('2020-03-26T14:35:56.356Z'), - note: 'I am a note', - user: 'unknown', - saveObjectId: 'note-id', - timelineId: 'abc', - version: 'testVersion', - }, - ], - }); + expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).not.toHaveBeenCalled(); + expect(dispatchAddNotes).toHaveBeenCalledWith({ + notes: [ + { + created: new Date('2020-03-26T14:35:56.356Z'), + eventId: null, + id: 'note-id', + lastEdit: new Date('2020-03-26T14:35:56.356Z'), + note: 'I am a note', + user: 'unknown', + saveObjectId: 'note-id', + timelineId: 'abc', + version: 'testVersion', + }, + ], }); }); it('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', async () => { + const { result } = renderHook(() => useUpdateTimeline(), { + wrapper: TestProviders, + }); + await waitFor(() => new Promise((resolve) => resolve(null))); + await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useUpdateTimeline(), { - wrapper: TestProviders, - }); - await waitForNextUpdate(); result.current({ ...defaultArgs, ruleNote: '# this would be some markdown', }); - const expectedNote: Note = { - created: new Date(anchor), - id: 'uuidv4()', - lastEdit: null, - note: '# this would be some markdown', - saveObjectId: null, - user: 'elastic', - version: null, - }; + }); - expect(dispatchAddNotes).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); - expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: TimelineId.active, - noteId: 'uuidv4()', - }); + const expectedNote: Note = { + created: new Date(anchor), + id: 'uuidv4()', + lastEdit: null, + note: '# this would be some markdown', + saveObjectId: null, + user: 'elastic', + version: null, + }; + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); + expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ + id: TimelineId.active, + noteId: 'uuidv4()', }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx index 32b5b8bc3c129..fd50be53c6a85 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx @@ -7,11 +7,10 @@ import React from 'react'; import { TimelineId, TimelineTabs } from '../../../../../common/types'; -import { renderHook, act } from '@testing-library/react-hooks/dom'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock'; import type { UseNotesInFlyoutArgs } from './use_notes_in_flyout'; import { useNotesInFlyout } from './use_notes_in_flyout'; -import { waitFor } from '@testing-library/react'; import { useDispatch } from 'react-redux'; jest.mock('react-redux', () => ({ @@ -202,8 +201,8 @@ describe('useNotesInFlyout', () => { expect(result.current.isNotesFlyoutVisible).toBe(false); }); - it('should close the flyout when activeTab is changed', () => { - const { result, rerender, waitForNextUpdate } = renderTestHook(); + it('should close the flyout when activeTab is changed', async () => { + const { result, rerender } = renderTestHook(); act(() => { result.current.setNotesEventId('event-1'); @@ -226,8 +225,6 @@ describe('useNotesInFlyout', () => { rerender({ activeTab: TimelineTabs.eql }); }); - waitForNextUpdate(); - - expect(result.current.isNotesFlyoutVisible).toBe(false); + await waitFor(() => expect(result.current.isNotesFlyoutVisible).toBe(false)); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/customizations/use_histogram_customizations.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/customizations/use_histogram_customizations.test.ts index d40b417b95218..10dfa97f35bdc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/customizations/use_histogram_customizations.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/customizations/use_histogram_customizations.test.ts @@ -11,7 +11,7 @@ import type { ClickTriggerEvent, MultiClickTriggerEvent, } from '@kbn/charts-plugin/public'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import type { DiscoverStateContainer, UnifiedHistogramCustomization, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/use_get_stateful_query_bar.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/use_get_stateful_query_bar.test.tsx index 17b018ffea99d..fe26d8780a903 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/use_get_stateful_query_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/use_get_stateful_query_bar.test.tsx @@ -6,7 +6,7 @@ */ import { TestProviders } from '../../../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useGetStatefulQueryBar } from './use_get_stateful_query_bar'; describe('useGetStatefulQueryBar', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx index 9c4b135b5e774..54155a493da64 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx @@ -8,8 +8,7 @@ import type { PropsWithChildren } from 'react'; import React, { memo } from 'react'; -import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; +import { render, renderHook } from '@testing-library/react'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { mockTimelineModel, TestProviders } from '../../../../../common/mock'; import { useKibana } from '../../../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts index 926082ff9ed41..66162fe82e458 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts @@ -6,7 +6,7 @@ */ import { TestProviders } from '../../../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useTimelineColumns } from './use_timeline_columns'; import { defaultUdtHeaders } from '../../body/column_headers/default_headers'; import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx index efbe954250037..8168742d4e08d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx @@ -6,7 +6,7 @@ */ import type { EuiDataGridControlColumn } from '@elastic/eui'; import { TestProviders } from '../../../../../common/mock'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useLicense } from '../../../../../common/hooks/use_license'; import { useTimelineControlColumn } from './use_timeline_control_columns'; import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns'; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index ba0151ff77b59..40c6e89478e2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -6,7 +6,7 @@ */ import { DataLoadingState } from '@kbn/unified-data-table'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import type { TimelineArgs, UseTimelineEventsProps } from '.'; import { initSortDefault, useTimelineEvents } from '.'; import { SecurityPageName } from '../../../common/constants'; @@ -15,7 +15,6 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime import { mockTimelineData } from '../../common/mock'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { useFetchNotes } from '../../notes/hooks/use_fetch_notes'; -import { waitFor } from '@testing-library/dom'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -142,136 +141,105 @@ describe('useTimelineEvents', () => { }; test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props }, - }); - - // useEffect on params request - await waitForNextUpdate(); + const { result } = renderHook((args) => useTimelineEvents(args), { + initialProps: props, + }); + + expect(result.current).toEqual([ + DataLoadingState.loading, + { + events: [], + id: TimelineId.active, + inspect: expect.objectContaining({ dsl: [], response: [] }), + loadPage: expect.any(Function), + pageInfo: expect.objectContaining({ + activePage: 0, + querySize: 0, + }), + refetch: expect.any(Function), + totalCount: -1, + refreshedAt: 0, + }, + ]); + }); + + test('happy path query', async () => { + const { result, rerender } = renderHook< + [DataLoadingState, TimelineArgs], + UseTimelineEventsProps + >((args) => useTimelineEvents(args), { + initialProps: { ...props, startDate: '', endDate: '' }, + }); + + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitFor(() => { + expect(mockSearch).toHaveBeenCalledTimes(2); expect(result.current).toEqual([ DataLoadingState.loaded, { - events: [], + events: mockEvents, id: TimelineId.active, inspect: result.current[1].inspect, loadPage: result.current[1].loadPage, pageInfo: result.current[1].pageInfo, refetch: result.current[1].refetch, - totalCount: -1, - refreshedAt: 0, + totalCount: 32, + refreshedAt: result.current[1].refreshedAt, }, ]); }); }); - test('happy path query', async () => { - await act(async () => { - const { result, waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props, startDate: '', endDate: '' }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ ...props, startDate, endDate }); - // useEffect on params request - await waitForNextUpdate(); - - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(2); - expect(result.current).toEqual([ - DataLoadingState.loaded, - { - events: mockEvents, - id: TimelineId.active, - inspect: result.current[1].inspect, - loadPage: result.current[1].loadPage, - pageInfo: result.current[1].pageInfo, - refetch: result.current[1].refetch, - totalCount: 32, - refreshedAt: result.current[1].refreshedAt, - }, - ]); - }); + test('Mock cache for active timeline when switching page', async () => { + const { result, rerender } = renderHook< + [DataLoadingState, TimelineArgs], + UseTimelineEventsProps + >((args) => useTimelineEvents(args), { + initialProps: { ...props, startDate: '', endDate: '' }, }); - }); - test('Mock cache for active timeline when switching page', async () => { - await act(async () => { - const { result, waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props, startDate: '', endDate: '' }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ ...props, startDate, endDate }); - // useEffect on params request - await waitForNextUpdate(); - - mockUseRouteSpy.mockReturnValue([ + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ ...props, startDate, endDate }); + + mockUseRouteSpy.mockReturnValue([ + { + pageName: SecurityPageName.timelines, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/timelines', + }, + ]); + + await waitFor(() => { + expect(mockSearch).toHaveBeenCalledTimes(2); + expect(result.current).toEqual([ + DataLoadingState.loaded, { - pageName: SecurityPageName.timelines, - detailName: undefined, - tabName: undefined, - search: '', - pathName: '/timelines', + events: mockEvents, + id: TimelineId.active, + inspect: result.current[1].inspect, + loadPage: result.current[1].loadPage, + pageInfo: result.current[1].pageInfo, + refetch: result.current[1].refetch, + totalCount: 32, + refreshedAt: result.current[1].refreshedAt, }, ]); - - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(2); - - expect(result.current).toEqual([ - DataLoadingState.loaded, - { - events: mockEvents, - id: TimelineId.active, - inspect: result.current[1].inspect, - loadPage: result.current[1].loadPage, - pageInfo: result.current[1].pageInfo, - refetch: result.current[1].refetch, - totalCount: 32, - refreshedAt: result.current[1].refreshedAt, - }, - ]); - }); }); }); test('Correlation pagination is calling search strategy when switching page', async () => { - await act(async () => { - const { result, waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { - ...props, - language: 'eql', - eqlOptions: { - eventCategoryField: 'category', - tiebreakerField: '', - timestampField: '@timestamp', - query: 'find it EQL', - size: 100, - }, - }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ + const { result, rerender } = renderHook< + [DataLoadingState, TimelineArgs], + UseTimelineEventsProps + >((args) => useTimelineEvents(args), { + initialProps: { ...props, - startDate, - endDate, language: 'eql', eqlOptions: { eventCategoryField: 'category', @@ -280,119 +248,121 @@ describe('useTimelineEvents', () => { query: 'find it EQL', size: 100, }, - }); - // useEffect on params request - await waitForNextUpdate(); - mockSearch.mockReset(); + }, + }); + + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ + ...props, + startDate, + endDate, + language: 'eql', + eqlOptions: { + eventCategoryField: 'category', + tiebreakerField: '', + timestampField: '@timestamp', + query: 'find it EQL', + size: 100, + }, + }); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + mockSearch.mockReset(); + act(() => { result.current[1].loadPage(4); - await waitForNextUpdate(); - expect(mockSearch).toHaveBeenCalledTimes(1); }); + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1)); }); test('should query again when a new field is added', async () => { - await act(async () => { - const { waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props, startDate: '', endDate: '' }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ ...props, startDate, endDate }); - // useEffect on params request - await waitForNextUpdate(); + const { rerender } = renderHook((args) => useTimelineEvents(args), { + initialProps: { ...props, startDate: '', endDate: '' }, + }); - expect(mockSearch).toHaveBeenCalledTimes(2); - mockSearch.mockClear(); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); - rerender({ - ...props, - startDate, - endDate, - fields: ['@timestamp', 'event.kind', 'event.category'], - }); - - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(1); - }); + expect(mockSearch).toHaveBeenCalledTimes(2); + mockSearch.mockClear(); + + rerender({ + ...props, + startDate, + endDate, + fields: ['@timestamp', 'event.kind', 'event.category'], }); + + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1)); }); test('should not query again when a field is removed', async () => { - await act(async () => { - const { waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props, startDate: '', endDate: '' }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ ...props, startDate, endDate }); - // useEffect on params request - await waitForNextUpdate(); + const { rerender } = renderHook((args) => useTimelineEvents(args), { + initialProps: { ...props, startDate: '', endDate: '' }, + }); - expect(mockSearch).toHaveBeenCalledTimes(2); - mockSearch.mockClear(); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); - rerender({ ...props, startDate, endDate, fields: ['@timestamp'] }); + expect(mockSearch).toHaveBeenCalledTimes(2); + mockSearch.mockClear(); - expect(mockSearch).toHaveBeenCalledTimes(0); - }); + rerender({ ...props, startDate, endDate, fields: ['@timestamp'] }); + + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0)); }); test('should not query again when a removed field is added back', async () => { - await act(async () => { - const { waitForNextUpdate, rerender } = renderHook< - UseTimelineEventsProps, - [DataLoadingState, TimelineArgs] - >((args) => useTimelineEvents(args), { - initialProps: { ...props, startDate: '', endDate: '' }, - }); - - // useEffect on params request - await waitForNextUpdate(); - rerender({ ...props, startDate, endDate }); - // useEffect on params request - await waitForNextUpdate(); + const { rerender } = renderHook((args) => useTimelineEvents(args), { + initialProps: { ...props, startDate: '', endDate: '' }, + }); - expect(mockSearch).toHaveBeenCalledTimes(2); - mockSearch.mockClear(); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitFor(() => new Promise((resolve) => resolve(null))); - // remove `event.kind` from default fields - rerender({ ...props, startDate, endDate, fields: ['@timestamp'] }); + expect(mockSearch).toHaveBeenCalledTimes(2); + mockSearch.mockClear(); - expect(mockSearch).toHaveBeenCalledTimes(0); + // remove `event.kind` from default fields + rerender({ ...props, startDate, endDate, fields: ['@timestamp'] }); - // request default Fields - rerender({ ...props, startDate, endDate }); + await waitFor(() => new Promise((resolve) => resolve(null))); - expect(mockSearch).toHaveBeenCalledTimes(0); - }); + expect(mockSearch).toHaveBeenCalledTimes(0); + + // request default Fields + rerender({ ...props, startDate, endDate }); + + // since there is no new update in useEffect, it should throw an timeout error + // await expect(waitFor(() => null)).rejects.toThrowError(); + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(0)); }); test('should return the combined list of events for all the pages when multiple pages are queried', async () => { - await act(async () => { - const { result } = renderHook((args) => useTimelineEvents(args), { - initialProps: { ...props }, - }); - await waitFor(() => { - expect(result.current[1].events).toHaveLength(10); - }); - - result.current[1].loadPage(1); - - await waitFor(() => { - expect(result.current[0]).toEqual(DataLoadingState.loadingMore); - }); - - await waitFor(() => { - expect(result.current[1].events).toHaveLength(20); - }); + const { result } = renderHook((args) => useTimelineEvents(args), { + initialProps: { ...props }, + }); + await waitFor(() => { + expect(result.current[1].events).toHaveLength(10); + }); + + result.current[1].loadPage(1); + + await waitFor(() => { + expect(result.current[0]).toEqual(DataLoadingState.loadingMore); + }); + + await waitFor(() => { + expect(result.current[1].events).toHaveLength(20); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.test.tsx index 9142aca78424c..db9413df30595 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { mockGlobalState, TestProviders, createMockStore } from '../../common/mock'; import { useTimelineDataFilters } from './use_timeline_data_filters'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index a4c054371a316..0864c2bb024bd 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useCreateTimeline } from './use_create_timeline'; import type { TimeRange } from '../../common/store/inputs/model'; import { RowRendererCount, TimelineTypeEnum } from '../../../common/api/timeline'; diff --git a/x-pack/plugins/threat_intelligence/public/hooks/use_integrations.test.tsx b/x-pack/plugins/threat_intelligence/public/hooks/use_integrations.test.tsx index 2df9bea337ba5..e13fb396808d2 100644 --- a/x-pack/plugins/threat_intelligence/public/hooks/use_integrations.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/hooks/use_integrations.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; import { INSTALLATION_STATUS, THREAT_INTELLIGENCE_CATEGORY } from '../utils/filter_integrations'; @@ -25,9 +25,7 @@ const renderUseQuery = (result: { items: any[] }) => describe('useIntegrations', () => { it('should have undefined data during loading state', async () => { const mockIntegrations = { items: [] }; - const { result, waitFor } = renderUseQuery(mockIntegrations); - - await waitFor(() => result.current.isLoading); + const { result } = renderUseQuery(mockIntegrations); expect(result.current.isLoading).toBeTruthy(); expect(result.current.data).toBeUndefined(); @@ -43,9 +41,9 @@ describe('useIntegrations', () => { }, ], }; - const { result, waitFor } = renderUseQuery(mockIntegrations); + const { result } = renderUseQuery(mockIntegrations); - await waitFor(() => result.current.isSuccess); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.isLoading).toBeFalsy(); expect(result.current.data).toEqual(mockIntegrations); diff --git a/x-pack/plugins/threat_intelligence/public/modules/block_list/hooks/use_policies.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/block_list/hooks/use_policies.test.tsx index a01b7da64b684..4a1af74a100a3 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/block_list/hooks/use_policies.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/block_list/hooks/use_policies.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { waitFor, renderHook } from '@testing-library/react'; import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; const createWrapper = () => { @@ -24,9 +24,7 @@ const renderUseQuery = (result: { items: any[] }) => describe('usePolicies', () => { it('should have undefined data during loading state', async () => { const mockPolicies = { items: [] }; - const { result, waitFor } = renderUseQuery(mockPolicies); - - await waitFor(() => result.current.isLoading); + const { result } = renderUseQuery(mockPolicies); expect(result.current.isLoading).toBeTruthy(); expect(result.current.data).toBeUndefined(); @@ -41,9 +39,9 @@ describe('usePolicies', () => { }, ], }; - const { result, waitFor } = renderUseQuery(mockPolicies); + const { result } = renderUseQuery(mockPolicies); - await waitFor(() => result.current.isSuccess); + await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); expect(result.current.isLoading).toBeFalsy(); expect(result.current.data).toEqual(mockPolicies); diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx index 8e2f5d3d96a25..a38b25fce0f61 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx @@ -6,7 +6,7 @@ */ import React, { FC, ReactNode } from 'react'; -import { Renderer, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react'; import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { KibanaContext } from '../../../hooks/use_kibana'; import { useCaseDisabled } from './use_case_permission'; @@ -27,7 +27,7 @@ const getProviderComponent = ); describe('useCasePermission', () => { - let hookResult: RenderHookResult<{}, boolean, Renderer>; + let hookResult: RenderHookResult; it('should return false if user has correct permissions and indicator has a name', () => { const mockedServices = { diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.test.tsx index 0c23962ccaa46..e9f43c316190a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { renderHook } from '@testing-library/react-hooks'; -import { useIndicatorById, UseIndicatorByIdValue } from './use_indicator_by_id'; +import { waitFor, renderHook } from '@testing-library/react'; +import { useIndicatorById } from './use_indicator_by_id'; import { TestProvidersComponent } from '../../../mocks/test_providers'; import { createFetchIndicatorById } from '../services/fetch_indicator_by_id'; import { Indicator } from '../../../../common/types/indicator'; @@ -16,13 +16,10 @@ jest.mock('../services/fetch_indicator_by_id'); const indicatorByIdQueryResult = { _id: 'testId' } as unknown as Indicator; const renderUseIndicatorById = (initialProps = { indicatorId: 'testId' }) => - renderHook<{ indicatorId: string }, UseIndicatorByIdValue>( - (props) => useIndicatorById(props.indicatorId), - { - initialProps, - wrapper: TestProvidersComponent, - } - ); + renderHook((props) => useIndicatorById(props.indicatorId), { + initialProps, + wrapper: TestProvidersComponent, + }); describe('useIndicatorById()', () => { type MockedCreateFetchIndicators = jest.MockedFunction; @@ -49,8 +46,7 @@ describe('useIndicatorById()', () => { expect(indicatorsQuery).toHaveBeenCalledTimes(1); // isLoading should turn to false eventually - await hookResult.waitFor(() => !hookResult.result.current.isLoading); - expect(hookResult.result.current.isLoading).toEqual(false); + await waitFor(() => expect(hookResult.result.current.isLoading).toBe(false)); }); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx index 11e1d03a32b51..72935990ef71f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; +import { act, waitFor, renderHook } from '@testing-library/react'; import { useAggregatedIndicators, UseAggregatedIndicatorsParam } from './use_aggregated_indicators'; import { mockedTimefilterService, TestProvidersComponent } from '../../../mocks/test_providers'; import { createFetchAggregatedIndicators } from '../services/fetch_aggregated_indicators'; @@ -47,7 +47,7 @@ describe('useAggregatedIndicators()', () => { it('should create and call the aggregatedIndicatorsQuery correctly', async () => { aggregatedIndicatorsQuery.mockResolvedValue([]); - const { result, rerender, waitFor } = renderUseAggregatedIndicators(); + const { result, rerender } = renderUseAggregatedIndicators(); // indicators service and the query should be called just once expect( @@ -81,7 +81,7 @@ describe('useAggregatedIndicators()', () => { expect.any(AbortSignal) ); - await waitFor(() => !result.current.isLoading); + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_column_settings.test.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_column_settings.test.ts index 74b76030bca37..cbba1c3043f3e 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_column_settings.test.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_column_settings.test.ts @@ -6,7 +6,7 @@ */ import { mockedServices, TestProvidersComponent } from '../../../mocks/test_providers'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { useColumnSettings } from './use_column_settings'; const renderUseColumnSettings = () => diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx index be710e771b040..2276d1cfcf635 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { act, renderHook } from '@testing-library/react-hooks'; -import { useIndicators, UseIndicatorsParams, UseIndicatorsValue } from './use_indicators'; +import { act, waitFor, renderHook } from '@testing-library/react'; +import { useIndicators, UseIndicatorsParams } from './use_indicators'; import { TestProvidersComponent } from '../../../mocks/test_providers'; import { createFetchIndicators } from '../services/fetch_indicators'; import { mockTimeRange } from '../../../mocks/mock_indicators_filters_context'; @@ -23,7 +23,7 @@ const useIndicatorsParams: UseIndicatorsParams = { const indicatorsQueryResult = { indicators: [], total: 0 }; const renderUseIndicators = (initialProps = useIndicatorsParams) => - renderHook((props) => useIndicators(props), { + renderHook(useIndicators, { initialProps, wrapper: TestProvidersComponent, }); @@ -53,7 +53,7 @@ describe('useIndicators()', () => { expect(indicatorsQuery).toHaveBeenCalledTimes(1); // isLoading should turn to false eventually - await hookResult.waitFor(() => !hookResult.result.current.isLoading); + await waitFor(() => expect(hookResult.result.current.isLoading).toBe(false)); expect(hookResult.result.current.isLoading).toEqual(false); }); }); @@ -77,7 +77,7 @@ describe('useIndicators()', () => { // Change page size await act(async () => hookResult.result.current.onChangeItemsPerPage(50)); - expect(indicatorsQuery).toHaveBeenCalledTimes(3); + await waitFor(() => expect(indicatorsQuery).toHaveBeenCalledTimes(3)); expect(indicatorsQuery).toHaveBeenLastCalledWith( expect.objectContaining({ pagination: expect.objectContaining({ pageIndex: 0, pageSize: 50 }), @@ -101,7 +101,7 @@ describe('useIndicators()', () => { expect.any(AbortSignal) ); - await hookResult.waitFor(() => !hookResult.result.current.isLoading); + await waitFor(() => expect(hookResult.result.current.isLoading).toBe(false)); expect(hookResult.result.current).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_toolbar_options.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_toolbar_options.test.tsx index 7af7320af0988..9a69b71fd3f75 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_toolbar_options.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_toolbar_options.test.tsx @@ -6,7 +6,7 @@ */ import { TestProvidersComponent } from '../../../mocks/test_providers'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useToolbarOptions } from './use_toolbar_options'; describe('useToolbarOptions()', () => { diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_total_count.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_total_count.test.tsx index 893367b42dbd3..8735817747f12 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_total_count.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_total_count.test.tsx @@ -6,7 +6,7 @@ */ import { mockedSearchService, TestProvidersComponent } from '../../../mocks/test_providers'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react'; import { BehaviorSubject } from 'rxjs'; import { useIndicatorsTotalCount } from './use_total_count'; diff --git a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filter_in_out.test.ts b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filter_in_out.test.ts index 7e435d944145b..18c8fccd573f1 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filter_in_out.test.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/query_bar/hooks/use_filter_in_out.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Renderer, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react'; import { generateMockIndicator, generateMockUrlIndicator, @@ -19,7 +19,7 @@ import { updateFiltersArray } from '../utils/filter'; jest.mock('../utils/filter', () => ({ updateFiltersArray: jest.fn() })); describe('useFilterInOut()', () => { - let hookResult: RenderHookResult<{}, UseFilterInValue, Renderer>; + let hookResult: RenderHookResult; it('should return empty object if Indicator is incorrect', () => { const indicator: Indicator = generateMockIndicator(); diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_add_to_timeline.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_add_to_timeline.test.tsx index 18d96282d98b6..0b5cc302ef0ea 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_add_to_timeline.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_add_to_timeline.test.tsx @@ -6,7 +6,7 @@ */ import { EMPTY_VALUE } from '../../../constants/common'; -import { Renderer, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react'; import { generateMockIndicator, generateMockUrlIndicator, @@ -16,7 +16,7 @@ import { TestProvidersComponent } from '../../../mocks/test_providers'; import { useAddToTimeline, UseAddToTimelineValue } from './use_add_to_timeline'; describe('useInvestigateInTimeline()', () => { - let hookResult: RenderHookResult<{}, UseAddToTimelineValue, Renderer>; + let hookResult: RenderHookResult; xit('should return empty object if Indicator is incorrect', () => { const indicator: Indicator = generateMockIndicator(); diff --git a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.test.tsx index 5d416c5ff56c7..dc9b3a4a00296 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/timeline/hooks/use_investigate_in_timeline.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { Renderer, renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react'; import { useInvestigateInTimeline, UseInvestigateInTimelineValue, @@ -18,7 +18,7 @@ import { import { TestProvidersComponent } from '../../../mocks/test_providers'; describe('useInvestigateInTimeline()', () => { - let hookResult: RenderHookResult<{}, UseInvestigateInTimelineValue, Renderer>; + let hookResult: RenderHookResult; it('should return empty object if Indicator is incorrect', () => { const indicator: Indicator = generateMockIndicator(); From 0d4df88bf2308b5fb00906a53644adc62d5be190 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 Dec 2024 05:55:33 +1100 Subject: [PATCH 10/10] [8.x] [Cloud Security][Bug] Fix for Page Crashing (#203028) (#203140) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security][Bug] Fix for Page Crashing (#203028)](https://github.com/elastic/kibana/pull/203028) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rickyanto Ang