diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 1ace41854898b..64d289cd65f3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -18,7 +18,6 @@ import React, { } from 'react'; import { EuiMarkdownEditor } from '@elastic/eui'; import type { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; -import { useLicense } from '../../hooks/use_license'; import { uiPlugins, parsingPlugins, processingPlugins } from './plugins'; import { useUpsellingMessage } from '../../hooks/use_upselling'; @@ -72,12 +71,10 @@ const MarkdownEditorComponent = forwardRef { - return uiPlugins({ licenseIsPlatinum, insightsUpsellingMessage }); - }, [licenseIsPlatinum, insightsUpsellingMessage]); + return uiPlugins({ insightsUpsellingMessage }); + }, [insightsUpsellingMessage]); // @ts-expect-error update types useImperativeHandle(ref, () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 681caa3dd4cb8..ed2c60ea2e961 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -27,15 +27,12 @@ export const { export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix]; export const uiPlugins = ({ - licenseIsPlatinum, insightsUpsellingMessage, }: { - licenseIsPlatinum: boolean; insightsUpsellingMessage: string | null; }) => { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ - licenseIsPlatinum, insightsUpsellingMessage, }); if (currentPlugins.includes(insightPluginWithLicense.name) === false) { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx index e29ad4ba89eb2..2b4ae4d2d9fcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -134,35 +134,21 @@ describe('insight component renderer', () => { describe('plugin', () => { it('renders insightsUpsellingMessage when provided', () => { const insightsUpsellingMessage = 'test message'; - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage }); + const result = plugin({ insightsUpsellingMessage }); expect(result.button.label).toEqual(insightsUpsellingMessage); }); it('disables the button when insightsUpsellingMessage is provided', () => { const insightsUpsellingMessage = 'test message'; - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage }); + const result = plugin({ insightsUpsellingMessage }); expect(result.button.isDisabled).toBeTruthy(); }); - it('disables the button when license is not Platinum', () => { - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null }); - - expect(result.button.isDisabled).toBeTruthy(); - }); - - it('show investigate message when license is Platinum', () => { - const result = plugin({ licenseIsPlatinum: true, insightsUpsellingMessage: null }); + it('show investigate message when insightsUpsellingMessage is not provided', () => { + const result = plugin({ insightsUpsellingMessage: null }); expect(result.button.label).toEqual('Investigate'); }); - - it('show upsell message when license is not Platinum', () => { - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null }); - - expect(result.button.label).toEqual( - 'Upgrade to platinum to make use of insights in investigation guides' - ); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 81f1a77768189..7efbebb776cd7 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -542,20 +542,16 @@ const exampleInsight = `${insightPrefix}{ }}`; export const plugin = ({ - licenseIsPlatinum, insightsUpsellingMessage, }: { - licenseIsPlatinum: boolean; insightsUpsellingMessage: string | null; }) => { - const label = licenseIsPlatinum ? i18n.INVESTIGATE : i18n.INSIGHT_UPSELL; - return { name: 'insights', button: { - label: insightsUpsellingMessage ?? label, + label: insightsUpsellingMessage ?? i18n.INVESTIGATE, iconType: 'timelineWithArrow', - isDisabled: !licenseIsPlatinum || !!insightsUpsellingMessage, + isDisabled: !!insightsUpsellingMessage, }, helpText: (
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts index e33a1f0d73539..1f2da4b0dcdd8 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts @@ -11,10 +11,6 @@ export const LABEL = i18n.translate('xpack.securitySolution.markdown.insight.lab defaultMessage: 'Label', }); -export const INSIGHT_UPSELL = i18n.translate('xpack.securitySolution.markdown.insight.upsell', { - defaultMessage: 'Upgrade to platinum to make use of insights in investigation guides', -}); - export const INVESTIGATE = i18n.translate('xpack.securitySolution.markdown.insight.title', { defaultMessage: 'Investigate', }); diff --git a/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx b/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx deleted file mode 100644 index ee93861db2d7e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback } from 'react'; -import { - EuiCard, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiButton, - EuiTextColor, - EuiImage, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { useNavigation } from '../../lib/kibana'; -import * as i18n from './translations'; -import paywallPng from '../../images/entity_paywall.png'; - -const PaywallDiv = styled.div` - max-width: 75%; - margin: 0 auto; - .euiCard__betaBadgeWrapper { - .euiCard__betaBadge { - width: auto; - } - } - .platinumCardDescription { - padding: 0 15%; - } -`; -const StyledEuiCard = styled(EuiCard)` - span.euiTitle { - max-width: 540px; - display: block; - margin: 0 auto; - } -`; - -export const Paywall = memo(({ heading }: { heading?: string }) => { - const { getAppUrl, navigateTo } = useNavigation(); - const subscriptionUrl = getAppUrl({ - appId: 'management', - path: 'stack/license_management', - }); - const goToSubscription = useCallback(() => { - navigateTo({ url: subscriptionUrl }); - }, [navigateTo, subscriptionUrl]); - return ( - - } - display="subdued" - title={ -

- {heading} -

- } - description={false} - paddingSize="xl" - > - - - -

- {i18n.UPGRADE_MESSAGE} -

-
- -
- - {i18n.UPGRADE_BUTTON} - -
-
-
-
-
- - - - - -
- ); -}); - -Paywall.displayName = 'Paywall'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx index 6469b0bcffb17..cd70445dcebae 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx @@ -35,7 +35,7 @@ const RenderWrapper: React.FunctionComponent = ({ children }) => { describe('use_upselling', () => { test('useUpsellingComponent returns sections', () => { - mockUpselling.registerSections({ + mockUpselling.setSections({ entity_analytics_panel: TestComponent, }); @@ -47,7 +47,7 @@ describe('use_upselling', () => { }); test('useUpsellingPage returns pages', () => { - mockUpselling.registerPages({ + mockUpselling.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -57,9 +57,9 @@ describe('use_upselling', () => { expect(result.current).toBe(TestComponent); }); - test('useUpsellingMessage returns pages', () => { + test('useUpsellingMessage returns messages', () => { const testMessage = 'test message'; - mockUpselling.registerMessages({ + mockUpselling.setMessages({ investigation_guide: testMessage, }); @@ -72,7 +72,7 @@ describe('use_upselling', () => { test('useUpsellingMessage returns null when upsellingMessageId not found', () => { const emptyMessages = {}; - mockUpselling.registerMessages(emptyMessages); + mockUpselling.setPages(emptyMessages); const { result } = renderHook( () => useUpsellingMessage('my_fake_message_id' as 'investigation_guide'), diff --git a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx index 4011df08ad0cb..2083e6c21f5c2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx @@ -14,7 +14,7 @@ const TestComponent = () =>
{'TEST component'}
; describe('UpsellingService', () => { it('registers sections', async () => { const service = new UpsellingService(); - service.registerSections({ + service.setSections({ entity_analytics_panel: TestComponent, }); @@ -23,9 +23,24 @@ describe('UpsellingService', () => { expect(value.get('entity_analytics_panel')).toEqual(TestComponent); }); + it('overwrites registered sections when called twice', async () => { + const service = new UpsellingService(); + service.setSections({ + entity_analytics_panel: TestComponent, + }); + + service.setSections({ + osquery_automated_response_actions: TestComponent, + }); + + const value = await firstValueFrom(service.sections$); + + expect(Array.from(value.keys())).toEqual(['osquery_automated_response_actions']); + }); + it('registers pages', async () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -34,10 +49,25 @@ describe('UpsellingService', () => { expect(value.get(SecurityPageName.hosts)).toEqual(TestComponent); }); + it('overwrites registered pages when called twice', async () => { + const service = new UpsellingService(); + service.setPages({ + [SecurityPageName.hosts]: TestComponent, + }); + + service.setPages({ + [SecurityPageName.users]: TestComponent, + }); + + const value = await firstValueFrom(service.pages$); + + expect(Array.from(value.keys())).toEqual([SecurityPageName.users]); + }); + it('registers messages', async () => { const testMessage = 'test message'; const service = new UpsellingService(); - service.registerMessages({ + service.setMessages({ investigation_guide: testMessage, }); @@ -46,9 +76,23 @@ describe('UpsellingService', () => { expect(value.get('investigation_guide')).toEqual(testMessage); }); + it('overwrites registered messages when called twice', async () => { + const testMessage = 'test message'; + const service = new UpsellingService(); + service.setMessages({ + investigation_guide: testMessage, + }); + + service.setMessages({}); + + const value = await firstValueFrom(service.messages$); + + expect(Array.from(value.keys())).toEqual([]); + }); + it('"isPageUpsellable" returns true when page is upsellable', () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -57,7 +101,7 @@ describe('UpsellingService', () => { it('"getPageUpselling" returns page component when page is upsellable', () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); diff --git a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts index 14d4949e8ea4b..27d15c1d12768 100644 --- a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts +++ b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts @@ -43,24 +43,33 @@ export class UpsellingService { this.messages$ = this.messagesSubject$.asObservable(); } - registerSections(sections: SectionUpsellings) { + setSections(sections: SectionUpsellings) { + this.sections.clear(); + Object.entries(sections).forEach(([sectionId, component]) => { this.sections.set(sectionId as UpsellingSectionId, component); }); + this.sectionsSubject$.next(this.sections); } - registerPages(pages: PageUpsellings) { + setPages(pages: PageUpsellings) { + this.pages.clear(); + Object.entries(pages).forEach(([pageId, component]) => { this.pages.set(pageId as SecurityPageName, component); }); + this.pagesSubject$.next(this.pages); } - registerMessages(messages: MessageUpsellings) { + setMessages(messages: MessageUpsellings) { + this.messages.clear(); + Object.entries(messages).forEach(([messageId, component]) => { this.messages.set(messageId as UpsellingMessageId, component); }); + this.messagesSubject$.next(this.messages); } diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.tsx b/x-pack/plugins/security_solution/public/common/links/links.test.tsx index b1ebd10d07851..7e00e39f75437 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.tsx +++ b/x-pack/plugins/security_solution/public/common/links/links.test.tsx @@ -182,9 +182,9 @@ describe('Security links', () => { expect(result.current).toStrictEqual([networkLinkItem]); }); - it('should return unauthorized page when page has upselling', async () => { + it('should return unauthorized page when page has upselling (serverless)', async () => { const upselling = new UpsellingService(); - upselling.registerPages({ [SecurityPageName.network]: () => }); + upselling.setPages({ [SecurityPageName.network]: () => }); const { result, waitForNextUpdate } = renderUseAppLinks(); const networkLinkItem = { @@ -192,8 +192,6 @@ describe('Security links', () => { title: 'Network', path: '/network', capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${CASES_FEATURE_ID}.write_cases`], - experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, - hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, licenseType: 'basic' as const, }; @@ -249,6 +247,67 @@ describe('Security links', () => { expect(result.current).toStrictEqual([{ ...networkLinkItem, unauthorized: true }]); }); + + it('should return unauthorized page when page has upselling (ESS)', async () => { + const upselling = new UpsellingService(); + upselling.setPages({ [SecurityPageName.network]: () => }); + const { result, waitForNextUpdate } = renderUseAppLinks(); + const hostLinkItem = { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum' as const, + }; + + mockUpselling.setPages({ + [SecurityPageName.hosts]: () => , + }); + + await act(async () => { + updateAppLinks([hostLinkItem], { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + upselling: mockUpselling, + }); + await waitForNextUpdate(); + }); + expect(result.current).toStrictEqual([{ ...hostLinkItem, unauthorized: true }]); + + // cleanup + mockUpselling.setPages({}); + }); + + it('should filter out experimental page even if it has upselling', async () => { + const upselling = new UpsellingService(); + upselling.setPages({ [SecurityPageName.network]: () => }); + const { result, waitForNextUpdate } = renderUseAppLinks(); + const hostLinkItem = { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum' as const, + experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + }; + + mockUpselling.setPages({ + [SecurityPageName.hosts]: () => , + }); + + await act(async () => { + updateAppLinks([hostLinkItem], { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + upselling: mockUpselling, + }); + await waitForNextUpdate(); + }); + expect(result.current).toStrictEqual([]); + + // cleanup + mockUpselling.setPages({}); + }); }); describe('useLinkExists', () => { diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 016a6a6e4fa19..e519ace88336a 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -153,10 +153,14 @@ const getNormalizedLink = (id: SecurityPageName): Readonly | und const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] => appLinks.reduce((acc, { links, ...appLinkWithoutSublinks }) => { - if (!isLinkAllowed(appLinkWithoutSublinks, linksPermissions)) { + if (!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions)) { return acc; } - if (!hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities)) { + + if ( + !hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities) || + !isLinkLicenseAllowed(appLinkWithoutSublinks, linksPermissions) + ) { if (linksPermissions.upselling.isPageUpsellable(appLinkWithoutSublinks.id)) { acc.push({ ...appLinkWithoutSublinks, unauthorized: true }); } @@ -175,7 +179,21 @@ const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissi return acc; }, []); -const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksPermissions) => { +const isLinkExperimentalKeyAllowed = ( + link: LinkItem, + { experimentalFeatures }: LinksPermissions +) => { + if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { + return false; + } + + if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { + return false; + } + return true; +}; + +const isLinkLicenseAllowed = (link: LinkItem, { license }: LinksPermissions) => { const linkLicenseType = link.licenseType ?? 'basic'; if (license) { if (!license.hasAtLeast(linkLicenseType)) { @@ -184,11 +202,5 @@ const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksP } else if (linkLicenseType !== 'basic') { return false; } - if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { - return false; - } - if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { - return false; - } return true; }; diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts index 1ba754a747b07..987189ec2d722 100644 --- a/x-pack/plugins/security_solution/public/index.ts +++ b/x-pack/plugins/security_solution/public/index.ts @@ -6,6 +6,7 @@ */ import type { PluginInitializerContext } from '@kbn/core/public'; + import { Plugin } from './plugin'; import type { PluginSetup, PluginStart } from './types'; export type { TimelineModel } from './timelines/store/timeline/model'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx index cfbdcfd12c26e..041ecbf6f77d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx @@ -166,7 +166,7 @@ describe('Onboarding Component new section', () => { let render: () => ReturnType; beforeEach(() => { - mockedContext.startServices.upselling.registerSections({ + mockedContext.startServices.upselling.setSections({ endpointPolicyProtections: () =>
{'pay up!'}
, }); newPolicy = getMockNewPackage(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx index 7f204ca56d4ca..c055ed3281a09 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx @@ -66,7 +66,7 @@ describe('Endpoint Policy Settings Form', () => { describe('and when policy protections are not available', () => { beforeEach(() => { - upsellingService.registerSections({ + upsellingService.setSections({ endpointPolicyProtections: () =>
{'pay up!'}
, }); }); diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts index 85b99420907ea..d40cda3294802 100644 --- a/x-pack/plugins/security_solution/public/mocks.ts +++ b/x-pack/plugins/security_solution/public/mocks.ts @@ -13,10 +13,11 @@ import type { PluginStart, PluginSetup } from './types'; const setupMock = (): PluginSetup => ({ resolver: jest.fn(), - upselling: new UpsellingService(), setAppLinksSwitcher: jest.fn(), }); +const upselling = new UpsellingService(); + const startMock = (): PluginStart => ({ getNavLinks$: jest.fn(() => new BehaviorSubject([])), setIsSidebarEnabled: jest.fn(), @@ -25,6 +26,7 @@ const startMock = (): PluginStart => ({ () => new BehaviorSubject({ leading: [], trailing: [] }) ), setExtraRoutes: jest.fn(), + getUpselling: () => upselling, }); export const securitySolutionMock = { diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index e330e233edd23..a9a2bbe6c7640 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -90,6 +90,7 @@ export const entityAnalyticsLinks: LinkItem = { path: ENTITY_ANALYTICS_PATH, capabilities: [`${SERVER_APP_ID}.show`], isBeta: false, + licenseType: 'platinum', globalSearchKeywords: [ENTITY_ANALYTICS], }; diff --git a/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx b/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx index 31cc7d0ae289c..8e7863b46ba3d 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx @@ -10,15 +10,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { EntityAnalyticsRiskScores } from '../components/entity_analytics/risk_score'; import { RiskScoreEntity } from '../../../common/search_strategy'; import { ENTITY_ANALYTICS } from '../../app/translations'; -import { Paywall } from '../../common/components/paywall'; -import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { HeaderPage } from '../../common/components/header_page'; import { LandingPageComponent } from '../../common/components/landing_page'; -import * as i18n from './translations'; import { EntityAnalyticsHeader } from '../components/entity_analytics/header'; import { EntityAnalyticsAnomalies } from '../components/entity_analytics/anomalies'; @@ -32,26 +29,20 @@ import { useHasSecurityCapability } from '../../helper_hooks'; const EntityAnalyticsComponent = () => { const { data: riskScoreEngineStatus } = useRiskEngineStatus(); const { indicesExist, loading: isSourcererLoading, indexPattern } = useSourcererDataView(); - const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities(); - const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics'); - const isRiskScoreModuleLicenseAvailable = - isPlatinumOrTrialLicense && hasEntityAnalyticsCapability; + const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics'); return ( <> {indicesExist ? ( <> - {isPlatinumOrTrialLicense && capabilitiesFetched && ( - - - - )} + + + + - {!isPlatinumOrTrialLicense && capabilitiesFetched ? ( - - ) : isSourcererLoading ? ( + {isSourcererLoading ? ( ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/translations.ts b/x-pack/plugins/security_solution/public/overview/pages/translations.ts index 5f44e18b53cd5..93d1d328c6177 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/pages/translations.ts @@ -104,13 +104,6 @@ export const DETECTION_RESPONSE_TITLE = i18n.translate( } ); -export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate( - 'xpack.securitySolution.entityAnalytics.pageDesc', - { - defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics', - } -); - export const TECHNICAL_PREVIEW = i18n.translate( 'xpack.securitySolution.entityAnalytics.technicalPreviewLabel', { diff --git a/x-pack/plugins/security_solution/public/plugin_contract.ts b/x-pack/plugins/security_solution/public/plugin_contract.ts index 77a08cda77c9b..06d72e736b041 100644 --- a/x-pack/plugins/security_solution/public/plugin_contract.ts +++ b/x-pack/plugins/security_solution/public/plugin_contract.ts @@ -40,7 +40,6 @@ export class PluginContract { public getSetupContract(): PluginSetup { return { resolver: lazyResolver, - upselling: this.upsellingService, setAppLinksSwitcher: (appLinksSwitcher) => { this.appLinksSwitcher = appLinksSwitcher; }, @@ -57,6 +56,7 @@ export class PluginContract { this.getStartedComponent$.next(getStartedComponent); }, getBreadcrumbsNav$: () => breadcrumbsNav$, + getUpselling: () => this.upsellingService, }; } diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 8f920bb10f1b6..ce754bdc29307 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -169,7 +169,6 @@ export type StartServices = CoreStart & export interface PluginSetup { resolver: () => Promise; - upselling: UpsellingService; setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void; } @@ -179,6 +178,7 @@ export interface PluginStart { setIsSidebarEnabled: (isSidebarEnabled: boolean) => void; setGetStartedPage: (getStartedComponent: React.ComponentType) => void; getBreadcrumbsNav$: () => Observable; + getUpselling: () => UpsellingService; } export interface AppObservableLibs { diff --git a/x-pack/plugins/security_solution_ess/kibana.jsonc b/x-pack/plugins/security_solution_ess/kibana.jsonc index cf1edb3f571c5..5e7e4584dc27d 100644 --- a/x-pack/plugins/security_solution_ess/kibana.jsonc +++ b/x-pack/plugins/security_solution_ess/kibana.jsonc @@ -9,7 +9,8 @@ "browser": true, "configPath": ["xpack", "securitySolutionEss"], "requiredPlugins": [ - "securitySolution" + "securitySolution", + "licensing", ], "optionalPlugins": [ "cloudExperiments", diff --git a/x-pack/plugins/security_solution/public/common/images/entity_paywall.png b/x-pack/plugins/security_solution_ess/public/common/images/entity_paywall.png similarity index 100% rename from x-pack/plugins/security_solution/public/common/images/entity_paywall.png rename to x-pack/plugins/security_solution_ess/public/common/images/entity_paywall.png diff --git a/x-pack/plugins/security_solution_ess/public/common/services.tsx b/x-pack/plugins/security_solution_ess/public/common/services.tsx index d00fd10350a7d..106782e337cc0 100644 --- a/x-pack/plugins/security_solution_ess/public/common/services.tsx +++ b/x-pack/plugins/security_solution_ess/public/common/services.tsx @@ -11,6 +11,7 @@ import { KibanaContextProvider, useKibana as useKibanaReact, } from '@kbn/kibana-react-plugin/public'; +import { NavigationProvider } from '@kbn/security-solution-navigation'; import type { SecuritySolutionEssPluginStartDeps } from '../types'; export type Services = CoreStart & SecuritySolutionEssPluginStartDeps; @@ -18,7 +19,11 @@ export type Services = CoreStart & SecuritySolutionEssPluginStartDeps; export const KibanaServicesProvider: React.FC<{ services: Services; }> = ({ services, children }) => { - return {children}; + return ( + + {children} + + ); }; export const useKibana = () => useKibanaReact(); @@ -29,3 +34,16 @@ export const createServices = ( ): Services => { return { ...core, ...pluginsStart }; }; + +export const withServicesProvider = ( + Component: React.ComponentType, + services: Services +) => { + return function WithServicesProvider(props: T) { + return ( + + + + ); + }; +}; diff --git a/x-pack/plugins/security_solution_ess/public/get_started/index.tsx b/x-pack/plugins/security_solution_ess/public/get_started/index.tsx index f85d20afe0c07..4b512fe2b9884 100644 --- a/x-pack/plugins/security_solution_ess/public/get_started/index.tsx +++ b/x-pack/plugins/security_solution_ess/public/get_started/index.tsx @@ -4,16 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import type React from 'react'; -import { KibanaServicesProvider, type Services } from '../common/services'; +import { withServicesProvider, type Services } from '../common/services'; import { GetStarted } from './lazy'; export const getSecurityGetStartedComponent = (services: Services): React.ComponentType => - function GetStartedComponent() { - return ( - - - - ); - }; + withServicesProvider(GetStarted, services); diff --git a/x-pack/plugins/security_solution_ess/public/plugin.ts b/x-pack/plugins/security_solution_ess/public/plugin.ts index 7224a46f682e4..a0c5aa694b73c 100644 --- a/x-pack/plugins/security_solution_ess/public/plugin.ts +++ b/x-pack/plugins/security_solution_ess/public/plugin.ts @@ -9,6 +9,7 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { subscribeBreadcrumbs } from './breadcrumbs'; import { createServices } from './common/services'; import { getSecurityGetStartedComponent } from './get_started'; +import { registerUpsellings } from './upselling/register_upsellings'; import type { SecuritySolutionEssPluginSetup, SecuritySolutionEssPluginStart, @@ -36,9 +37,13 @@ export class SecuritySolutionEssPlugin core: CoreStart, startDeps: SecuritySolutionEssPluginStartDeps ): SecuritySolutionEssPluginStart { - const { securitySolution } = startDeps; + const { securitySolution, licensing } = startDeps; const services = createServices(core, startDeps); + licensing.license$.subscribe((license) => { + registerUpsellings(securitySolution.getUpselling(), license, services); + }); + securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services)); subscribeBreadcrumbs(services); diff --git a/x-pack/plugins/security_solution_ess/public/types.ts b/x-pack/plugins/security_solution_ess/public/types.ts index 2bdeeffebf723..effa3750d3646 100644 --- a/x-pack/plugins/security_solution_ess/public/types.ts +++ b/x-pack/plugins/security_solution_ess/public/types.ts @@ -10,6 +10,7 @@ import type { PluginStart as SecuritySolutionPluginStart, } from '@kbn/security-solution-plugin/public'; import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SecuritySolutionEssPluginSetup {} @@ -24,4 +25,5 @@ export interface SecuritySolutionEssPluginSetupDeps { export interface SecuritySolutionEssPluginStartDeps { securitySolution: SecuritySolutionPluginStart; cloudExperiments?: CloudExperimentsPluginStart; + licensing: LicensingPluginStart; } diff --git a/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx b/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx new file mode 100644 index 0000000000000..8dd16883f5088 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) => + i18n.translate('xpack.securitySolutionEss.markdown.insight.upsell', { + defaultMessage: 'Upgrade to {requiredLicense} to make use of insights in investigation guides', + values: { + requiredLicense, + }, + }); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx new file mode 100644 index 0000000000000..c7c4cd915fc03 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { + EuiCard, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiTextColor, + EuiImage, + EuiPageHeader, + EuiSpacer, +} from '@elastic/eui'; + +import styled from '@emotion/styled'; +import { useNavigation } from '@kbn/security-solution-navigation'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import * as i18n from './translations'; +import paywallPng from '../../common/images/entity_paywall.png'; + +const PaywallDiv = styled.div` + max-width: 75%; + margin: 0 auto; + .euiCard__betaBadgeWrapper { + .euiCard__betaBadge { + width: auto; + } + } + .platinumCardDescription { + padding: 0 15%; + } +`; +const StyledEuiCard = styled(EuiCard)` + span.euiTitle { + max-width: 540px; + display: block; + margin: 0 auto; + } +`; + +const EntityAnalyticsUpsellingComponent = () => { + const { getAppUrl, navigateTo } = useNavigation(); + const subscriptionUrl = getAppUrl({ + appId: 'management', + path: 'stack/license_management', + }); + const goToSubscription = useCallback(() => { + navigateTo({ url: subscriptionUrl }); + }, [navigateTo, subscriptionUrl]); + return ( + + + + + + } + display="subdued" + title={ +

+ {i18n.ENTITY_ANALYTICS_LICENSE_DESC} +

+ } + description={false} + paddingSize="xl" + > + + + +

+ {i18n.UPGRADE_MESSAGE} +

+
+ +
+ + {i18n.UPGRADE_BUTTON} + +
+
+
+
+
+ + + + + +
+
+
+ ); +}; + +EntityAnalyticsUpsellingComponent.displayName = 'EntityAnalyticsUpsellingComponent'; + +// eslint-disable-next-line import/no-default-export +export default React.memo(EntityAnalyticsUpsellingComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/paywall/translations.ts b/x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts similarity index 51% rename from x-pack/plugins/security_solution/public/common/components/paywall/translations.ts rename to x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts index a78fda1e90fa7..5ec6b838fbd46 100644 --- a/x-pack/plugins/security_solution/public/common/components/paywall/translations.ts +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts @@ -7,14 +7,28 @@ import { i18n } from '@kbn/i18n'; -export const PLATINUM = i18n.translate('xpack.securitySolution.paywall.platinum', { +export const PLATINUM = i18n.translate('xpack.securitySolutionEss.paywall.platinum', { defaultMessage: 'Platinum', }); -export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolution.paywall.upgradeMessage', { +export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolutionEss.paywall.upgradeMessage', { defaultMessage: 'This feature is available with Platinum or higher subscription', }); -export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolution.paywall.upgradeButton', { +export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolutionEss.paywall.upgradeButton', { defaultMessage: 'Upgrade to Platinum', }); + +export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate( + 'xpack.securitySolutionEss.entityAnalytics.pageDesc', + { + defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics', + } +); + +export const ENTITY_ANALYTICS_TITLE = i18n.translate( + 'xpack.securitySolutionEss.navigation.entityAnalytics', + { + defaultMessage: 'Entity Analytics', + } +); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx new file mode 100644 index 0000000000000..f750507d17bc8 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -0,0 +1,102 @@ +/* + * 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 { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import type { UpsellingService } from '@kbn/security-solution-plugin/public'; +import type { + MessageUpsellings, + PageUpsellings, + SectionUpsellings, + UpsellingMessageId, + UpsellingSectionId, +} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types'; +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; +import { lazy } from 'react'; +import type React from 'react'; +import { UPGRADE_INVESTIGATION_GUIDE } from './messages/investigation_guide_upselling'; +import type { Services } from '../common/services'; +import { withServicesProvider } from '../common/services'; +const EntityAnalyticsUpsellingLazy = lazy(() => import('./pages/entity_analytics_upselling')); + +interface UpsellingsConfig { + minimumLicenseRequired: LicenseType; + component: React.ComponentType; +} + +interface UpsellingsMessageConfig { + minimumLicenseRequired: LicenseType; + message: string; + id: UpsellingMessageId; +} + +type UpsellingPages = Array; +type UpsellingSections = Array; +type UpsellingMessages = UpsellingsMessageConfig[]; + +export const registerUpsellings = ( + upselling: UpsellingService, + license: ILicense, + services: Services +) => { + const upsellingPagesToRegister = upsellingPages.reduce( + (pageUpsellings, { pageName, minimumLicenseRequired, component }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + pageUpsellings[pageName] = withServicesProvider(component, services); + } + return pageUpsellings; + }, + {} + ); + + const upsellingSectionsToRegister = upsellingSections.reduce( + (sectionUpsellings, { id, minimumLicenseRequired, component }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + sectionUpsellings[id] = component; + } + return sectionUpsellings; + }, + {} + ); + + const upsellingMessagesToRegister = upsellingMessages.reduce( + (messagesUpsellings, { id, minimumLicenseRequired, message }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + messagesUpsellings[id] = message; + } + return messagesUpsellings; + }, + {} + ); + + upselling.setPages(upsellingPagesToRegister); + upselling.setSections(upsellingSectionsToRegister); + upselling.setMessages(upsellingMessagesToRegister); +}; + +// Upsellings for entire pages, linked to a SecurityPageName +export const upsellingPages: UpsellingPages = [ + // It is highly advisable to make use of lazy loaded components to minimize bundle size. + { + pageName: SecurityPageName.entityAnalytics, + minimumLicenseRequired: 'platinum', + component: EntityAnalyticsUpsellingLazy, + }, +]; + +// Upsellings for sections, linked by arbitrary ids +export const upsellingSections: UpsellingSections = [ + // It is highly advisable to make use of lazy loaded components to minimize bundle size. +]; + +// Upsellings for sections, linked by arbitrary ids +export const upsellingMessages: UpsellingMessages = [ + { + id: 'investigation_guide', + minimumLicenseRequired: 'platinum', + message: UPGRADE_INVESTIGATION_GUIDE('platinum'), + }, +]; diff --git a/x-pack/plugins/security_solution_ess/tsconfig.json b/x-pack/plugins/security_solution_ess/tsconfig.json index c37fc77790254..08c7e49b6a166 100644 --- a/x-pack/plugins/security_solution_ess/tsconfig.json +++ b/x-pack/plugins/security_solution_ess/tsconfig.json @@ -19,5 +19,8 @@ "@kbn/i18n", "@kbn/cloud-experiments-plugin", "@kbn/kibana-react-plugin", + "@kbn/security-solution-navigation", + "@kbn/licensing-plugin", + "@kbn/shared-ux-page-kibana-template", ] } diff --git a/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx b/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx index 0a1d1e7693912..6468ebd602543 100644 --- a/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx +++ b/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx @@ -9,8 +9,6 @@ import { EuiTitle, useEuiTheme, useEuiShadow } from '@elastic/eui'; import React from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { css } from '@emotion/react'; - -import { NavigationProvider } from '@kbn/security-solution-navigation'; import { WelcomePanel } from './welcome_panel'; import { TogglePanel } from './toggle_panel'; import { @@ -114,17 +112,15 @@ export const GetStartedComponent: React.FC = ({ productTypes }) padding: 0 ${euiTheme.base * 2.25}px; `} > - - - + ); diff --git a/x-pack/plugins/security_solution_serverless/public/plugin.ts b/x-pack/plugins/security_solution_serverless/public/plugin.ts index f16e7a97c617c..946e84c895f74 100644 --- a/x-pack/plugins/security_solution_serverless/public/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/public/plugin.ts @@ -40,7 +40,6 @@ export class SecuritySolutionServerlessPlugin _core: CoreSetup, setupDeps: SecuritySolutionServerlessPluginSetupDeps ): SecuritySolutionServerlessPluginSetup { - registerUpsellings(setupDeps.securitySolution.upselling, this.config.productTypes); setupDeps.securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher); return {}; @@ -55,6 +54,7 @@ export class SecuritySolutionServerlessPlugin const services = createServices(core, startDeps); + registerUpsellings(securitySolution.getUpselling(), this.config.productTypes); securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes)); configureNavigation(services, this.config); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx new file mode 100644 index 0000000000000..a3640195544e0 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx @@ -0,0 +1,28 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +const withSuspenseUpsell = ( + Component: React.ComponentType +): React.FC => + function WithSuspenseUpsell(props) { + return ( + }> + + + ); + }; + +export const ThreatIntelligencePaywallLazy = withSuspenseUpsell( + lazy(() => import('./pages/threat_intelligence_paywall')) +); + +export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell( + lazy(() => import('./pages/osquery_automated_response_actions')) +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx similarity index 90% rename from x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx rename to x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx index 761a1426f1a07..591e979fbfbbe 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx @@ -22,6 +22,3 @@ export const investigationGuideUpselling = (requiredPLI: AppFeatureKey): string const productTypeRequired = getProductTypeByPLI(requiredPLI); return productTypeRequired ? UPGRADE_INVESTIGATION_GUIDE(productTypeRequired) : ''; }; - -// eslint-disable-next-line import/no-default-export -export { investigationGuideUpselling as default }; diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx index 8ba6fc4a65981..6c886c681d0aa 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx @@ -31,58 +31,58 @@ describe('registerUpsellings', () => { it('should not register anything when all PLIs features are enabled', () => { mockGetProductAppFeatures.mockReturnValue(ALL_APP_FEATURE_KEYS); - const registerPages = jest.fn(); - const registerSections = jest.fn(); - const registerMessages = jest.fn(); + const setPages = jest.fn(); + const setSections = jest.fn(); + const setMessages = jest.fn(); const upselling = { - registerPages, - registerSections, - registerMessages, + setPages, + setSections, + setMessages, } as unknown as UpsellingService; registerUpsellings(upselling, allProductTypes); - expect(registerPages).toHaveBeenCalledTimes(1); - expect(registerPages).toHaveBeenCalledWith({}); + expect(setPages).toHaveBeenCalledTimes(1); + expect(setPages).toHaveBeenCalledWith({}); - expect(registerSections).toHaveBeenCalledTimes(1); - expect(registerSections).toHaveBeenCalledWith({}); + expect(setSections).toHaveBeenCalledTimes(1); + expect(setSections).toHaveBeenCalledWith({}); - expect(registerMessages).toHaveBeenCalledTimes(1); - expect(registerMessages).toHaveBeenCalledWith({}); + expect(setMessages).toHaveBeenCalledTimes(1); + expect(setMessages).toHaveBeenCalledWith({}); }); it('should register all upsellings pages, sections and messages when PLIs features are disabled', () => { mockGetProductAppFeatures.mockReturnValue([]); - const registerPages = jest.fn(); - const registerSections = jest.fn(); - const registerMessages = jest.fn(); + const setPages = jest.fn(); + const setSections = jest.fn(); + const setMessages = jest.fn(); const upselling = { - registerPages, - registerSections, - registerMessages, + setPages, + setSections, + setMessages, } as unknown as UpsellingService; registerUpsellings(upselling, allProductTypes); const expectedPagesObject = Object.fromEntries( - upsellingPages.map(({ pageName }) => [pageName, expect.any(Object)]) + upsellingPages.map(({ pageName }) => [pageName, expect.anything()]) ); - expect(registerPages).toHaveBeenCalledTimes(1); - expect(registerPages).toHaveBeenCalledWith(expectedPagesObject); + expect(setPages).toHaveBeenCalledTimes(1); + expect(setPages).toHaveBeenCalledWith(expectedPagesObject); const expectedSectionsObject = Object.fromEntries( - upsellingSections.map(({ id }) => [id, expect.any(Object)]) + upsellingSections.map(({ id }) => [id, expect.anything()]) ); - expect(registerSections).toHaveBeenCalledTimes(1); - expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject); + expect(setSections).toHaveBeenCalledTimes(1); + expect(setSections).toHaveBeenCalledWith(expectedSectionsObject); const expectedMessagesObject = Object.fromEntries( upsellingMessages.map(({ id }) => [id, expect.any(String)]) ); - expect(registerMessages).toHaveBeenCalledTimes(1); - expect(registerMessages).toHaveBeenCalledWith(expectedMessagesObject); + expect(setMessages).toHaveBeenCalledTimes(1); + expect(setMessages).toHaveBeenCalledWith(expectedMessagesObject); }); }); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index 2997a39b544e1..01290dad9f00b 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -15,37 +15,19 @@ import type { MessageUpsellings, UpsellingMessageId, } from '@kbn/security-solution-plugin/public/common/lib/upsellings/types'; -import React, { lazy } from 'react'; +import React from 'react'; import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management'; import type { SecurityProductTypes } from '../../common/config'; import { getProductAppFeatures } from '../../common/pli/pli_features'; -import investigationGuideUpselling from './pages/investigation_guide_upselling'; - -const ThreatIntelligencePaywallLazy = lazy(async () => { - const ThreatIntelligencePaywall = (await import('./pages/threat_intelligence_paywall')).default; - - return { - default: () => , - }; -}); - -const OsqueryResponseActionsUpsellingSectionlLazy = lazy(async () => { - const OsqueryResponseActionsUpsellingSection = ( - await import('./pages/osquery_automated_response_actions') - ).default; - - return { - default: () => ( - - ), - }; -}); +import { investigationGuideUpselling } from './messages/investigation_guide_upselling'; +import { + OsqueryResponseActionsUpsellingSectionLazy, + ThreatIntelligencePaywallLazy, +} from './lazy_upselling'; interface UpsellingsConfig { pli: AppFeatureKey; - component: React.LazyExoticComponent; + component: React.ComponentType; } interface UpsellingsMessageConfig { @@ -94,42 +76,35 @@ export const registerUpsellings = ( {} ); - upselling.registerPages(upsellingPagesToRegister); - upselling.registerSections(upsellingSectionsToRegister); - upselling.registerMessages(upsellingMessagesToRegister); + upselling.setPages(upsellingPagesToRegister); + upselling.setSections(upsellingSectionsToRegister); + upselling.setMessages(upsellingMessagesToRegister); }; // Upsellings for entire pages, linked to a SecurityPageName export const upsellingPages: UpsellingPages = [ - // Sample code for registering a Upselling page - // Make sure the component is lazy loaded `const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));` - // { - // pageName: SecurityPageName.entityAnalytics, - // pli: AppFeatureKey.advancedInsights, - // component: () => , - // }, + // It is highly advisable to make use of lazy loaded components to minimize bundle size. { pageName: SecurityPageName.threatIntelligence, pli: AppFeatureKey.threatIntelligence, - component: ThreatIntelligencePaywallLazy, + component: () => ( + + ), }, ]; // Upsellings for sections, linked by arbitrary ids export const upsellingSections: UpsellingSections = [ - // Sample code for registering a Upselling section - // Make sure the component is lazy loaded `const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));` - // { - // id: 'entity_analytics_panel', - // pli: AppFeatureKey.advancedInsights, - // component: () => , - // }, + // It is highly advisable to make use of lazy loaded components to minimize bundle size. { id: 'osquery_automated_response_actions', pli: AppFeatureKey.osqueryAutomatedResponseActions, - component: OsqueryResponseActionsUpsellingSectionlLazy, + component: () => ( + + ), }, - { id: 'endpointPolicyProtections', pli: AppFeatureKey.endpointPolicyProtections, diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/generic_upselling_section.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/generic_upselling_section.tsx similarity index 100% rename from x-pack/plugins/security_solution_serverless/public/upselling/pages/generic_upselling_section.tsx rename to x-pack/plugins/security_solution_serverless/public/upselling/sections/generic_upselling_section.tsx diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 903e32ecfc72a..e76f335fcc301 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -32976,7 +32976,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "Utilisateurs critiques", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "Le tableau des risques de l'hôte n'est pas affecté par la plage temporelle. Ce tableau montre le dernier score de risque enregistré pour chaque hôte.", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "Scores de risque de l'hôte", - "xpack.securitySolution.entityAnalytics.pageDesc": "Détecter les menaces des utilisateurs et des hôtes de votre réseau avec l'Analyse des entités", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "Le panneau de Score de risque de l'hôte affiche la liste des hôtes à risque ainsi que leur dernier score de risque. Vous pouvez filtrer cette liste à l’aide de filtres globaux dans la barre de recherche KQL. Le filtre de sélecteur de plage temporelle affiche les alertes dans l’intervalle de temps sélectionné uniquement et ne filtre pas la liste des hôtes à risque.", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "En savoir plus", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "En version d'évaluation technique", @@ -33664,7 +33663,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "Plage temporelle relative", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "Sélectionnez une plage horaire pour limiter la requête, par rapport à l'heure de création de l'alerte (facultatif).", "xpack.securitySolution.markdown.insight.title": "Examiner", - "xpack.securitySolution.markdown.insight.upsell": "Mettez à niveau vers Platinum pour pouvoir utiliser les informations exploitables dans des guides d’investigation", "xpack.securitySolution.markdown.invalid": "Markdown non valide détecté", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "Ajouter une recherche", "xpack.securitySolution.markdown.osquery.addModalTitle": "Ajouter une recherche", @@ -33948,9 +33946,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "Affichant", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "Affiner votre recherche pour mieux filtrer les résultats", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - trop de résultats", - "xpack.securitySolution.paywall.platinum": "Platinum", - "xpack.securitySolution.paywall.upgradeButton": "Mettre à niveau vers Platinum", - "xpack.securitySolution.paywall.upgradeMessage": "Cette fonctionnalité est disponible avec l'abonnement Platinum ou supérieur", "xpack.securitySolution.policiesTab": "Politiques", "xpack.securitySolution.policy.backToPolicyList": "Retour à la liste des politiques", "xpack.securitySolution.policy.list.createdAt": "Date de création", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f032bb7d6df05..74c6db4b1e59c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32975,7 +32975,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "重要なユーザー", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "ホストリスク表は時間範囲の影響を受けません。この表は、各ホストの最後に記録されたリスクスコアを示します。", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "ホストリスクスコア", - "xpack.securitySolution.entityAnalytics.pageDesc": "Entity Analyticsを使用して、ネットワーク内のユーザーとホストから脅威を検出", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "ホストリスクスコアパネルには、リスクのあるホストの一覧と最新のリスクスコアが表示されます。KQL検索バーのグローバルフィルターを使って、この一覧をフィルタリングできます。時間範囲ピッカーフィルターは、選択した時間範囲内のアラートのみを表示し、リスクのあるホストの一覧をフィルタリングしません。", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "詳細", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "テクニカルプレビュー", @@ -33663,7 +33662,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "相対的時間範囲", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "アラートの作成日時に相対的な、クエリを限定するための時間範囲を選択します(任意)。", "xpack.securitySolution.markdown.insight.title": "調査", - "xpack.securitySolution.markdown.insight.upsell": "プラチナにアップグレードして、調査ガイドのインサイトを利用", "xpack.securitySolution.markdown.invalid": "無効なマークダウンが検出されました", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "クエリを追加", "xpack.securitySolution.markdown.osquery.addModalTitle": "クエリを追加", @@ -33947,9 +33945,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "表示中", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます", - "xpack.securitySolution.paywall.platinum": "プラチナ", - "xpack.securitySolution.paywall.upgradeButton": "プラチナにアップグレード", - "xpack.securitySolution.paywall.upgradeMessage": "この機能は、プラチナ以上のサブスクリプションでご利用いただけます", "xpack.securitySolution.policiesTab": "ポリシー", "xpack.securitySolution.policy.backToPolicyList": "ポリシーリストに戻る", "xpack.securitySolution.policy.list.createdAt": "作成日", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 93d5b6a0e5d53..b64a7850e0191 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -32971,7 +32971,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "关键用户", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "主机风险表不受时间范围影响。本表显示每台主机最新记录的风险分数。", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "主机风险分数", - "xpack.securitySolution.entityAnalytics.pageDesc": "通过实体分析检测来自您网络中用户和主机的威胁", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "“主机风险分数”面板显示有风险主机及其最新风险分数的列表。可以在 KQL 搜索栏中使用全局筛选来筛选此列表。时间范围选取器筛选将仅显示选定时间范围内的告警,并且不筛选有风险主机列表。", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "了解详情", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "处于技术预览状态", @@ -33659,7 +33658,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "相对时间范围", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "选择相对于告警创建时间的时间范围(可选)以限制查询。", "xpack.securitySolution.markdown.insight.title": "调查", - "xpack.securitySolution.markdown.insight.upsell": "升级到白金级以利用调查指南中的洞见", "xpack.securitySolution.markdown.invalid": "检测到无效 Markdown", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "添加查询", "xpack.securitySolution.markdown.osquery.addModalTitle": "添加查询", @@ -33943,9 +33941,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多", - "xpack.securitySolution.paywall.platinum": "白金级", - "xpack.securitySolution.paywall.upgradeButton": "升级到白金级", - "xpack.securitySolution.paywall.upgradeMessage": "白金级或更高级订阅可以使用此功能", "xpack.securitySolution.policiesTab": "策略", "xpack.securitySolution.policy.backToPolicyList": "返回到策略列表", "xpack.securitySolution.policy.list.createdAt": "创建日期",