diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 52147f549edc5..50c9a2d4a913e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -388,9 +388,6 @@ import type { InstallMigrationRulesResponse, InstallTranslatedMigrationRulesRequestParamsInput, InstallTranslatedMigrationRulesResponse, - RetryRuleMigrationRequestParamsInput, - RetryRuleMigrationRequestBodyInput, - RetryRuleMigrationResponse, StartRuleMigrationRequestParamsInput, StartRuleMigrationRequestBodyInput, StartRuleMigrationResponse, @@ -2033,22 +2030,6 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } - /** - * Retries a SIEM rules migration using the migration id provided - */ - async retryRuleMigration(props: RetryRuleMigrationProps) { - this.log.info(`${new Date().toISOString()} Calling API RetryRuleMigration`); - return this.kbnClient - .request({ - path: replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params), - headers: { - [ELASTIC_HTTP_VERSION_HEADER]: '1', - }, - method: 'PUT', - body: props.body, - }) - .catch(catchAxiosErrorFormatAndThrow); - } async riskEngineGetPrivileges() { this.log.info(`${new Date().toISOString()} Calling API RiskEngineGetPrivileges`); return this.kbnClient @@ -2600,10 +2581,6 @@ export interface ReadRuleProps { export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } -export interface RetryRuleMigrationProps { - params: RetryRuleMigrationRequestParamsInput; - body: RetryRuleMigrationRequestBodyInput; -} export interface RulePreviewProps { query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts index f4298a97a2c27..409df3e97a9b6 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/constants.ts @@ -17,7 +17,6 @@ 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; export const SIEM_RULE_MIGRATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/stats` as const; export const SIEM_RULE_MIGRATION_TRANSLATION_STATS_PATH = `${SIEM_RULE_MIGRATION_PATH}/translation_stats` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts index 1f4be5a9e2e2a..5f659149e594a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.gen.ts @@ -231,28 +231,6 @@ export const InstallTranslatedMigrationRulesResponse = z.object({ installed: z.boolean(), }); -export type RetryRuleMigrationRequestParams = z.infer; -export const RetryRuleMigrationRequestParams = z.object({ - migration_id: NonEmptyString, -}); -export type RetryRuleMigrationRequestParamsInput = z.input; - -export type RetryRuleMigrationRequestBody = z.infer; -export const RetryRuleMigrationRequestBody = z.object({ - connector_id: ConnectorId, - langsmith_options: LangSmithOptions.optional(), - filter: RuleMigrationRetryFilter.optional(), -}); -export type RetryRuleMigrationRequestBodyInput = z.input; - -export type RetryRuleMigrationResponse = z.infer; -export const RetryRuleMigrationResponse = z.object({ - /** - * Indicates the migration retry has been started. `false` means the migration does not need to be retried. - */ - started: z.boolean(), -}); - export type StartRuleMigrationRequestParams = z.infer; export const StartRuleMigrationRequestParams = z.object({ migration_id: NonEmptyString, @@ -263,6 +241,10 @@ export type StartRuleMigrationRequestBody = z.infer; diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml index f4360cf291580..3d656c686615e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/api/rules/rule_migration.schema.yaml @@ -320,6 +320,9 @@ paths: $ref: '../../common.schema.yaml#/components/schemas/ConnectorId' langsmith_options: $ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions' + retry: + $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRetryFilter' + description: The optional indicator to retry the rule translation based on this filter criteria responses: 200: description: Indicates the migration start request has been processed successfully. @@ -336,53 +339,6 @@ paths: 204: description: Indicates the migration id was not found. - /internal/siem_migrations/rules/{migration_id}/retry: - put: - summary: Retries a rule migration - operationId: RetryRuleMigration - x-codegen-enabled: true - x-internal: true - description: Retries a SIEM rules migration using the migration id provided - tags: - - SIEM Rule Migrations - parameters: - - name: migration_id - in: path - required: true - schema: - description: The migration id to retry - $ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString' - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - connector_id - properties: - connector_id: - $ref: '../../common.schema.yaml#/components/schemas/ConnectorId' - langsmith_options: - $ref: '../../common.schema.yaml#/components/schemas/LangSmithOptions' - filter: - $ref: '../../rule_migration.schema.yaml#/components/schemas/RuleMigrationRetryFilter' - responses: - 200: - description: Indicates the migration retry request has been processed successfully. - content: - application/json: - schema: - type: object - required: - - started - properties: - started: - type: boolean - description: Indicates the migration retry has been started. `false` means the migration does not need to be retried. - 204: - description: Indicates the migration id was not found. - /internal/siem_migrations/rules/{migration_id}/stats: get: summary: Gets a rule migration task stats diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.gen.ts index f55b87f7af703..c6d0959cc10cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.gen.ts @@ -15,13 +15,6 @@ */ import { z } from '@kbn/zod'; -import { isNonEmptyString } from '@kbn/zod-helpers'; - -/** - * A string that does not contain only whitespace characters - */ -export type NonEmptyString = z.infer; -export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString); /** * The GenAI connector id to use. diff --git a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.schema.yaml index 9e15bd857c728..08a2562edffdb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/siem_migrations/model/common.schema.yaml @@ -6,14 +6,10 @@ paths: {} components: x-codegen-enabled: true schemas: - NonEmptyString: - type: string - format: nonempty - minLength: 1 - description: A string that does not contain only whitespace characters ConnectorId: type: string description: The GenAI connector id to use. + LangSmithOptions: type: object description: The LangSmith options object. diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx index 324dd405d5141..b3e8423f4d22e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/missing_ai_connector_callout.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; -import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiPanel } from '@elastic/eui'; import { CardCallOut } from '../../common/card_callout'; import * as i18n from './translations'; @@ -17,7 +16,7 @@ interface MissingAIConnectorCalloutProps { export const MissingAIConnectorCallout = React.memo( ({ onExpandAiConnectorsCard }) => ( - + } /> - + ) ); MissingAIConnectorCallout.displayName = 'MissingAIConnectorCallout'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx index 1dae4d523c953..50d5ff5810e70 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/rule_migrations_panels.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; import { UploadRulesPanel } from './upload_rules_panel'; @@ -22,24 +22,52 @@ export interface RuleMigrationsPanelsProps { } export const RuleMigrationsPanels = React.memo( ({ migrationsStats, isConnectorsCardComplete, expandConnectorsCard }) => { - if (migrationsStats.length === 0) { - return isConnectorsCardComplete ? ( - - ) : ( - - ); - } + const latestMigrationsStats = useMemo( + () => migrationsStats.slice().reverse(), + [migrationsStats] + ); + + const [expandedCardId, setExpandedCardId] = useState(() => { + if (latestMigrationsStats[0]?.status === SiemMigrationTaskStatus.FINISHED) { + return latestMigrationsStats[0]?.id; + } + return undefined; + }); + + useEffect(() => { + if (!expandedCardId && latestMigrationsStats.length > 0) { + const runningMigration = latestMigrationsStats.find( + ({ status }) => status === SiemMigrationTaskStatus.RUNNING + ); + if (runningMigration) { + setExpandedCardId(runningMigration.id); // Set the next migration to be expanded when it finishes + } + } + }, [latestMigrationsStats, expandedCardId]); + + const getOnToggleCollapsed = useCallback( + (id: string) => (isCollapsed: boolean) => { + setExpandedCardId(isCollapsed ? undefined : id); + }, + [] + ); return ( - {isConnectorsCardComplete ? ( - - ) : ( - + {!isConnectorsCardComplete && ( + <> + + + )} + 0} + isDisabled={!isConnectorsCardComplete} + /> - {migrationsStats.map((migrationStats) => ( + + {latestMigrationsStats.map((migrationStats) => ( {migrationStats.status === SiemMigrationTaskStatus.READY && ( @@ -48,7 +76,11 @@ export const RuleMigrationsPanels = React.memo( )} {migrationStats.status === SiemMigrationTaskStatus.FINISHED && ( - + )} ))} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts index 4073423f1f8ae..b5c15b00e20e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/translations.ts @@ -21,12 +21,13 @@ export const START_MIGRATION_CARD_FOOTER_NOTE = i18n.translate( 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.', + defaultMessage: + 'You need an LLM connector to power SIEM rule migration. Set one up or choose an existing one to get started.', } ); export const START_MIGRATION_CARD_CONNECTOR_MISSING_BUTTON = i18n.translate( 'xpack.securitySolution.onboarding.startMigration.connectorMissingText', - { defaultMessage: 'AI provider step' } + { defaultMessage: 'Set up AI Connector' } ); export const START_MIGRATION_CARD_UPLOAD_TITLE = i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx index 1a9bd2d17b945..2911aae8183a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/upload_rules_panel.tsx @@ -24,56 +24,74 @@ import { useStyles } from './upload_rules_panel.styles'; export interface UploadRulesPanelProps { isUploadMore?: boolean; + isDisabled?: boolean; } -export const UploadRulesPanel = React.memo(({ isUploadMore = false }) => { - const styles = useStyles(isUploadMore); - const { openFlyout } = useRuleMigrationDataInputContext(); - const onOpenFlyout = useCallback(() => { - openFlyout(); - }, [openFlyout]); +export const UploadRulesPanel = React.memo( + ({ isUploadMore = false, isDisabled = false }) => { + const styles = useStyles(isUploadMore); + const { openFlyout } = useRuleMigrationDataInputContext(); + 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}

-
-
- - - -
- )} -
- - {isUploadMore ? ( - - {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} - - ) : ( - - {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} - - )} - -
-
- ); -}); + return ( + + + + + + + {isUploadMore ? ( + +

{i18n.START_MIGRATION_CARD_UPLOAD_MORE_TITLE}

+
+ ) : ( + + + +

{i18n.START_MIGRATION_CARD_UPLOAD_TITLE}

+
+
+ + +

{i18n.START_MIGRATION_CARD_UPLOAD_DESCRIPTION}

+
+
+ + + +
+ )} +
+ + {isUploadMore ? ( + + {i18n.START_MIGRATION_CARD_UPLOAD_MORE_BUTTON} + + ) : ( + + {i18n.START_MIGRATION_CARD_UPLOAD_BUTTON} + + )} + +
+
+ ); + } +); UploadRulesPanel.displayName = 'UploadRulesPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg index e8568a943f70c..d2656fa7e9a3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/icon/siem_migrations.svg @@ -1,28 +1,47 @@ - - - - + + + + - - + + - - - - - - - - + + + + + + + - + + - - + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts index 99caaaacf706c..148417ce3273e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/api/index.ts @@ -24,7 +24,6 @@ import { SIEM_RULE_MIGRATION_RESOURCES_MISSING_PATH, SIEM_RULE_MIGRATION_RESOURCES_PATH, SIEM_RULE_MIGRATIONS_PREBUILT_RULES_PATH, - SIEM_RULE_MIGRATION_RETRY_PATH, SIEM_RULE_MIGRATIONS_INTEGRATIONS_PATH, } from '../../../../common/siem_migrations/constants'; import type { @@ -42,9 +41,7 @@ import type { UpsertRuleMigrationResourcesResponse, GetRuleMigrationPrebuiltRulesResponse, UpdateRuleMigrationResponse, - RetryRuleMigrationRequestBody, StartRuleMigrationResponse, - RetryRuleMigrationResponse, GetRuleMigrationIntegrationsResponse, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; @@ -142,6 +139,8 @@ export interface StartRuleMigrationParams { migrationId: string; /** The connector id to use for the migration */ connectorId: string; + /** Optional indicator to retry the migration with specific filtering criteria */ + retry?: SiemMigrationRetryFilter; /** Optional LangSmithOptions to use for the for the migration */ langSmithOptions?: LangSmithOptions; /** Optional AbortSignal for cancelling request */ @@ -151,48 +150,17 @@ export interface StartRuleMigrationParams { export const startRuleMigration = async ({ migrationId, connectorId, + retry, langSmithOptions, 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 } - ); -}; - -export interface RetryRuleMigrationParams { - /** `id` of the migration to reprocess rules for */ - migrationId: string; - /** The connector id to use for the reprocessing */ - connectorId: string; - /** Optional LangSmithOptions to use for the for the reprocessing */ - langSmithOptions?: LangSmithOptions; - /** Optional indicator to filter migration rules to retry */ - filter?: SiemMigrationRetryFilter; - /** Optional AbortSignal for cancelling request */ - signal?: AbortSignal; -} -/** Starts a reprocessing of migration rules in a specific migration. */ -export const retryRuleMigration = async ({ - migrationId, - connectorId, - langSmithOptions, - filter, - signal, -}: RetryRuleMigrationParams): Promise => { - const body: RetryRuleMigrationRequestBody = { + const body: StartRuleMigrationRequestBody = { connector_id: connectorId, - filter, + retry, + langsmith_options: langSmithOptions, }; - if (langSmithOptions) { - body.langsmith_options = langSmithOptions; - } - return KibanaServices.get().http.put( - replaceParams(SIEM_RULE_MIGRATION_RETRY_PATH, { migration_id: migrationId }), + return KibanaServices.get().http.put( + replaceParams(SIEM_RULE_MIGRATION_START_PATH, { migration_id: migrationId }), { body: JSON.stringify(body), version: '1', signal } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx index 4f8e73f43f6c3..0ac807235dc71 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.tsx @@ -18,9 +18,13 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { - RuleMigrationResourceBase, - RuleMigrationTaskStats, +import { + SiemMigrationRetryFilter, + SiemMigrationTaskStatus, +} from '../../../../../common/siem_migrations/constants'; +import { + type RuleMigrationResourceBase, + type RuleMigrationTaskStats, } from '../../../../../common/siem_migrations/model/rule_migration.gen'; import { RulesDataInput } from './steps/rules/rules_data_input'; import { useStartMigration } from '../../service/hooks/use_start_migration'; @@ -45,13 +49,15 @@ export const MigrationDataInputFlyout = React.memo(); + const isRetry = migrationStats?.status === SiemMigrationTaskStatus.FINISHED; const { startMigration, isLoading: isStartLoading } = useStartMigration(onClose); const onStartMigration = useCallback(() => { if (migrationStats?.id) { - startMigration(migrationStats.id); + const retryFilter = isRetry ? SiemMigrationRetryFilter.NOT_FULLY_TRANSLATED : undefined; + startMigration(migrationStats.id, retryFilter); } - }, [migrationStats, startMigration]); + }, [startMigration, migrationStats?.id, isRetry]); const [dataInputStep, setDataInputStep] = useState(DataInputStep.Rules); @@ -153,10 +159,17 @@ export const MigrationDataInputFlyout = React.memo - + {isRetry ? ( + + ) : ( + + )}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx new file mode 100644 index 0000000000000..1be66ac0a1e76 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/upload_file_button.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, type EuiButtonProps, type PropsForButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const DATA_INPUT_FILE_UPLOAD_BUTTON = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.uploadButtonText', + { defaultMessage: 'Upload' } +); + +export const UploadFileButton = React.memo>((props) => { + return ( + + {DATA_INPUT_FILE_UPLOAD_BUTTON} + + ); +}); +UploadFileButton.displayName = 'UploadFileButton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts index 54622191b6d68..43051544551d8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/common/use_parse_file_input.ts @@ -20,12 +20,12 @@ export const useParseFileInput = (onFileParsed: OnFileParsed) => { const parseFile = useCallback( (files: FileList | null) => { - if (!files) { + setError(undefined); + + if (!files || files.length === 0) { return; } - setError(undefined); - const file = files[0]; const reader = new FileReader(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx index 0dc05493f7469..6f4641ed4b26a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/lookups/sub_steps/lookups_file_upload/lookups_file_upload.tsx @@ -6,14 +6,7 @@ */ import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { - EuiButton, - EuiFilePicker, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiText, -} from '@elastic/eui'; +import { EuiFilePicker, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui'; import type { EuiFilePickerClass, EuiFilePickerProps, @@ -21,6 +14,7 @@ import type { import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import { FILE_UPLOAD_ERROR } from '../../../../translations'; import * as i18n from './translations'; +import { UploadFileButton } from '../../../common/upload_file_button'; export interface LookupsFileUploadProps { createResources: (resources: RuleMigrationResourceData[]) => void; @@ -112,6 +106,9 @@ export const LookupsFileUpload = React.memo( return fileErrors; }, [apiError, fileErrors]); + const showLoader = isParsing || isLoading; + const isButtonDisabled = showLoader || lookupResources.length === 0; + return ( @@ -129,19 +126,17 @@ export const LookupsFileUpload = React.memo( ref={filePickerRef as React.Ref>} fullWidth initialPromptText={ - <> - - {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT} - - + + {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_PROMPT} + } accept="application/text" onChange={parseFile} multiple display="large" aria-label="Upload lookups files" - isLoading={isParsing || isLoading} - disabled={isParsing || isLoading} + isLoading={showLoader} + disabled={showLoader} data-test-subj="lookupsFilePicker" data-loading={isParsing} /> @@ -150,9 +145,11 @@ export const LookupsFileUpload = React.memo( - - {i18n.LOOKUPS_DATA_INPUT_FILE_UPLOAD_BUTTON} - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx index 5cea4afdb8537..1688e406afcdb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/macros/sub_steps/macros_file_upload/macros_file_upload.tsx @@ -5,13 +5,18 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; import { isPlainObject } from 'lodash'; +import { EuiFilePicker, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui'; +import type { + EuiFilePickerClass, + EuiFilePickerProps, +} from '@elastic/eui/src/components/form/file_picker/file_picker'; import type { RuleMigrationResourceData } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import { FILE_UPLOAD_ERROR } from '../../../../translations'; import type { SPLUNK_MACROS_COLUMNS } from '../../../../constants'; import { useParseFileInput, type SplunkRow } from '../../../common/use_parse_file_input'; +import { UploadFileButton } from '../../../common/upload_file_button'; import * as i18n from './translations'; type SplunkMacroResult = Partial>; @@ -23,16 +28,29 @@ export interface MacrosFileUploadProps { } export const MacrosFileUpload = React.memo( ({ createResources, apiError, isLoading }) => { - const onFileParsed = useCallback( - (content: Array>) => { - const macros = content.map(formatMacroRow); - createResources(macros); - }, - [createResources] - ); + const [macrosToUpload, setMacrosToUpload] = useState([]); + const filePickerRef = useRef(null); + + const createMacros = useCallback(() => { + filePickerRef.current?.removeFiles(); + createResources(macrosToUpload); + }, [createResources, macrosToUpload]); + + const onFileParsed = useCallback((content: Array>) => { + const macros = content.map(formatMacroRow); + setMacrosToUpload(macros); + }, []); const { parseFile, isParsing, error: fileError } = useParseFileInput(onFileParsed); + const onFileChange = useCallback( + (files: FileList | null) => { + setMacrosToUpload([]); + parseFile(files); + }, + [parseFile] + ); + const error = useMemo(() => { if (apiError) { return apiError; @@ -40,36 +58,53 @@ export const MacrosFileUpload = React.memo( return fileError; }, [apiError, fileError]); + const showLoader = isParsing || isLoading; + const isButtonDisabled = showLoader || macrosToUpload.length === 0; + return ( - - {error} - - } - isInvalid={error != null} - fullWidth - > - - - {i18n.MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + + {error} - - } - accept="application/json, application/x-ndjson" - onChange={parseFile} - display="large" - aria-label="Upload macros file" - isLoading={isParsing || isLoading} - disabled={isParsing || isLoading} - data-test-subj="macrosFilePicker" - data-loading={isParsing} - /> - + } + isInvalid={error != null} + fullWidth + > + >} + fullWidth + initialPromptText={ + + {i18n.MACROS_DATA_INPUT_FILE_UPLOAD_PROMPT} + + } + accept="application/json, application/x-ndjson" + onChange={onFileChange} + display="large" + aria-label="Upload macros file" + isLoading={showLoader} + disabled={showLoader} + data-test-subj="macrosFilePicker" + data-loading={isParsing} + /> + + + + + + + + + + ); } ); diff --git a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx index bec9182420073..84cd5cb5e06f9 100644 --- a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/rules/sub_steps/rules_file_upload/rules_file_upload.tsx @@ -5,15 +5,21 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { isPlainObject } from 'lodash'; +import { EuiFilePicker, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui'; +import type { + EuiFilePickerClass, + EuiFilePickerProps, +} from '@elastic/eui/src/components/form/file_picker/file_picker'; +import type { CreateRuleMigrationRequestBody } from '../../../../../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { OriginalRule } from '../../../../../../../../../common/siem_migrations/model/rule_migration.gen'; import type { CreateMigration } from '../../../../../../service/hooks/use_create_migration'; -import * as i18n from './translations'; import { FILE_UPLOAD_ERROR } from '../../../../translations'; import { useParseFileInput, type SplunkRow } from '../../../common/use_parse_file_input'; import type { SPLUNK_RULES_COLUMNS } from '../../../../constants'; +import { UploadFileButton } from '../../../common/upload_file_button'; +import * as i18n from './translations'; type SplunkRulesResult = Partial>; @@ -25,16 +31,29 @@ export interface RulesFileUploadProps { } export const RulesFileUpload = React.memo( ({ createMigration, apiError, isLoading, isCreated }) => { - const onFileParsed = useCallback( - (content: Array>) => { - const rules = content.map(formatRuleRow); - createMigration(rules); - }, - [createMigration] - ); + const [rulesToUpload, setRulesToUpload] = useState([]); + const filePickerRef = useRef(null); + + const createRules = useCallback(() => { + filePickerRef.current?.removeFiles(); + createMigration(rulesToUpload); + }, [createMigration, rulesToUpload]); + + const onFileParsed = useCallback((content: Array>) => { + const rules = content.map(formatRuleRow); + setRulesToUpload(rules); + }, []); const { parseFile, isParsing, error: fileError } = useParseFileInput(onFileParsed); + const onFileChange = useCallback( + (files: FileList | null) => { + setRulesToUpload([]); + parseFile(files); + }, + [parseFile] + ); + const error = useMemo(() => { if (apiError) { return apiError; @@ -42,36 +61,54 @@ export const RulesFileUpload = React.memo( return fileError; }, [apiError, fileError]); + const showLoader = isParsing || isLoading; + const isDisabled = showLoader || isCreated; + const isButtonDisabled = isDisabled || rulesToUpload.length === 0; + return ( - - {error} - - } - isInvalid={error != null} - fullWidth - > - - - {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + + + {error} - - } - accept="application/json, application/x-ndjson" - onChange={parseFile} - display="large" - aria-label="Upload rules file" - isLoading={isParsing || isLoading} - disabled={isLoading || isCreated} - data-test-subj="rulesFilePicker" - data-loading={isParsing} - /> - + } + isInvalid={error != null} + fullWidth + > + >} + fullWidth + initialPromptText={ + + {i18n.RULES_DATA_INPUT_FILE_UPLOAD_PROMPT} + + } + accept="application/json, application/x-ndjson" + onChange={onFileChange} + display="large" + aria-label="Upload rules file" + isLoading={showLoader} + disabled={isDisabled} + data-test-subj="rulesFilePicker" + data-loading={isParsing} + /> + + + + + + + + + + ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx index 0be6fa7b75f5a..9fca25e88f916 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_progress_panel.tsx @@ -34,7 +34,7 @@ export const MigrationProgressPanel = React.memo( return ( - +

{i18n.RULE_MIGRATION_TITLE(migrationStats.number)}

@@ -45,40 +45,33 @@ export const MigrationProgressPanel = React.memo( {i18n.RULE_MIGRATION_PROGRESS_DESCRIPTION(migrationStats.rules.total)}
- +
+ + + + + + + + {preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING} + + - - - - - - - {preparing ? i18n.RULE_MIGRATION_PREPARING : i18n.RULE_MIGRATION_TRANSLATING} - - - - - - - {!preparing && ( - <> - - - - - )} + + {!preparing && ( + <> + + + + + )}
); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx index c8f12d4374834..41d95ffa5455c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/migration_result_panel.tsx @@ -16,11 +16,15 @@ import { EuiBasicTable, EuiHealth, EuiText, + EuiAccordion, + EuiButtonIcon, + type EuiBasicTableColumn, } from '@elastic/eui'; import { Chart, BarSeries, Settings, ScaleType } from '@elastic/charts'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { useElasticChartsTheme } from '@kbn/charts-theme'; +import { css } from '@emotion/react'; import { PanelText } from '../../../../common/components/panel_text'; import { convertTranslationResultIntoText, @@ -33,88 +37,120 @@ import { SecuritySolutionLinkButton } from '../../../../common/components/links' import type { RuleMigrationStats } from '../../types'; import { RuleTranslationResult } from '../../../../../common/siem_migrations/constants'; import * as i18n from './translations'; +import { RuleMigrationsUploadMissingPanel } from './upload_missing_panel'; + +const headerStyle = css` + &:hover { + cursor: pointer; + text-decoration: underline; + } +`; export interface MigrationResultPanelProps { migrationStats: RuleMigrationStats; + isCollapsed: boolean; + onToggleCollapsed: (isCollapsed: boolean) => void; } -export const MigrationResultPanel = React.memo(({ migrationStats }) => { - const { data: translationStats, isLoading: isLoadingTranslationStats } = - useGetMigrationTranslationStats(migrationStats.id); - return ( - - - - - -

{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}

-
-
- - -

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

-
-
-
-
- - - - - + +export const MigrationResultPanel = React.memo( + ({ migrationStats, isCollapsed = false, onToggleCollapsed }) => { + const { data: translationStats, isLoading: isLoadingTranslationStats } = + useGetMigrationTranslationStats(migrationStats.id); + + return ( + + + + onToggleCollapsed(!isCollapsed)} css={headerStyle}> + + + +

{i18n.RULE_MIGRATION_COMPLETE_TITLE(migrationStats.number)}

+
+
+ + +

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

+
+
+
+
+ + onToggleCollapsed(!isCollapsed)} + aria-label={isCollapsed ? i18n.RULE_MIGRATION_EXPAND : i18n.RULE_MIGRATION_COLLAPSE} + /> + +
+
+ + + + - + + + + + + +

{i18n.RULE_MIGRATION_SUMMARY_TITLE}

+
+
+
- -

{i18n.RULE_MIGRATION_SUMMARY_TITLE}

-
-
-
-
- - - - - {isLoadingTranslationStats ? ( - - ) : ( - translationStats && ( - <> - - {i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE} - - - - - ) - )} - - - + + - - {i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON} - + {isLoadingTranslationStats ? ( + + ) : ( + translationStats && ( + <> + + {i18n.RULE_MIGRATION_SUMMARY_CHART_TITLE} + + + + + ) + )} + + + + + + {i18n.RULE_MIGRATION_VIEW_TRANSLATED_RULES_BUTTON} + + + - - - - -
- {/* TODO: uncomment when retry API is ready */} +
+ + + +
+ - - ); -}); + ); + } +); MigrationResultPanel.displayName = 'MigrationResultPanel'; const TranslationResultsChart = React.memo<{ @@ -172,12 +208,36 @@ const TranslationResultsChart = React.memo<{ }); TranslationResultsChart.displayName = 'TranslationResultsChart'; +interface TranslationResultsTableItem { + title: string; + value: number; + color: string; +} + +const columns: Array> = [ + { + field: 'title', + name: i18n.RULE_MIGRATION_TABLE_COLUMN_RESULT, + render: (title: string, { color }) => ( + + {title} + + ), + }, + { + field: 'value', + name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES, + align: 'right', + render: (value: string) => {value}, + }, +]; + const TranslationResultsTable = React.memo<{ translationStats: RuleMigrationTranslationStats; }>(({ translationStats }) => { const translationResultColors = useResultVisColors(); - const items = useMemo(() => { - return [ + const items = useMemo( + () => [ { title: convertTranslationResultIntoText(RuleTranslationResult.FULL), value: translationStats.rules.success.result.full, @@ -198,26 +258,10 @@ const TranslationResultsTable = React.memo<{ value: translationStats.rules.failed, color: translationResultColors.error, }, - ]; - }, [translationStats, translationResultColors]); - - return ( - {value}, - }, - { - field: 'value', - name: i18n.RULE_MIGRATION_TABLE_COLUMN_RULES, - align: 'right', - }, - ]} - /> + ], + [translationStats, translationResultColors] ); + + return ; }); TranslationResultsTable.displayName = 'TranslationResultsTable'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts index 55e73bca32b5d..b97717d889e66 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/translations.ts @@ -36,12 +36,16 @@ export const RULE_MIGRATION_PROGRESS_DESCRIPTION = (totalRules: number) => defaultMessage: `Processing migration of {totalRules} rules.`, values: { totalRules }, }); +export const RULE_MIGRATION_IN_PROGRESS_BADGE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.progress.badge', + { defaultMessage: `Translation in progress` } +); export const RULE_MIGRATION_PREPARING = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.preparing', + 'xpack.securitySolution.siemMigrations.rules.panel.progress.preparing', { defaultMessage: `Preparing environment for the AI powered translation.` } ); export const RULE_MIGRATION_TRANSLATING = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.panel.translating', + 'xpack.securitySolution.siemMigrations.rules.panel.progress.translating', { defaultMessage: `Translating rules` } ); @@ -57,6 +61,10 @@ export const RULE_MIGRATION_COMPLETE_DESCRIPTION = (createdAt: string, finishedA values: { createdAt, finishedAt }, }); +export const RULE_MIGRATION_COMPLETE_BADGE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.result.badge', + { defaultMessage: `Translation complete` } +); export const RULE_MIGRATION_SUMMARY_TITLE = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.result.summary.title', { defaultMessage: 'Translation Summary' } @@ -99,3 +107,12 @@ export const RULE_MIGRATION_UPLOAD_BUTTON = i18n.translate( 'xpack.securitySolution.siemMigrations.rules.panel.uploadMacros.button', { defaultMessage: 'Upload' } ); + +export const RULE_MIGRATION_EXPAND = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.expand', + { defaultMessage: 'Expand rule migration' } +); +export const RULE_MIGRATION_COLLAPSE = i18n.translate( + 'xpack.securitySolution.siemMigrations.rules.panel.collapse', + { defaultMessage: 'Collapse rule migration' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx index 569c726c9a87e..d71a5ead02e35 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/migration_status_panels/upload_missing_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { EuiButton, EuiFlexGroup, @@ -25,13 +25,13 @@ import type { RuleMigrationStats } from '../../types'; interface RuleMigrationsUploadMissingPanelProps { migrationStats: RuleMigrationStats; - spacerSizeTop?: SpacerSize; + topSpacerSize?: SpacerSize; } export const RuleMigrationsUploadMissingPanel = React.memo( - ({ migrationStats, spacerSizeTop }) => { + ({ migrationStats, topSpacerSize }) => { const { euiTheme } = useEuiTheme(); const { openFlyout } = useRuleMigrationDataInputContext(); - const [missingResources, setMissingResources] = React.useState([]); + const [missingResources, setMissingResources] = useState([]); const { getMissingResources, isLoading } = useGetMissingResources(setMissingResources); useEffect(() => { @@ -47,7 +47,7 @@ export const RuleMigrationsUploadMissingPanel = React.memo - {spacerSizeTop && } + {topSpacerSize && } = React.mem const { mutateAsync: installMigrationRules } = useInstallMigrationRules(migrationId); const { mutateAsync: installTranslatedMigrationRules } = useInstallTranslatedMigrationRules(migrationId); - const { retryRuleMigration, isLoading: isRetryLoading } = useRetryRuleMigration(refetchData); + const { startMigration, isLoading: isRetryLoading } = useStartMigration(refetchData); const [isTableLoading, setTableLoading] = useState(false); const installSingleRule = useCallback( @@ -209,8 +209,8 @@ export const MigrationRulesTable: React.FC = React.mem ); const reprocessFailedRules = useCallback(async () => { - retryRuleMigration(migrationId, SiemMigrationRetryFilter.FAILED); - }, [migrationId, retryRuleMigration]); + startMigration(migrationId, SiemMigrationRetryFilter.FAILED); + }, [migrationId, startMigration]); const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading || isRetryLoading; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx index af4e2abf5ebd7..c9fba086c63f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/pages/index.tsx @@ -39,7 +39,8 @@ export const MigrationRulesPage: React.FC = React.memo( }, }) => { const { navigateTo } = useNavigation(); - const { data: ruleMigrationsStats, isLoading, refreshStats } = useLatestStats(); + const { data, isLoading, refreshStats } = useLatestStats(); + const ruleMigrationsStats = useMemo(() => data.slice().reverse(), [data]); // Show the most recent migration first const [integrations, setIntegrations] = React.useState< Record | undefined diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts deleted file mode 100644 index 1fbce27a0fe86..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_retry_rules.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { 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'; -import type { SiemMigrationRetryFilter } from '../../../../../common/siem_migrations/constants'; - -export const RETRY_RULE_MIGRATION_SUCCESS = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesSuccess', - { defaultMessage: 'Retry rule migration started successfully.' } -); -export const RETRY_RULE_MIGRATION_ERROR = i18n.translate( - 'xpack.securitySolution.siemMigrations.rules.service.retryMigrationRulesError', - { defaultMessage: 'Error retrying a rule migration.' } -); - -export type RetryRuleMigration = (migrationId: string, filter?: SiemMigrationRetryFilter) => void; -export type OnSuccess = () => void; - -export const useRetryRuleMigration = (onSuccess?: OnSuccess) => { - const { siemMigrations, notifications } = useKibana().services; - const [state, dispatch] = useReducer(reducer, initialState); - - const retryRuleMigration = useCallback( - (migrationId, filter) => { - (async () => { - try { - dispatch({ type: 'start' }); - await siemMigrations.rules.retryRuleMigration(migrationId, filter); - - notifications.toasts.addSuccess(RETRY_RULE_MIGRATION_SUCCESS); - dispatch({ type: 'success' }); - onSuccess?.(); - } catch (err) { - const apiError = err.body ?? err; - notifications.toasts.addError(apiError, { title: RETRY_RULE_MIGRATION_ERROR }); - dispatch({ type: 'error', error: apiError }); - } - })(); - }, - [siemMigrations.rules, notifications.toasts, onSuccess] - ); - - return { isLoading: state.loading, error: state.error, retryRuleMigration }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts index 6794439d1298e..9944e298a31d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/hooks/use_start_migration.ts @@ -7,6 +7,7 @@ import { useCallback, useReducer } from 'react'; import { i18n } from '@kbn/i18n'; +import type { SiemMigrationRetryFilter } from '../../../../../common/siem_migrations/constants'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import { reducer, initialState } from './common/api_request_reducer'; @@ -19,7 +20,7 @@ export const RULES_DATA_INPUT_START_MIGRATION_ERROR = i18n.translate( { defaultMessage: 'Error starting migration.' } ); -export type StartMigration = (migrationId: string) => void; +export type StartMigration = (migrationId: string, retry?: SiemMigrationRetryFilter) => void; export type OnSuccess = () => void; export const useStartMigration = (onSuccess?: OnSuccess) => { @@ -27,11 +28,11 @@ export const useStartMigration = (onSuccess?: OnSuccess) => { const [state, dispatch] = useReducer(reducer, initialState); const startMigration = useCallback( - (migrationId) => { + (migrationId, retry) => { (async () => { try { dispatch({ type: 'start' }); - await siemMigrations.rules.startRuleMigration(migrationId); + await siemMigrations.rules.startRuleMigration(migrationId, retry); notifications.toasts.addSuccess(RULES_DATA_INPUT_START_MIGRATION_SUCCESS); dispatch({ type: 'success' }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts index 26cafe3878bc0..6c4bc218f03ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/service/rule_migrations_service.ts @@ -21,7 +21,6 @@ import type { import type { CreateRuleMigrationRequestBody, GetRuleMigrationStatsResponse, - RetryRuleMigrationResponse, StartRuleMigrationResponse, UpsertRuleMigrationResourcesRequestBody, } from '../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; @@ -38,7 +37,6 @@ import { type GetRuleMigrationsStatsAllParams, getMissingResources, upsertMigrationResources, - retryRuleMigration, getIntegrations, } from '../api'; import type { RuleMigrationStats } from '../types'; @@ -124,30 +122,10 @@ export class SiemRulesMigrationsService { } } - 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 retryRuleMigration( + public async startRuleMigration( migrationId: string, - filter?: SiemMigrationRetryFilter - ): Promise { + retry?: SiemMigrationRetryFilter + ): Promise { const connectorId = this.connectorIdStorage.get(); if (!connectorId) { throw new Error(i18n.MISSING_CONNECTOR_ERROR); @@ -162,12 +140,7 @@ export class SiemRulesMigrationsService { }; } - const result = await retryRuleMigration({ - migrationId, - connectorId, - langSmithOptions, - filter, - }); + const result = await startRuleMigration({ migrationId, connectorId, retry, langSmithOptions }); this.startPolling(); return result; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts index d25c252fb8fec..8571cdfe0b1a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/utils/translation_results/index.ts @@ -12,11 +12,20 @@ import * as i18n from './translations'; export const useResultVisColors = () => { const { euiTheme } = useEuiTheme(); + if (euiTheme.themeName === 'EUI_THEME_AMSTERDAM') { + return { + [RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVis0, + [RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorVis5, + [RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorVis7, + error: euiTheme.colors.vis.euiColorVis9, + }; + } + // Borealis return { - [RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVis0, - [RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorVis5, - [RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorVis7, - error: euiTheme.colors.vis.euiColorVis9, + [RuleTranslationResult.FULL]: euiTheme.colors.vis.euiColorVisSuccess0, + [RuleTranslationResult.PARTIAL]: euiTheme.colors.vis.euiColorSeverity7, + [RuleTranslationResult.UNTRANSLATABLE]: euiTheme.colors.vis.euiColorSeverity10, + error: euiTheme.colors.vis.euiColorSeverity14, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts index 05d2c8f1a5dc4..24d33ea27f23c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/index.ts @@ -17,7 +17,6 @@ import { registerSiemRuleMigrationsStopRoute } from './stop'; import { registerSiemRuleMigrationsStatsAllRoute } from './stats_all'; import { registerSiemRuleMigrationsResourceUpsertRoute } from './resources/upsert'; import { registerSiemRuleMigrationsResourceGetRoute } from './resources/get'; -import { registerSiemRuleMigrationsRetryRoute } from './retry'; import { registerSiemRuleMigrationsInstallRoute } from './install'; import { registerSiemRuleMigrationsInstallTranslatedRoute } from './install_translated'; import { registerSiemRuleMigrationsResourceGetMissingRoute } from './resources/missing'; @@ -34,7 +33,6 @@ export const registerSiemRuleMigrationsRoutes = ( registerSiemRuleMigrationsPrebuiltRulesRoute(router, logger); registerSiemRuleMigrationsGetRoute(router, logger); registerSiemRuleMigrationsStartRoute(router, logger); - registerSiemRuleMigrationsRetryRoute(router, logger); registerSiemRuleMigrationsStatsRoute(router, logger); registerSiemRuleMigrationsTranslationStatsRoute(router, logger); registerSiemRuleMigrationsStopRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts deleted file mode 100644 index df2fea4dbdbf8..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/retry.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IKibanaResponse, Logger } from '@kbn/core/server'; -import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import { APMTracer } from '@kbn/langchain/server/tracers/apm'; -import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; -import { - RetryRuleMigrationRequestBody, - RetryRuleMigrationRequestParams, - type RetryRuleMigrationResponse, -} from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; -import { SIEM_RULE_MIGRATION_RETRY_PATH } from '../../../../../common/siem_migrations/constants'; -import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { withLicense } from './util/with_license'; -import type { RuleMigrationFilters } from '../data/rule_migrations_data_rules_client'; - -export const registerSiemRuleMigrationsRetryRoute = ( - router: SecuritySolutionPluginRouter, - logger: Logger -) => { - router.versioned - .put({ - path: SIEM_RULE_MIGRATION_RETRY_PATH, - access: 'internal', - security: { authz: { requiredPrivileges: ['securitySolution'] } }, - }) - .addVersion( - { - version: '1', - validate: { - request: { - params: buildRouteValidationWithZod(RetryRuleMigrationRequestParams), - body: buildRouteValidationWithZod(RetryRuleMigrationRequestBody), - }, - }, - }, - withLicense( - async (context, req, res): Promise> => { - const migrationId = req.params.migration_id; - const { - langsmith_options: langsmithOptions, - connector_id: connectorId, - filter, - } = req.body; - - try { - const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']); - - const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient(); - const inferenceClient = ctx.securitySolution.getInferenceClient(); - const actionsClient = ctx.actions.getActionsClient(); - const soClient = ctx.core.savedObjects.client; - const rulesClient = await ctx.alerting.getRulesClient(); - - const invocationConfig = { - callbacks: [ - new APMTracer({ projectName: langsmithOptions?.project_name ?? 'default' }, logger), - ...getLangSmithTracer({ ...langsmithOptions, logger }), - ], - }; - - const filters: RuleMigrationFilters = { - ...(filter === 'failed' ? { failed: true } : {}), - ...(filter === 'not_fully_translated' ? { fullyTranslated: false } : {}), - }; - const { updated } = await ruleMigrationsClient.task.updateToRetry(migrationId, filters); - if (!updated) { - return res.ok({ body: { started: false } }); - } - - const { exists, started } = await ruleMigrationsClient.task.start({ - migrationId, - connectorId, - invocationConfig, - inferenceClient, - actionsClient, - soClient, - rulesClient, - }); - - if (!exists) { - return res.noContent(); - } - return res.ok({ body: { started } }); - } catch (err) { - logger.error(err); - return res.badRequest({ body: err.message }); - } - } - ) - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts index 4e50d3d583c65..130ffe1d80379 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/start.ts @@ -17,6 +17,7 @@ import { } from '../../../../../common/siem_migrations/model/api/rules/rule_migration.gen'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { withLicense } from './util/with_license'; +import { getRetryFilter } from './util/retry'; export const registerSiemRuleMigrationsStartRoute = ( router: SecuritySolutionPluginRouter, @@ -41,7 +42,11 @@ export const registerSiemRuleMigrationsStartRoute = ( withLicense( async (context, req, res): Promise> => { const migrationId = req.params.migration_id; - const { langsmith_options: langsmithOptions, connector_id: connectorId } = req.body; + const { + langsmith_options: langsmithOptions, + connector_id: connectorId, + retry, + } = req.body; try { const ctx = await context.resolve(['core', 'actions', 'alerting', 'securitySolution']); @@ -52,6 +57,16 @@ export const registerSiemRuleMigrationsStartRoute = ( const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); + if (retry) { + const { updated } = await ruleMigrationsClient.task.updateToRetry( + migrationId, + getRetryFilter(retry) + ); + if (!updated) { + return res.ok({ body: { started: false } }); + } + } + const invocationConfig = { callbacks: [ new APMTracer({ projectName: langsmithOptions?.project_name ?? 'default' }, logger), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/retry.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/retry.ts new file mode 100644 index 0000000000000..700218b4292ee --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/api/util/retry.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 type { RuleMigrationRetryFilter } from '../../../../../../common/siem_migrations/model/rule_migration.gen'; +import type { RuleMigrationFilters } from '../../data/rule_migrations_data_rules_client'; + +const RETRY_FILTERS: Record = { + failed: { failed: true }, + not_fully_translated: { fullyTranslated: false }, +}; + +export const getRetryFilter = (retryFilter: RuleMigrationRetryFilter): RuleMigrationFilters => { + return RETRY_FILTERS[retryFilter]; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts index 3c932e97977e7..b68ce9e2a6828 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/siem_migrations/rules/task/rule_migrations_task_client.ts @@ -212,16 +212,15 @@ export class RuleMigrationsTaskClient { /** Updates all the rules in a migration to be re-executed */ public async updateToRetry( migrationId: string, - filter: RuleMigrationFilters = {} + filter: RuleMigrationFilters ): Promise<{ updated: boolean }> { if (this.migrationsRunning.has(migrationId)) { return { updated: false }; } - // Update all the rules in the migration to pending + await this.data.rules.updateStatus(migrationId, filter, SiemMigrationStatus.PENDING, { refresh: true, }); - // await this.data.rules.updateRetry(migrationId); return { updated: true }; } 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 273bc342cbc01..4e9c66324cb26 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 @@ -138,10 +138,6 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/ import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen'; -import { - RetryRuleMigrationRequestParamsInput, - RetryRuleMigrationRequestBodyInput, -} from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; import { RulePreviewRequestQueryInput, RulePreviewRequestBodyInput, @@ -1417,22 +1413,6 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, - /** - * Retries a SIEM rules migration using the migration id provided - */ - retryRuleMigration(props: RetryRuleMigrationProps, kibanaSpace: string = 'default') { - return supertest - .put( - routeWithNamespace( - replaceParams('/internal/siem_migrations/rules/{migration_id}/retry', props.params), - kibanaSpace - ) - ) - .set('kbn-xsrf', 'true') - .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(props.body as object); - }, riskEngineGetPrivileges(kibanaSpace: string = 'default') { return supertest .get(routeWithNamespace('/internal/risk_engine/privileges', kibanaSpace)) @@ -1917,10 +1897,6 @@ export interface ReadRuleProps { export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } -export interface RetryRuleMigrationProps { - params: RetryRuleMigrationRequestParamsInput; - body: RetryRuleMigrationRequestBodyInput; -} export interface RulePreviewProps { query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput;