diff --git a/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.test.tsx b/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.test.tsx new file mode 100644 index 0000000000000..9f73821f37c1c --- /dev/null +++ b/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { SiemMigrationStartUpsellSection } from './siem_migrations_start'; + +describe('SiemMigrationStartUpsellSection', () => { + it('should render the component with all sections correctly', () => { + render( + + ); + + expect(screen.getByTestId('siemMigrationStartUpsellSection')).toBeVisible(); + + expect(screen.getByTestId('siemMigrationStartUpsellTitle')).toBeVisible(); + expect(screen.getByTestId('siemMigrationStartUpsellTitle')).toHaveTextContent('title'); + + expect(screen.getByTestId('siemMigrationStartUpsellMessage')).toBeVisible(); + expect(screen.getByTestId('siemMigrationStartUpsellMessage')).toHaveTextContent( + 'upgradeMessage' + ); + + expect(screen.getByTestId('siemMigrationStartUpsellHref')).toBeVisible(); + expect(screen.getByTestId('siemMigrationStartUpsellHref')).toHaveAttribute( + 'href', + 'https://upgrade.Href' + ); + }); + + it('should render the component without upgradeHref', () => { + render(); + + expect(screen.queryByTestId('SiemMigrationStartUpsellHref')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.tsx b/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.tsx new file mode 100644 index 0000000000000..bf61709313ff3 --- /dev/null +++ b/x-pack/solutions/security/packages/upselling/sections/siem_migrations_start.tsx @@ -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 { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import React from 'react'; + +export const SiemMigrationStartUpsellSection = React.memo(function SiemMigrationStartUpsellSection({ + title, + upgradeMessage, + upgradeHref, +}: { + title: React.ReactNode; + upgradeMessage: React.ReactNode; + upgradeHref?: string; +}) { + return ( + <> + + + + + + {upgradeMessage} + + + {upgradeHref ? ( + + + {'Manage License'} + + + ) : null} + + + + + ); +}); diff --git a/x-pack/solutions/security/packages/upselling/service/types.ts b/x-pack/solutions/security/packages/upselling/service/types.ts index 2b335e2ebf27b..02e7b19af4c03 100644 --- a/x-pack/solutions/security/packages/upselling/service/types.ts +++ b/x-pack/solutions/security/packages/upselling/service/types.ts @@ -20,7 +20,8 @@ export type UpsellingSectionId = | 'endpoint_custom_notification' | 'cloud_security_posture_integration_installation' | 'ruleDetailsEndpointExceptions' - | 'automatic_import'; + | 'automatic_import' + | 'siem_migrations_start'; export type UpsellingMessageId = | 'investigation_guide' diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx index 9e12d785bf32a..948a351419e3f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx @@ -24,7 +24,13 @@ export const CenteredLoadingSpinner = React.memo( [topOffset, euiTheme] ); - return ; + return ( + + ); } ); CenteredLoadingSpinner.displayName = 'CenteredLoadingSpinner'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 89df2a0510d25..45ff50bf530e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -58,6 +58,7 @@ import { UpsellingService } from '@kbn/security-solution-upselling/service'; import { calculateBounds } from '@kbn/data-plugin/common'; import { alertingPluginMock } from '@kbn/alerting-plugin/public/mocks'; import { createTelemetryServiceMock } from '../telemetry/telemetry_service.mock'; +import { createSiemMigrationsMock } from '../../mock/mock_siem_migrations_service'; const mockUiSettings: Record = { [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, @@ -128,6 +129,7 @@ export const createStartServicesMock = ( const mockSetHeaderActionMenu = jest.fn(); const timelineDataService = dataPluginMock.createStartContract(); const alerting = alertingPluginMock.createStartContract(); + const siemMigrations = createSiemMigrationsMock(); /* * Below mocks are needed by unified field list @@ -258,6 +260,7 @@ export const createStartServicesMock = ( upselling: new UpsellingService(), timelineDataService, alerting, + siemMigrations, } as unknown as StartServices; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/mock/index.ts index 3be928b9dcc1f..30b64c532f3a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/index.ts @@ -20,3 +20,4 @@ export * from './timeline_results'; export * from './utils'; export * from './create_store'; export * from './create_react_query_wrapper'; +export * from './mock_siem_migrations_service'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_siem_migrations_service.ts b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_siem_migrations_service.ts new file mode 100644 index 0000000000000..7e0e563a44a5a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_siem_migrations_service.ts @@ -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 { createTelemetryServiceMock } from '../lib/telemetry/telemetry_service.mock'; + +const createRuleMigrationStorageMock = () => { + return { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + }; +}; + +export const createSiemMigrationsMock = () => { + return { + rules: { + getLatestStats$: jest.fn(), + getMissingCapabilities: jest.fn(), + hasMissingCapabilities: jest.fn(), + isAvailable: jest.fn(), + startPolling: jest.fn(), + createRuleMigration: jest.fn(), + upsertMigrationResources: jest.fn(), + startRuleMigration: jest.fn(), + getRuleMigrationStats: jest.fn(), + getRuleMigrationsStats: jest.fn(), + getMissingResources: jest.fn(), + getIntegrations: jest.fn(), + connectorIdStorage: createRuleMigrationStorageMock(), + traceOptionsStorage: createRuleMigrationStorageMock(), + telemetry: createTelemetryServiceMock(), + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts index 9e8426df92ddc..14d50283337b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/index.ts @@ -25,5 +25,4 @@ export const aiConnectorCardConfig: OnboardingCardConfig getCardIcon(OnboardingCardId.siemMigrationsStart), - licenseTypeRequired: 'enterprise', Component: React.lazy( () => import( 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 50d5ff5810e70..4c126f30a464b 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 @@ -53,7 +53,7 @@ export const RuleMigrationsPanels = React.memo( ); return ( - + {!isConnectorsCardComplete && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.test.tsx new file mode 100644 index 0000000000000..373fa5e3a1ffd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { ComponentProps } from 'react'; +import React from 'react'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import * as useLatestStatsModule from '../../../../../../siem_migrations/rules/service/hooks/use_latest_stats'; +import StartMigrationCard from './start_migration_card'; +import * as useUpsellingComponentModule from '../../../../../../common/hooks/use_upselling'; +import { TestProviders } from '../../../../../../common/mock'; +import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; +import type { RuleMigrationStats } from '../../../../../../siem_migrations/rules/types'; +import { OnboardingCardId } from '../../../../../constants'; +import * as useGetMigrationTranslationStatsModule from '../../../../../../siem_migrations/rules/logic/use_get_migration_translation_stats'; +import * as useGetMissingResourcesModule from '../../../../../../siem_migrations/rules/service/hooks/use_get_missing_resources'; + +const useLatestStatsSpy = jest.spyOn(useLatestStatsModule, 'useLatestStats'); + +const useUpsellingComponentMock = jest.spyOn(useUpsellingComponentModule, 'useUpsellingComponent'); + +const useGetMigrationTranslationStatsSpy = jest.spyOn( + useGetMigrationTranslationStatsModule, + 'useGetMigrationTranslationStats' +); + +const useGetMissingResourcesMock = jest.spyOn( + useGetMissingResourcesModule, + 'useGetMissingResources' +); + +const MockUpsellingComponent = () => { + return
{`Start Migrations Upselling Component`}
; +}; + +const mockedLatestStats = { + data: [], + isLoading: false, + refreshStats: jest.fn(), +}; + +const mockTranslationStats = { + isLoading: false, + data: { + id: '1', + rules: { + total: 1, + failed: 0, + success: { + result: { + full: 1, + partial: 0, + failed: 0, + }, + }, + }, + }, +} as unknown as ReturnType< + typeof useGetMigrationTranslationStatsModule.useGetMigrationTranslationStats +>; + +const mockMissingResources = { + getMissingResources: jest.fn(() => []), + isLoading: false, +} as unknown as ReturnType; + +type TestComponentProps = ComponentProps; + +const defaultProps: TestComponentProps = { + setComplete: jest.fn(), + isCardComplete: jest.fn( + (cardId: OnboardingCardId) => cardId === OnboardingCardId.siemMigrationsAiConnectors + ), + setExpandedCardId: jest.fn(), + checkComplete: jest.fn(), + isCardAvailable: () => true, + checkCompleteMetadata: { + missingCapabilities: [], + }, +}; + +const renderTestComponent = (props: Partial> = {}) => { + const finalProps: TestComponentProps = { + ...defaultProps, + ...props, + }; + + return render( + + + + ); +}; + +describe('StartMigrationsBody', () => { + beforeEach(() => { + useLatestStatsSpy.mockReturnValue(mockedLatestStats); + useUpsellingComponentMock.mockReturnValue(null); + useGetMigrationTranslationStatsSpy.mockReturnValue(mockTranslationStats); + useGetMissingResourcesMock.mockReturnValue(mockMissingResources); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render upsell correctly when available', () => { + useUpsellingComponentMock.mockReturnValue(MockUpsellingComponent); + + renderTestComponent(); + + expect(screen.getByTestId('mockUpsellSection')).toBeVisible(); + expect(screen.getByTestId('startMigrationUploadRulesButton')).toBeVisible(); + expect(screen.getByTestId('startMigrationUploadRulesButton')).toBeDisabled(); + }); + + it('should render missing Privileges Callout when there are missing capabilities but NO Upsell', () => { + renderTestComponent({ + checkCompleteMetadata: { + missingCapabilities: ['missingPrivileges'], + }, + }); + + expect(screen.getByTestId('missingPrivilegesGroup')).toBeVisible(); + }); + + it('should render component correctly when no upsell and no missing capabilities', () => { + renderTestComponent(); + + expect(screen.getByTestId('StartMigrationsCardBody')).toBeVisible(); + expect(screen.getByTestId('StartMigrationsCardBody')).not.toBeEmptyDOMElement(); + }); + + it('should mark card as complete when migration is finished', async () => { + useLatestStatsSpy.mockReturnValue({ + ...mockedLatestStats, + data: [ + { + id: '1', + status: SiemMigrationTaskStatus.FINISHED, + } as unknown as RuleMigrationStats, + ], + }); + + await act(async () => { + renderTestComponent(); + }); + + await waitFor(() => { + expect(defaultProps.setComplete).toHaveBeenCalledWith(true); + }); + }); + + it('should render loader when migration handler is loading', async () => { + const latestStatus = { + ...mockedLatestStats, + isLoading: true, + data: [ + { + id: '1', + status: SiemMigrationTaskStatus.RUNNING, + rules: { + total: 1, + pending: 1, + processing: 1, + completed: 0, + failed: 0, + }, + } as unknown as RuleMigrationStats, + ], + }; + + useLatestStatsSpy.mockReturnValue(latestStatus); + + renderTestComponent(); + + expect(screen.getByTestId('centeredLoadingSpinner')).toBeVisible(); + }); + + it('should render progress bar when migration is running', async () => { + const latestStats = { + ...mockedLatestStats, + isLoading: false, + data: [ + { + id: '1', + status: SiemMigrationTaskStatus.RUNNING, + rules: { + total: 1, + pending: 1, + processing: 1, + completed: 0, + failed: 0, + }, + } as unknown as RuleMigrationStats, + ], + }; + useLatestStatsSpy.mockReturnValue(latestStats); + + await act(async () => { + renderTestComponent(); + }); + + await waitFor(() => { + expect(screen.getByTestId('migrationProgressPanel')).toBeVisible(); + }); + }); + + it('should render result panel when migration is finished', async () => { + const latestStats = { + ...mockedLatestStats, + isLoading: false, + data: [ + { + id: '1', + status: SiemMigrationTaskStatus.FINISHED, + rules: { + total: 1, + pending: 0, + processing: 0, + completed: 1, + failed: 0, + }, + } as unknown as RuleMigrationStats, + ], + }; + + useLatestStatsSpy.mockReturnValue(latestStats); + + await act(async () => { + renderTestComponent(); + }); + + expect(screen.getByTestId('ruleMigrationPanelGroup')).toBeVisible(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx index 34e7a25d9a125..10b91e57ed2c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_card.tsx @@ -6,7 +6,8 @@ */ import React, { useCallback, useEffect, useMemo } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { useUpsellingComponent } from '../../../../../../common/hooks/use_upselling'; import { PanelText } from '../../../../../../common/components/panel_text'; import { RuleMigrationDataInputWrapper } from '../../../../../../siem_migrations/rules/components/data_input_flyout/data_input_wrapper'; import { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; @@ -23,6 +24,7 @@ import { MissingPrivilegesCallOut, MissingPrivilegesDescription, } from '../../common/missing_privileges'; +import { UploadRulesSectionPanel } from './upload_rules_panel'; const StartMigrationsBody: OnboardingCardComponent = React.memo( ({ setComplete, isCardComplete, setExpandedCardId }) => { @@ -49,7 +51,11 @@ const StartMigrationsBody: OnboardingCardComponent = React.memo( return ( - + {isLoading ? ( ) : ( @@ -72,10 +78,26 @@ StartMigrationsBody.displayName = 'StartMigrationsBody'; export const StartMigrationCard: OnboardingCardComponent = React.memo( ({ checkCompleteMetadata, ...props }) => { + const UpsellSectionComp = useUpsellingComponent('siem_migrations_start'); if (!checkCompleteMetadata) { return ; } + if (UpsellSectionComp) { + return ( + + + + + + + + + + + ); + } + const { missingCapabilities } = checkCompleteMetadata; if (missingCapabilities.length > 0) { return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts new file mode 100644 index 0000000000000..bd30310d054ae --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { SiemMigrationTaskStatus } from '../../../../../../../common/siem_migrations/constants'; +import { createStartServicesMock } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import type { SiemMigrationsService } from '../../../../../../siem_migrations/service'; +import { checkStartMigrationCardComplete } from './start_migration_check_complete'; + +describe('startMigrationCheckComplete', () => { + test('should return default values if siem migrations are not available', async () => { + // Arrange + const siemMigrations = { + rules: { + getMissingCapabilities: jest.fn().mockReturnValue([]), + isAvailable: jest.fn().mockReturnValue(false), + }, + } as unknown as SiemMigrationsService; + + const services = { + ...createStartServicesMock(), + siemMigrations, + }; + const result = await checkStartMigrationCardComplete(services); + + expect(result).toEqual({ isComplete: false, metadata: { missingCapabilities: [] } }); + }); + + test('should query Stats if siem migrations are available', async () => { + const siemMigrations = { + rules: { + getMissingCapabilities: jest.fn().mockReturnValue([]), + isAvailable: jest.fn().mockReturnValue(true), + getRuleMigrationsStats: jest.fn().mockReturnValue([ + { + status: SiemMigrationTaskStatus.FINISHED, + }, + ]), + }, + } as unknown as SiemMigrationsService; + + const services = { + ...createStartServicesMock(), + siemMigrations, + }; + + const result = await checkStartMigrationCardComplete(services); + + expect(siemMigrations.rules.getRuleMigrationsStats).toHaveBeenCalled(); + + expect(result).toEqual({ + isComplete: true, + metadata: { missingCapabilities: [] }, + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts index 2e3a8f238ac43..ec5d100ff61fb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/start_migration/start_migration_check_complete.ts @@ -19,12 +19,11 @@ export const checkStartMigrationCardComplete: OnboardingCardCheckComplete< let isComplete = false; - if (missingCapabilities.length === 0) { + if (siemMigrations.rules.isAvailable()) { const migrationsStats = await siemMigrations.rules.getRuleMigrationsStats(); isComplete = migrationsStats.some( (migrationStats) => migrationStats.status === SiemMigrationTaskStatus.FINISHED ); } - return { isComplete, metadata: { missingCapabilities } }; }; 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 ffa425f8e7df6..f2e93c3796944 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 @@ -27,16 +27,14 @@ export interface UploadRulesPanelProps { isUploadMore?: boolean; isDisabled?: boolean; } -export const UploadRulesPanel = React.memo( - ({ isUploadMore = false, isDisabled = false }) => { - const styles = useStyles(isUploadMore); - const { telemetry } = useKibana().services.siemMigrations.rules; - const { openFlyout } = useRuleMigrationDataInputContext(); - const onOpenFlyout = useCallback(() => { - openFlyout(); - telemetry.reportSetupMigrationOpen({ isFirstMigration: !isUploadMore }); - }, [openFlyout, telemetry, isUploadMore]); +export interface UploadRulesSectionPanelProps extends UploadRulesPanelProps { + onOpenFlyout?: React.MouseEventHandler; +} + +export const UploadRulesSectionPanel = React.memo( + function UploadRulesSectionPanel({ isUploadMore = false, isDisabled = false, onOpenFlyout }) { + const styles = useStyles(isUploadMore); return ( @@ -75,6 +73,7 @@ export const UploadRulesPanel = React.memo( {isUploadMore ? ( ( ) : ( ( ); } ); + +export const UploadRulesPanel = React.memo(function UploadRulesPanel({ + isUploadMore = false, + isDisabled = false, +}: UploadRulesPanelProps) { + const { telemetry } = useKibana().services.siemMigrations.rules; + const { openFlyout } = useRuleMigrationDataInputContext(); + + const onOpenFlyout = useCallback(() => { + openFlyout(); + telemetry.reportSetupMigrationOpen({ isFirstMigration: !isUploadMore }); + }, [openFlyout, telemetry, isUploadMore]); + + return ( + + ); +}); + UploadRulesPanel.displayName = 'UploadRulesPanel'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts index c424f28cef71b..612e7ccba2f93 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { SIEM_MIGRATIONS_FEATURE_ID } from '@kbn/security-solution-features/constants'; import { OnboardingTopicId } from './constants'; import { defaultBodyConfig, @@ -28,8 +27,6 @@ export const onboardingConfig: TopicConfig[] = [ defaultMessage: 'SIEM Rule migration', }), body: siemMigrationsBodyConfig, - licenseTypeRequired: 'enterprise', - capabilitiesRequired: `${SIEM_MIGRATIONS_FEATURE_ID}.all`, disabledExperimentalFlagRequired: 'siemMigrationsDisabled', }, ]; 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 9fca25e88f916..37b8a0fed66c0 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 @@ -33,7 +33,7 @@ export const MigrationProgressPanel = React.memo( const preparing = migrationStats.rules.pending === migrationStats.rules.total; return ( - + 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 0c86ca3a8e615..6f80eea2275b9 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 @@ -96,6 +96,14 @@ export class SiemRulesMigrationsService { return this.getMissingCapabilities(level).length > 0; } + /** + * checks if the service is available based on + * + * - the license + * - capabilities + * - feature flag + * + */ public isAvailable() { return ( !ExperimentalFeaturesService.get().siemMigrationsDisabled && diff --git a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx index 56775b6c4433e..f77c5a32efe6f 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx +++ b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx @@ -19,6 +19,14 @@ export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell( ) ); +export const SiemMigrationsStartUpsellSectionLazy = withSuspenseUpsell( + lazy(() => + import('./sections/siem_migration_start').then(({ SiemMigrationStartUpsellSection }) => ({ + default: SiemMigrationStartUpsellSection, + })) + ) +); + export const EntityAnalyticsUpsellingPageLazy = lazy(() => import('./pages/entity_analytics_upselling').then(({ EntityAnalyticsUpsellingPageESS }) => ({ default: EntityAnalyticsUpsellingPageESS, diff --git a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/register_upsellings.tsx index 36a3fc13e240e..eb7124cb1c942 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/register_upsellings.tsx +++ b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -31,6 +31,7 @@ import { AttackDiscoveryUpsellingPageLazy, EntityAnalyticsUpsellingPageLazy, EntityAnalyticsUpsellingSectionLazy, + SiemMigrationsStartUpsellSectionLazy, } from './lazy_upselling'; interface UpsellingsConfig { @@ -111,6 +112,11 @@ export const upsellingSections: UpsellingSections = [ minimumLicenseRequired: 'platinum', component: EntityAnalyticsUpsellingSectionLazy, }, + { + id: 'siem_migrations_start', + minimumLicenseRequired: 'enterprise', + component: SiemMigrationsStartUpsellSectionLazy, + }, ]; // Upsellings for sections, linked by arbitrary ids diff --git a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/sections/siem_migration_start.tsx b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/sections/siem_migration_start.tsx new file mode 100644 index 0000000000000..3d0a370146ef9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/sections/siem_migration_start.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 { SiemMigrationStartUpsellSection as SiemMigrationStartUpsellSectionCommon } from '@kbn/security-solution-upselling/sections/siem_migrations_start'; +import { useKibana } from '../../common/services'; +import * as i18n from '../translations'; + +export const SiemMigrationStartUpsellSection = () => { + const { services } = useKibana(); + return ( + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/translations.ts b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/translations.ts index 9af27d3ca5242..867755b9ba9d5 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/public/upselling/translations.ts @@ -14,3 +14,18 @@ export const UPGRADE_LICENSE_MESSAGE = (requiredLicense: string) => requiredLicense, }, }); + +export const SIEM_MIGRATION_UPSELLING_TITLE = (requiredLicense: string) => + i18n.translate('xpack.securitySolutionEss.upselling.siemMigrations.title', { + defaultMessage: '{requiredLicense} license required', + values: { + requiredLicense, + }, + }); + +export const SIEM_MIGRATION_UPGRADE_LICENSE_MESSAGE = i18n.translate( + 'xpack.securitySolutionEss.upselling.siemMigrations.upgradeLicenseMessage', + { + defaultMessage: 'To use this feature, upgrade your Elastic subscription level.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx index 526654a6f4509..014ba27debec4 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx @@ -41,6 +41,14 @@ export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell( ) ); +export const SiemMigrationsStartUpsellSectionLazy = withSuspenseUpsell( + lazy(() => + import('./sections/siem_migrations/siem_migrations_start').then( + ({ SiemMigrationStartUpsellSection }) => ({ default: SiemMigrationStartUpsellSection }) + ) + ) +); + export const AttackDiscoveryUpsellingPageLazy = withSuspenseUpsell( lazy(() => import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageServerless }) => ({ diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/siem_migrations/siem_migrations_start.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/siem_migrations/siem_migrations_start.tsx new file mode 100644 index 0000000000000..11d1df1d11510 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/sections/siem_migrations/siem_migrations_start.tsx @@ -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 React from 'react'; +import { SiemMigrationStartUpsellSection as SiemMigrationStartUpsellSectionCommon } from '@kbn/security-solution-upselling/sections/siem_migrations_start'; +import * as i18n from '../../translations'; + +export const SiemMigrationStartUpsellSection = () => { + return ( + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/translations.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/translations.ts index f70a36f90348f..39db5f3198b3e 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/translations.ts @@ -26,3 +26,19 @@ export const ADDITIONAL_CHARGES_MESSAGE = i18n.translate( 'Please be aware that activating these features may incur additional charges depending on your subscription plan. Review your plan details carefully to avoid unexpected costs before proceeding.', } ); + +export const SIEM_MIGRATION_UPSELLING_TITLE = (requiredTier: string) => + i18n.translate('xpack.securitySolutionServerless.upselling.siemMigrations.title', { + defaultMessage: 'Security {requiredTier} tier required', + values: { + requiredTier, + }, + }); + +export const SIEM_MIGRATION_UPGRADE_MESSAGE = i18n.translate( + 'xpack.securitySolutionServerless.upselling.siemMigrations.upgradeTierMessage', + { + defaultMessage: + 'To use this feature, you need to upgrade your Elastic Cloud Serverless feature tier. Update your subscription or contact your administrator for assistance.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx index 6079a13ff6d2c..e726917d356b7 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/upsellings.tsx @@ -33,6 +33,7 @@ import { EntityAnalyticsUpsellingPageLazy, EntityAnalyticsUpsellingSectionLazy, OsqueryResponseActionsUpsellingSectionLazy, + SiemMigrationsStartUpsellSectionLazy, ThreatIntelligencePaywallLazy, } from './lazy_upselling'; import * as i18n from './translations'; @@ -141,6 +142,11 @@ export const upsellingSections: UpsellingSections = [ /> ), }, + { + id: 'siem_migrations_start', + pli: ProductFeatureKey.siemMigrations, + component: SiemMigrationsStartUpsellSectionLazy, + }, { id: 'automatic_import', pli: ProductFeatureKey.automaticImport,