mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solutions] Add PLI authorisation for Investigation Guide (#162704)
## Summary
Add PLI authorization checks and Upselling message to Investigation.
*This PR restricts access to the features* and creates an updated
upselling hover message.
* It updates the Upselling registering to accept string because the
Markdown component doesn't accept components as hover messages.
### How to test it?
* Open timeline
* Go to the notes tab
* You should find an investigation guide button on the markdown editor
toolbar
#### ESS `yarn start`
* Run ESS with a basic license
* It should not change
* Run ESS with a platinum
* It should not change
#### Serverless `yarn serverless-security`
* Run Serverless with security essentials (serverless.security.yml)
* It should show the Upselling message
```
xpack.serverless.security.productTypes:
[
{ product_line: 'security', product_tier: 'essentials' }
]
```
* Run Serverless with security complete
(kibana/config/serverless.security.yml)
* It should show the Investigation guide button
```
xpack.serverless.security.productTypes:
[
{ product_line: 'security', product_tier: 'complete' },
]
```
<img width="1761" alt="Screenshot 2023-07-31 at 09 42 41" src="1a1e0313
-7335-4a20-84e3-ec2d48f80b9c">
### Checklist
Delete any items that are not applicable to this PR.
- [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/packages/kbn-i18n/README.md)
- [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
This commit is contained in:
parent
ed7d6cf463
commit
b6d94d7883
22 changed files with 250 additions and 22 deletions
|
@ -11,6 +11,11 @@ export enum AppFeatureSecurityKey {
|
|||
*/
|
||||
advancedInsights = 'advanced_insights',
|
||||
|
||||
/**
|
||||
* Enables Investigation guide in Timeline
|
||||
*/
|
||||
investigationGuide = 'investigation_guide',
|
||||
|
||||
/**
|
||||
* Enables access to the Endpoint List and associated views that allows management of hosts
|
||||
* running endpoint security
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { ContextShape } from '@elastic/eui/src/components/markdown_editor/m
|
|||
import { useLicense } from '../../hooks/use_license';
|
||||
|
||||
import { uiPlugins, parsingPlugins, processingPlugins } from './plugins';
|
||||
import { useUpsellingMessage } from '../../hooks/use_upselling';
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
onChange: (content: string) => void;
|
||||
|
@ -73,9 +74,10 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
|
|||
|
||||
const licenseIsPlatinum = useLicense().isPlatinumPlus();
|
||||
|
||||
const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
|
||||
const uiPluginsWithState = useMemo(() => {
|
||||
return uiPlugins({ licenseIsPlatinum });
|
||||
}, [licenseIsPlatinum]);
|
||||
return uiPlugins({ licenseIsPlatinum, insightsUpsellingMessage });
|
||||
}, [licenseIsPlatinum, insightsUpsellingMessage]);
|
||||
|
||||
// @ts-expect-error update types
|
||||
useImperativeHandle(ref, () => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
getDefaultEuiMarkdownProcessingPlugins,
|
||||
getDefaultEuiMarkdownUiPlugins,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as timelineMarkdownPlugin from './timeline';
|
||||
import * as osqueryMarkdownPlugin from './osquery';
|
||||
import * as insightMarkdownPlugin from './insight';
|
||||
|
@ -27,14 +26,30 @@ export const {
|
|||
|
||||
export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix];
|
||||
|
||||
export const uiPlugins = ({ licenseIsPlatinum }: { licenseIsPlatinum: boolean }) => {
|
||||
export const uiPlugins = ({
|
||||
licenseIsPlatinum,
|
||||
insightsUpsellingMessage,
|
||||
}: {
|
||||
licenseIsPlatinum: boolean;
|
||||
insightsUpsellingMessage: string | null;
|
||||
}) => {
|
||||
const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name);
|
||||
const insightPluginWithLicense = insightMarkdownPlugin.plugin({ licenseIsPlatinum });
|
||||
const insightPluginWithLicense = insightMarkdownPlugin.plugin({
|
||||
licenseIsPlatinum,
|
||||
insightsUpsellingMessage,
|
||||
});
|
||||
if (currentPlugins.includes(insightPluginWithLicense.name) === false) {
|
||||
nonStatefulUiPlugins.push(timelineMarkdownPlugin.plugin);
|
||||
nonStatefulUiPlugins.push(osqueryMarkdownPlugin.plugin);
|
||||
nonStatefulUiPlugins.push(insightPluginWithLicense);
|
||||
} else {
|
||||
// When called for the second time we need to update insightMarkdownPlugin
|
||||
const index = nonStatefulUiPlugins.findIndex(
|
||||
(plugin) => plugin.name === insightPluginWithLicense.name
|
||||
);
|
||||
nonStatefulUiPlugins[index] = insightPluginWithLicense;
|
||||
}
|
||||
|
||||
return nonStatefulUiPlugins;
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { KibanaServices } from '../../../../lib/kibana';
|
||||
import { licenseService } from '../../../../hooks/use_license';
|
||||
import type { DefaultTimeRangeSetting } from '../../../../utils/default_date_settings';
|
||||
import { renderer as Renderer } from '.';
|
||||
import { plugin, renderer as Renderer } from '.';
|
||||
import type { InvestigateInTimelineButtonProps } from '../../../event_details/table/investigate_in_timeline_button';
|
||||
|
||||
jest.mock('../../../../lib/kibana');
|
||||
|
@ -130,3 +130,39 @@ describe('insight component renderer', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin', () => {
|
||||
it('renders insightsUpsellingMessage when provided', () => {
|
||||
const insightsUpsellingMessage = 'test message';
|
||||
const result = plugin({ licenseIsPlatinum: false, 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 });
|
||||
|
||||
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 });
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -541,13 +541,21 @@ const exampleInsight = `${insightPrefix}{
|
|||
]
|
||||
}}`;
|
||||
|
||||
export const plugin = ({ licenseIsPlatinum }: { licenseIsPlatinum: boolean }) => {
|
||||
export const plugin = ({
|
||||
licenseIsPlatinum,
|
||||
insightsUpsellingMessage,
|
||||
}: {
|
||||
licenseIsPlatinum: boolean;
|
||||
insightsUpsellingMessage: string | null;
|
||||
}) => {
|
||||
const label = licenseIsPlatinum ? i18n.INVESTIGATE : i18n.INSIGHT_UPSELL;
|
||||
|
||||
return {
|
||||
name: 'insights',
|
||||
button: {
|
||||
label: licenseIsPlatinum ? i18n.INVESTIGATE : i18n.INIGHT_UPSELL,
|
||||
label: insightsUpsellingMessage ?? label,
|
||||
iconType: 'timelineWithArrow',
|
||||
isDisabled: !licenseIsPlatinum,
|
||||
isDisabled: !licenseIsPlatinum || !!insightsUpsellingMessage,
|
||||
},
|
||||
helpText: (
|
||||
<div>
|
||||
|
|
|
@ -11,7 +11,7 @@ export const LABEL = i18n.translate('xpack.securitySolution.markdown.insight.lab
|
|||
defaultMessage: 'Label',
|
||||
});
|
||||
|
||||
export const INIGHT_UPSELL = i18n.translate('xpack.securitySolution.markdown.insight.upsell', {
|
||||
export const INSIGHT_UPSELL = i18n.translate('xpack.securitySolution.markdown.insight.upsell', {
|
||||
defaultMessage: 'Upgrade to platinum to make use of insights in investigation guides',
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { renderHook } from '@testing-library/react-hooks';
|
|||
import React from 'react';
|
||||
import { SecurityPageName } from '../../../common';
|
||||
import { UpsellingService } from '../lib/upsellings';
|
||||
import { useUpsellingComponent, useUpsellingPage } from './use_upselling';
|
||||
import { useUpsellingComponent, useUpsellingMessage, useUpsellingPage } from './use_upselling';
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
|
||||
|
@ -47,4 +47,24 @@ describe('use_upselling', () => {
|
|||
const { result } = renderHook(() => useUpsellingPage(SecurityPageName.hosts));
|
||||
expect(result.current).toBe(TestComponent);
|
||||
});
|
||||
|
||||
test('useUpsellingMessage returns pages', () => {
|
||||
const testMessage = 'test message';
|
||||
mockUpselling.registerMessages({
|
||||
investigation_guide: testMessage,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpsellingMessage('investigation_guide'));
|
||||
expect(result.current).toBe(testMessage);
|
||||
});
|
||||
|
||||
test('useUpsellingMessage returns null when upsellingMessageId not found', () => {
|
||||
const emptyMessages = {};
|
||||
mockUpselling.registerMessages(emptyMessages);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useUpsellingMessage('my_fake_message_id' as 'investigation_guide')
|
||||
);
|
||||
expect(result.current).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import useObservable from 'react-use/lib/useObservable';
|
|||
import type { UpsellingSectionId } from '../lib/upsellings';
|
||||
import { useKibana } from '../lib/kibana';
|
||||
import type { SecurityPageName } from '../../../common';
|
||||
import type { UpsellingMessageId } from '../lib/upsellings/types';
|
||||
|
||||
export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentType | null => {
|
||||
const { upselling } = useKibana().services;
|
||||
|
@ -18,6 +19,13 @@ export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentTy
|
|||
return useMemo(() => upsellingSections?.get(id) ?? null, [id, upsellingSections]);
|
||||
};
|
||||
|
||||
export const useUpsellingMessage = (id: UpsellingMessageId): string | null => {
|
||||
const { upselling } = useKibana().services;
|
||||
const upsellingMessages = useObservable(upselling.messages$);
|
||||
|
||||
return useMemo(() => upsellingMessages?.get(id) ?? null, [id, upsellingMessages]);
|
||||
};
|
||||
|
||||
export const useUpsellingPage = (pageName: SecurityPageName): React.ComponentType | null => {
|
||||
const { upselling } = useKibana().services;
|
||||
const UpsellingPage = useMemo(() => upselling.getPageUpselling(pageName), [pageName, upselling]);
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
import type { SecurityPageName } from '../../../../common';
|
||||
|
||||
export type PageUpsellings = Partial<Record<SecurityPageName, React.ComponentType>>;
|
||||
export type MessageUpsellings = Partial<Record<UpsellingMessageId, string>>;
|
||||
export type SectionUpsellings = Partial<Record<UpsellingSectionId, React.ComponentType>>;
|
||||
|
||||
export type UpsellingSectionId = 'entity_analytics_panel';
|
||||
|
||||
export type UpsellingMessageId = 'investigation_guide';
|
||||
|
|
|
@ -34,6 +34,18 @@ describe('UpsellingService', () => {
|
|||
expect(value.get(SecurityPageName.hosts)).toEqual(TestComponent);
|
||||
});
|
||||
|
||||
it('registers messages', async () => {
|
||||
const testMessage = 'test message';
|
||||
const service = new UpsellingService();
|
||||
service.registerMessages({
|
||||
investigation_guide: testMessage,
|
||||
});
|
||||
|
||||
const value = await firstValueFrom(service.messages$);
|
||||
|
||||
expect(value.get('investigation_guide')).toEqual(testMessage);
|
||||
});
|
||||
|
||||
it('"isPageUpsellable" returns true when page is upsellable', () => {
|
||||
const service = new UpsellingService();
|
||||
service.registerPages({
|
||||
|
|
|
@ -8,24 +8,39 @@
|
|||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { SecurityPageName } from '../../../../common';
|
||||
import type { SectionUpsellings, PageUpsellings, UpsellingSectionId } from './types';
|
||||
import type {
|
||||
SectionUpsellings,
|
||||
PageUpsellings,
|
||||
UpsellingSectionId,
|
||||
UpsellingMessageId,
|
||||
MessageUpsellings,
|
||||
} from './types';
|
||||
|
||||
export class UpsellingService {
|
||||
private sections: Map<UpsellingSectionId, React.ComponentType>;
|
||||
private pages: Map<SecurityPageName, React.ComponentType>;
|
||||
private messages: Map<UpsellingMessageId, string>;
|
||||
|
||||
private messagesSubject$: BehaviorSubject<Map<UpsellingMessageId, string>>;
|
||||
private sectionsSubject$: BehaviorSubject<Map<UpsellingSectionId, React.ComponentType>>;
|
||||
private pagesSubject$: BehaviorSubject<Map<SecurityPageName, React.ComponentType>>;
|
||||
|
||||
public sections$: Observable<Map<UpsellingSectionId, React.ComponentType>>;
|
||||
public pages$: Observable<Map<SecurityPageName, React.ComponentType>>;
|
||||
public messages$: Observable<Map<UpsellingMessageId, string>>;
|
||||
|
||||
constructor() {
|
||||
this.sections = new Map();
|
||||
this.sectionsSubject$ = new BehaviorSubject(new Map());
|
||||
this.sections$ = this.sectionsSubject$.asObservable();
|
||||
|
||||
this.pages = new Map();
|
||||
this.pagesSubject$ = new BehaviorSubject(new Map());
|
||||
this.pages$ = this.pagesSubject$.asObservable();
|
||||
|
||||
this.messages = new Map();
|
||||
this.messagesSubject$ = new BehaviorSubject(new Map());
|
||||
this.messages$ = this.messagesSubject$.asObservable();
|
||||
}
|
||||
|
||||
registerSections(sections: SectionUpsellings) {
|
||||
|
@ -42,6 +57,13 @@ export class UpsellingService {
|
|||
this.pagesSubject$.next(this.pages);
|
||||
}
|
||||
|
||||
registerMessages(messages: MessageUpsellings) {
|
||||
Object.entries(messages).forEach(([messageId, component]) => {
|
||||
this.messages.set(messageId as UpsellingMessageId, component);
|
||||
});
|
||||
this.messagesSubject$.next(this.messages);
|
||||
}
|
||||
|
||||
isPageUpsellable(id: SecurityPageName) {
|
||||
return this.pages.has(id);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ import React from 'react';
|
|||
|
||||
import { NewNote } from './new_note';
|
||||
|
||||
jest.mock('../../../../common/hooks/use_upselling', () => ({
|
||||
useUpsellingMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('NewNote', () => {
|
||||
const note = 'The contents of a new note';
|
||||
|
||||
|
|
|
@ -44,6 +44,10 @@ jest.mock(
|
|||
'../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'
|
||||
);
|
||||
|
||||
jest.mock('../../../../common/hooks/use_upselling', () => ({
|
||||
useUpsellingMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/components/user_privileges', () => {
|
||||
return {
|
||||
useUserPrivileges: () => ({
|
||||
|
|
|
@ -151,6 +151,16 @@ export const getSecurityAppFeaturesConfig = (
|
|||
},
|
||||
},
|
||||
},
|
||||
[AppFeatureSecurityKey.investigationGuide]: {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['investigation-guide'],
|
||||
},
|
||||
read: {
|
||||
ui: ['investigation-guide'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[AppFeatureSecurityKey.threatIntelligence]: {
|
||||
privileges: {
|
||||
|
|
|
@ -17,6 +17,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
|
|||
essentials: [],
|
||||
complete: [
|
||||
AppFeatureKey.advancedInsights,
|
||||
AppFeatureKey.investigationGuide,
|
||||
AppFeatureKey.threatIntelligence,
|
||||
AppFeatureKey.casesConnectors,
|
||||
],
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { PLI_APP_FEATURES } from '../../../common/pli/pli_config';
|
||||
|
||||
export const useProductTypeByPLI = (requiredPLI: AppFeatureKey): string | null => {
|
||||
export const getProductTypeByPLI = (requiredPLI: AppFeatureKey): string | null => {
|
||||
if (PLI_APP_FEATURES.security.essentials.includes(requiredPLI)) {
|
||||
return 'Security Essentials';
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { useProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
export const GenericUpsellingPage: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
|
||||
function GenericUpsellingPage({ requiredPLI }) {
|
||||
const productTypeRequired = useProductTypeByPLI(requiredPLI);
|
||||
const productTypeRequired = getProductTypeByPLI(requiredPLI);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { useProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
export const GenericUpsellingSection: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
|
||||
function GenericUpsellingSection({ requiredPLI }) {
|
||||
const productTypeRequired = useProductTypeByPLI(requiredPLI);
|
||||
const productTypeRequired = getProductTypeByPLI(requiredPLI);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
export const UPGRADE_INVESTIGATION_GUIDE = (productTypeRequired: string) =>
|
||||
i18n.translate('xpack.securitySolutionServerless.markdown.insight.upsell', {
|
||||
defaultMessage:
|
||||
'Upgrade to {productTypeRequired} to make use of insights in investigation guides',
|
||||
values: {
|
||||
productTypeRequired,
|
||||
},
|
||||
});
|
||||
|
||||
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 };
|
|
@ -10,11 +10,11 @@ import { EuiEmptyPrompt, EuiIcon } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
|
||||
|
||||
const ThreatIntelligencePaywall: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
|
||||
function PaywallComponent({ requiredPLI }) {
|
||||
const productTypeRequired = useProductTypeByPLI(requiredPLI);
|
||||
const productTypeRequired = getProductTypeByPLI(requiredPLI);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import type { UpsellingService } from '@kbn/security-solution-plugin/public';
|
||||
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common';
|
||||
import { registerUpsellings, upsellingPages, upsellingSections } from './register_upsellings';
|
||||
import {
|
||||
registerUpsellings,
|
||||
upsellingMessages,
|
||||
upsellingPages,
|
||||
upsellingSections,
|
||||
} from './register_upsellings';
|
||||
import { ProductLine, ProductTier } from '../../common/product';
|
||||
import type { SecurityProductTypes } from '../../common/config';
|
||||
|
||||
|
@ -28,9 +33,11 @@ describe('registerUpsellings', () => {
|
|||
|
||||
const registerPages = jest.fn();
|
||||
const registerSections = jest.fn();
|
||||
const registerMessages = jest.fn();
|
||||
const upselling = {
|
||||
registerPages,
|
||||
registerSections,
|
||||
registerMessages,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, allProductTypes);
|
||||
|
@ -40,16 +47,22 @@ describe('registerUpsellings', () => {
|
|||
|
||||
expect(registerSections).toHaveBeenCalledTimes(1);
|
||||
expect(registerSections).toHaveBeenCalledWith({});
|
||||
|
||||
expect(registerMessages).toHaveBeenCalledTimes(1);
|
||||
expect(registerMessages).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('should register all upsellings pages and sections when PLIs features are disabled', () => {
|
||||
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 upselling = {
|
||||
registerPages,
|
||||
registerSections,
|
||||
registerMessages,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, allProductTypes);
|
||||
|
@ -65,5 +78,11 @@ describe('registerUpsellings', () => {
|
|||
);
|
||||
expect(registerSections).toHaveBeenCalledTimes(1);
|
||||
expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject);
|
||||
|
||||
const expectedMessagesObject = Object.fromEntries(
|
||||
upsellingMessages.map(({ id }) => [id, expect.any(String)])
|
||||
);
|
||||
expect(registerMessages).toHaveBeenCalledTimes(1);
|
||||
expect(registerMessages).toHaveBeenCalledWith(expectedMessagesObject);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,17 +12,29 @@ import type {
|
|||
UpsellingSectionId,
|
||||
} from '@kbn/security-solution-plugin/public';
|
||||
import React, { lazy } from 'react';
|
||||
import type {
|
||||
MessageUpsellings,
|
||||
UpsellingMessageId,
|
||||
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
|
||||
import type { SecurityProductTypes } from '../../common/config';
|
||||
import { getProductAppFeatures } from '../../common/pli/pli_features';
|
||||
|
||||
import investigationGuideUpselling from './pages/investigation_guide_upselling';
|
||||
const ThreatIntelligencePaywallLazy = lazy(() => import('./pages/threat_intelligence_paywall'));
|
||||
|
||||
interface UpsellingsConfig {
|
||||
pli: AppFeatureKey;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
interface UpsellingsMessageConfig {
|
||||
pli: AppFeatureKey;
|
||||
message: string;
|
||||
id: UpsellingMessageId;
|
||||
}
|
||||
|
||||
type UpsellingPages = Array<UpsellingsConfig & { pageName: SecurityPageName }>;
|
||||
type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
|
||||
type UpsellingMessages = UpsellingsMessageConfig[];
|
||||
|
||||
export const registerUpsellings = (
|
||||
upselling: UpsellingService,
|
||||
|
@ -50,8 +62,19 @@ export const registerUpsellings = (
|
|||
{}
|
||||
);
|
||||
|
||||
const upsellingMessagesToRegister = upsellingMessages.reduce<MessageUpsellings>(
|
||||
(messagesUpsellings, { id, pli, message }) => {
|
||||
if (!enabledPLIsSet.has(pli)) {
|
||||
messagesUpsellings[id] = message;
|
||||
}
|
||||
return messagesUpsellings;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
upselling.registerPages(upsellingPagesToRegister);
|
||||
upselling.registerSections(upsellingSectionsToRegister);
|
||||
upselling.registerMessages(upsellingMessagesToRegister);
|
||||
};
|
||||
|
||||
// Upsellings for entire pages, linked to a SecurityPageName
|
||||
|
@ -82,3 +105,12 @@ export const upsellingSections: UpsellingSections = [
|
|||
// component: () => <GenericUpsellingSectionLazy requiredPLI={AppFeatureKey.advancedInsights} />,
|
||||
// },
|
||||
];
|
||||
|
||||
// Upsellings for sections, linked by arbitrary ids
|
||||
export const upsellingMessages: UpsellingMessages = [
|
||||
{
|
||||
id: 'investigation_guide',
|
||||
pli: AppFeatureKey.investigationGuide,
|
||||
message: investigationGuideUpselling(AppFeatureKey.investigationGuide),
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue