Skip to content

Commit

Permalink
[Security Solutions] Add upselling service to security solutions ESS …
Browse files Browse the repository at this point in the history
…plugin (#163406)

## Summary

* Use Serverless upsell architecture on ESS. But check for the required
license instead of capabilities.
* Covert Investigation Guide and Entity Analytics upsell to the new
architecture.
* Update upsell registering functions always to clear the state when
registering a new value. It fits perfectly ESS because the license is
observable.
### Checklist

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

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
machadoum and kibanamachine authored Aug 16, 2023
1 parent a00c240 commit 5b2859f
Show file tree
Hide file tree
Showing 41 changed files with 534 additions and 301 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -72,12 +71,10 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
}
}, [autoFocusDisabled]);

const licenseIsPlatinum = useLicense().isPlatinumPlus();

const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
const uiPluginsWithState = useMemo(() => {
return uiPlugins({ licenseIsPlatinum, insightsUpsellingMessage });
}, [licenseIsPlatinum, insightsUpsellingMessage]);
return uiPlugins({ insightsUpsellingMessage });
}, [insightsUpsellingMessage]);

// @ts-expect-error update types
useImperativeHandle(ref, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const RenderWrapper: React.FunctionComponent = ({ children }) => {

describe('use_upselling', () => {
test('useUpsellingComponent returns sections', () => {
mockUpselling.registerSections({
mockUpselling.setSections({
entity_analytics_panel: TestComponent,
});

Expand All @@ -47,7 +47,7 @@ describe('use_upselling', () => {
});

test('useUpsellingPage returns pages', () => {
mockUpselling.registerPages({
mockUpselling.setPages({
[SecurityPageName.hosts]: TestComponent,
});

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

Expand All @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const TestComponent = () => <div>{'TEST component'}</div>;
describe('UpsellingService', () => {
it('registers sections', async () => {
const service = new UpsellingService();
service.registerSections({
service.setSections({
entity_analytics_panel: TestComponent,
});

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

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit 5b2859f

Please sign in to comment.