Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create upselling package and implement EntityAnalytics serverless upselling #164136

Merged
merged 11 commits into from
Aug 24, 2023
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@
"@kbn/security-solution-serverless": "link:x-pack/plugins/security_solution_serverless",
"@kbn/security-solution-side-nav": "link:x-pack/packages/security-solution/side_nav",
"@kbn/security-solution-storybook-config": "link:x-pack/packages/security-solution/storybook/config",
"@kbn/security-solution-upselling": "link:x-pack/packages/security-solution/upselling",
"@kbn/security-test-endpoints-plugin": "link:x-pack/test/security_functional/plugins/test_endpoints",
"@kbn/securitysolution-autocomplete": "link:packages/kbn-securitysolution-autocomplete",
"@kbn/securitysolution-data-table": "link:x-pack/packages/security-solution/data_table",
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,8 @@
"@kbn/security-solution-side-nav/*": ["x-pack/packages/security-solution/side_nav/*"],
"@kbn/security-solution-storybook-config": ["x-pack/packages/security-solution/storybook/config"],
"@kbn/security-solution-storybook-config/*": ["x-pack/packages/security-solution/storybook/config/*"],
"@kbn/security-solution-upselling": ["x-pack/packages/security-solution/upselling"],
"@kbn/security-solution-upselling/*": ["x-pack/packages/security-solution/upselling/*"],
"@kbn/security-test-endpoints-plugin": ["x-pack/test/security_functional/plugins/test_endpoints"],
"@kbn/security-test-endpoints-plugin/*": ["x-pack/test/security_functional/plugins/test_endpoints/*"],
"@kbn/securitysolution-autocomplete": ["packages/kbn-securitysolution-autocomplete"],
Expand Down
3 changes: 3 additions & 0 deletions x-pack/packages/security-solution/upselling/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Security Solution Upselling

This package contains the upselling service that registers pages/component/messages and shared upselling components for ESS and Serverless plugins.
12 changes: 12 additions & 0 deletions x-pack/packages/security-solution/upselling/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/security-solution/upselling'],
};
5 changes: 5 additions & 0 deletions x-pack/packages/security-solution/upselling/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/security-solution-upselling",
"owner": "@elastic/security-threat-hunting-explore"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';

export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) =>
i18n.translate('xpack.securitySolutionEss.markdown.insight.upsell', {
i18n.translate('securitySolutionPackages.markdown.insight.upsell', {
defaultMessage: 'Upgrade to {requiredLicense} to make use of insights in investigation guides',
values: {
requiredLicense,
Expand Down
6 changes: 6 additions & 0 deletions x-pack/packages/security-solution/upselling/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/security-solution-upselling",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '@testing-library/react';
import EntityAnalyticsUpsellingComponent from './entity_analytics';

jest.mock('@kbn/security-solution-navigation', () => {
const original = jest.requireActual('@kbn/security-solution-navigation');
return {
...original,
useNavigation: () => ({
navigateTo: jest.fn(),
}),
};
});

describe('EntityAnalyticsUpselling', () => {
it('should render', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);
expect(getByTestId('paywallCardDescription')).toBeInTheDocument();
});

it('should throw exception when requiredLicense and requiredProduct are not provided', () => {
expect(() => render(<EntityAnalyticsUpsellingComponent />)).toThrow();
});

it('should show product message when requiredProduct is provided', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent
requiredProduct="TEST PRODUCT"
requiredLicense="TEST LICENSE"
/>
);

expect(getByTestId('paywallCardDescription')).toHaveTextContent(
'Entity risk scoring capability is available in our TEST PRODUCT license tier'
);
});

it('should show product badge when requiredProduct is provided', () => {
const { getByText } = render(
<EntityAnalyticsUpsellingComponent
requiredProduct="TEST PRODUCT"
requiredLicense="TEST LICENSE"
/>
);

expect(getByText('TEST PRODUCT')).toBeInTheDocument();
});

it('should show license message when requiredLicense is provided', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);

expect(getByTestId('paywallCardDescription')).toHaveTextContent(
'This feature is available with TEST LICENSE or higher subscription'
);
});

it('should show license badge when requiredLicense is provided', () => {
const { getByText } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);

expect(getByText('TEST LICENSE')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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';
import paywallPng from '../images/entity_paywall.png';

const PaywallDiv = styled.div`
max-width: 75%;
Expand All @@ -33,7 +33,7 @@ const PaywallDiv = styled.div`
width: auto;
}
}
.platinumCardDescription {
.paywallCardDescription {
padding: 0 15%;
}
`;
Expand All @@ -45,24 +45,39 @@ const StyledEuiCard = styled(EuiCard)`
}
`;

const EntityAnalyticsUpsellingComponent = () => {
const { getAppUrl, navigateTo } = useNavigation();
const subscriptionUrl = getAppUrl({
appId: 'management',
path: 'stack/license_management',
});
const EntityAnalyticsUpsellingComponent = ({
requiredLicense,
requiredProduct,
subscriptionUrl,
}: {
requiredLicense?: string;
requiredProduct?: string;
subscriptionUrl?: string;
}) => {
const { navigateTo } = useNavigation();

const goToSubscription = useCallback(() => {
navigateTo({ url: subscriptionUrl });
}, [navigateTo, subscriptionUrl]);

if (!requiredProduct && !requiredLicense) {
throw new Error('requiredProduct or requiredLicense must be defined');
}

const upgradeMessage = requiredProduct
? i18n.UPGRADE_PRODUCT_MESSAGE(requiredProduct)
: i18n.UPGRADE_LICENSE_MESSAGE(requiredLicense ?? '');

const requiredProductOrLicense = requiredProduct ?? requiredLicense ?? '';

return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<EuiPageHeader pageTitle={i18n.ENTITY_ANALYTICS_TITLE} />
<EuiSpacer size="xl" />
<PaywallDiv>
<StyledEuiCard
data-test-subj="platinumCard"
betaBadgeProps={{ label: i18n.PLATINUM }}
betaBadgeProps={{ label: requiredProductOrLicense }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
Expand All @@ -73,26 +88,33 @@ const EntityAnalyticsUpsellingComponent = () => {
description={false}
paddingSize="xl"
>
<EuiFlexGroup className="platinumCardDescription" direction="column" gutterSize="none">
<EuiFlexGroup
data-test-subj="paywallCardDescription"
className="paywallCardDescription"
direction="column"
gutterSize="none"
>
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{i18n.UPGRADE_MESSAGE}</EuiTextColor>
<EuiTextColor color="subdued">{upgradeMessage}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON}
</EuiButton>
</div>
{subscriptionUrl && (
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON(requiredProductOrLicense)}
</EuiButton>
</div>
)}
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
<EuiFlexGroup>
<EuiFlexItem>
<EuiImage alt={i18n.UPGRADE_MESSAGE} src={paywallPng} size="fullWidth" />
<EuiImage alt={upgradeMessage} src={paywallPng} size="fullWidth" />
</EuiFlexItem>
</EuiFlexGroup>
</PaywallDiv>
Expand Down
47 changes: 47 additions & 0 deletions x-pack/packages/security-solution/upselling/pages/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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_LICENSE_MESSAGE = (requiredLicense: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeLicenseMessage', {
defaultMessage: 'This feature is available with {requiredLicense} or higher subscription',
values: {
requiredLicense,
},
});

export const UPGRADE_PRODUCT_MESSAGE = (requiredProduct: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeProductMessage', {
defaultMessage:
'Entity risk scoring capability is available in our {requiredProduct} license tier',
values: {
requiredProduct,
},
});

export const UPGRADE_BUTTON = (requiredLicenseOrProduct: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeButton', {
defaultMessage: 'Upgrade to {requiredLicenseOrProduct}',
values: {
requiredLicenseOrProduct,
},
});

export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate(
'securitySolutionPackages.entityAnalytics.pageDesc',
{
defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics',
}
);

export const ENTITY_ANALYTICS_TITLE = i18n.translate(
'securitySolutionPackages.entityAnalytics.navigation',
{
defaultMessage: 'Entity Analytics',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
* 2.0.
*/
export { UpsellingService } from './upselling_service';
export type { PageUpsellings, SectionUpsellings, UpsellingSectionId } from './types';
export type {
PageUpsellings,
SectionUpsellings,
UpsellingSectionId,
UpsellingMessageId,
} from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { SecurityPageName } from '../../../../common';
import type { SecurityPageName } from '@kbn/security-solution-navigation';

export type PageUpsellings = Partial<Record<SecurityPageName, React.ComponentType>>;
export type MessageUpsellings = Partial<Record<UpsellingMessageId, string>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import React from 'react';
import { firstValueFrom } from 'rxjs';
import { SecurityPageName } from '../../../../common';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { UpsellingService } from './upselling_service';

const TestComponent = () => <div>{'TEST component'}</div>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { SecurityPageName } from '../../../../common';
import type { SecurityPageName } from '@kbn/security-solution-navigation';
import type {
SectionUpsellings,
PageUpsellings,
Expand Down
25 changes: 25 additions & 0 deletions x-pack/packages/security-solution/upselling/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop",
"@testing-library/jest-dom",
"@testing-library/react",
"@kbn/ambient-ui-types"
]
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"kbn_references": [
"@kbn/i18n",
],
"exclude": [
"target/**/*"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { AppLinkItems } from '../../links';
import { updateAppLinks } from '../../links';
import { mockGlobalState } from '../../mock';
import type { Capabilities } from '@kbn/core-capabilities-common';
import { UpsellingService } from '../../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';

const defaultAppLinks: AppLinkItems = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React, { memo, useContext } from 'react';
import type { UpsellingService } from '../../..';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';

export const UpsellingProviderContext = React.createContext<UpsellingService | null>(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { SecurityPageName } from '../../../common';
import { UpsellingService } from '../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { useUpsellingComponent, useUpsellingMessage, useUpsellingPage } from './use_upselling';
import { UpsellingProvider } from '../components/upselling_provider';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type React from 'react';
import type {
UpsellingSectionId,
UpsellingMessageId,
} from '@kbn/security-solution-upselling/service';
import { useUpsellingService } from '../components/upselling_provider';
import type { UpsellingSectionId } from '../lib/upsellings';
import type { SecurityPageName } from '../../../common';
import type { UpsellingMessageId } from '../lib/upsellings/types';

export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentType | null => {
const upselling = useUpsellingService();
Expand Down
Loading