mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ 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:
parent
bdc4790272
commit
44a184c701
26 changed files with 612 additions and 22 deletions
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -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'
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -25,5 +25,4 @@ export const aiConnectorCardConfig: OnboardingCardConfig<AIConnectorCardMetadata
|
|||
)
|
||||
),
|
||||
checkComplete: checkAiConnectorsCardComplete,
|
||||
licenseTypeRequired: 'enterprise',
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
|
|
|
@ -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: [] },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 } };
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 }) => ({
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue