[ Siem Migrations ] Upsell Siem Migrations Start (#212607)

## Summary

This PR adds the Upsell section for SIEM Migration Start section.

- [Design
Source](https://www.figma.com/design/BD9GZZz6y8pfSbubAt5H2W/%5B8.18%5D-GenAI-Powered-SIEM-Migration%3A-Rule-translation?node-id=63-81202&p=f&t=8x9RlFegceXzwYQf-0)

SIEM migrations has below requirements in ESS and Serverless and if
these requirements are not met, We show the upsell sections.

- ESS
   - `Enterprise` license.
- Serverless
   - `Complete` Product tier

## Demo

|Instance|Demo|
|---|---|
|Serverless|<video
src="https://github.com/user-attachments/assets/58d3ce98-7108-4d74-9f5c-e270804f2666"/>|
|ESS|<video
src="https://github.com/user-attachments/assets/85b650a7-fa11-4855-9927-aab89a2ed8ef"/>|




### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...
This commit is contained in:
Jatin Kathuria 2025-02-28 04:51:31 +01:00 committed by GitHub
parent bdc4790272
commit 44a184c701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 612 additions and 22 deletions

View file

@ -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(
<SiemMigrationStartUpsellSection
title="title"
upgradeMessage="upgradeMessage"
upgradeHref="https://upgrade.Href"
/>
);
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(<SiemMigrationStartUpsellSection title="title" upgradeMessage="upgradeMessage" />);
expect(screen.queryByTestId('SiemMigrationStartUpsellHref')).not.toBeInTheDocument();
});
});

View file

@ -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 (
<>
<EuiPanel data-test-subj="siemMigrationStartUpsellSection" paddingSize="none" hasBorder>
<EuiCallOut
data-test-subj="siemMigrationStartUpsellTitle"
title={title}
color="warning"
iconType="lock"
>
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiText size="s" data-test-subj="siemMigrationStartUpsellMessage">
{upgradeMessage}
</EuiText>
</EuiFlexItem>
{upgradeHref ? (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="siemMigrationStartUpsellHref"
href={upgradeHref}
color="warning"
fill
>
{'Manage License'}
</EuiButton>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiCallOut>
</EuiPanel>
</>
);
});

View file

@ -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'

View file

@ -24,7 +24,13 @@ export const CenteredLoadingSpinner = React.memo<CenteredLoadingSpinnerProps>(
[topOffset, euiTheme]
);
return <EuiLoadingSpinner {...euiLoadingSpinnerProps} style={style} />;
return (
<EuiLoadingSpinner
data-test-subj="centeredLoadingSpinner"
{...euiLoadingSpinnerProps}
style={style}
/>
);
}
);
CenteredLoadingSpinner.displayName = 'CenteredLoadingSpinner';

View file

@ -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<string, unknown> = {
[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;
};

View file

@ -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';

View file

@ -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(),
},
};
};

View file

@ -25,5 +25,4 @@ export const aiConnectorCardConfig: OnboardingCardConfig<AIConnectorCardMetadata
)
),
checkComplete: checkAiConnectorsCardComplete,
licenseTypeRequired: 'enterprise',
};

View file

@ -17,7 +17,6 @@ export const startMigrationCardConfig: OnboardingCardConfig<StartMigrationCardMe
id: OnboardingCardId.siemMigrationsStart,
title: START_MIGRATION_CARD_TITLE,
icon: () => getCardIcon(OnboardingCardId.siemMigrationsStart),
licenseTypeRequired: 'enterprise',
Component: React.lazy(
() =>
import(

View file

@ -53,7 +53,7 @@ export const RuleMigrationsPanels = React.memo<RuleMigrationsPanelsProps>(
);
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup data-test-subj="ruleMigrationPanelGroup" direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
{!isConnectorsCardComplete && (
<>

View file

@ -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 <div data-test-subj="mockUpsellSection">{`Start Migrations Upselling Component`}</div>;
};
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<typeof useGetMissingResourcesModule.useGetMissingResources>;
type TestComponentProps = ComponentProps<typeof StartMigrationCard>;
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<ComponentProps<typeof StartMigrationCard>> = {}) => {
const finalProps: TestComponentProps = {
...defaultProps,
...props,
};
return render(
<TestProviders>
<StartMigrationCard {...finalProps} />
</TestProviders>
);
};
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();
});
});

View file

@ -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 (
<RuleMigrationDataInputWrapper onFlyoutClosed={refreshStats}>
<OnboardingCardContentPanel paddingSize="none" className={styles}>
<OnboardingCardContentPanel
data-test-subj="StartMigrationsCardBody"
paddingSize="none"
className={styles}
>
{isLoading ? (
<CenteredLoadingSpinner />
) : (
@ -72,10 +78,26 @@ StartMigrationsBody.displayName = 'StartMigrationsBody';
export const StartMigrationCard: OnboardingCardComponent<StartMigrationCardMetadata> = React.memo(
({ checkCompleteMetadata, ...props }) => {
const UpsellSectionComp = useUpsellingComponent('siem_migrations_start');
if (!checkCompleteMetadata) {
return <CenteredLoadingSpinner />;
}
if (UpsellSectionComp) {
return (
<OnboardingCardContentPanel paddingSize="none">
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem>
<UpsellSectionComp />
</EuiFlexItem>
<EuiFlexItem>
<UploadRulesSectionPanel isUploadMore={false} isDisabled />
</EuiFlexItem>
</EuiFlexGroup>
</OnboardingCardContentPanel>
);
}
const { missingCapabilities } = checkCompleteMetadata;
if (missingCapabilities.length > 0) {
return (

View file

@ -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: [] },
});
});
});

View file

@ -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 } };
};

View file

@ -27,16 +27,14 @@ export interface UploadRulesPanelProps {
isUploadMore?: boolean;
isDisabled?: boolean;
}
export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(
({ isUploadMore = false, isDisabled = false }) => {
const styles = useStyles(isUploadMore);
const { telemetry } = useKibana().services.siemMigrations.rules;
const { openFlyout } = useRuleMigrationDataInputContext();
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
openFlyout();
telemetry.reportSetupMigrationOpen({ isFirstMigration: !isUploadMore });
}, [openFlyout, telemetry, isUploadMore]);
export interface UploadRulesSectionPanelProps extends UploadRulesPanelProps {
onOpenFlyout?: React.MouseEventHandler;
}
export const UploadRulesSectionPanel = React.memo<UploadRulesSectionPanelProps>(
function UploadRulesSectionPanel({ isUploadMore = false, isDisabled = false, onOpenFlyout }) {
const styles = useStyles(isUploadMore);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize={isUploadMore ? 'm' : 'l'}>
@ -75,6 +73,7 @@ export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(
<EuiFlexItem grow={false}>
{isUploadMore ? (
<EuiButtonEmpty
data-test-subj="startMigrationUploadMoreButton"
iconType="download"
iconSide="right"
onClick={onOpenFlyout}
@ -84,6 +83,7 @@ export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(
</EuiButtonEmpty>
) : (
<EuiButton
data-test-subj="startMigrationUploadRulesButton"
iconType="download"
iconSide="right"
onClick={onOpenFlyout}
@ -98,4 +98,26 @@ export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(
);
}
);
export const UploadRulesPanel = React.memo<UploadRulesPanelProps>(function UploadRulesPanel({
isUploadMore = false,
isDisabled = false,
}: UploadRulesPanelProps) {
const { telemetry } = useKibana().services.siemMigrations.rules;
const { openFlyout } = useRuleMigrationDataInputContext();
const onOpenFlyout = useCallback<React.MouseEventHandler>(() => {
openFlyout();
telemetry.reportSetupMigrationOpen({ isFirstMigration: !isUploadMore });
}, [openFlyout, telemetry, isUploadMore]);
return (
<UploadRulesSectionPanel
isDisabled={isDisabled}
isUploadMore={isUploadMore}
onOpenFlyout={onOpenFlyout}
/>
);
});
UploadRulesPanel.displayName = 'UploadRulesPanel';

View file

@ -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',
},
];

View file

@ -33,7 +33,7 @@ export const MigrationProgressPanel = React.memo<MigrationProgressPanelProps>(
const preparing = migrationStats.rules.pending === migrationStats.rules.total;
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="m">
<EuiPanel data-test-subj="migrationProgressPanel" hasShadow={false} hasBorder paddingSize="m">
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem grow={false}>
<PanelText size="s" semiBold>

View file

@ -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 &&

View file

@ -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,

View file

@ -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

View file

@ -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 (
<SiemMigrationStartUpsellSectionCommon
title={i18n.SIEM_MIGRATION_UPSELLING_TITLE('Enterprise')}
upgradeMessage={i18n.SIEM_MIGRATION_UPGRADE_LICENSE_MESSAGE}
upgradeHref={services.application.getUrlForApp('management', {
path: 'stack/license_management',
})}
/>
);
};

View file

@ -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.',
}
);

View file

@ -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 }) => ({

View file

@ -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 (
<SiemMigrationStartUpsellSectionCommon
title={i18n.SIEM_MIGRATION_UPSELLING_TITLE('Complete')}
upgradeMessage={i18n.SIEM_MIGRATION_UPGRADE_MESSAGE}
/>
);
};

View file

@ -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.',
}
);

View file

@ -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,