Skip to content

Commit

Permalink
[Security Solution] expandable flyout - add paywall to prevalence det…
Browse files Browse the repository at this point in the history
…ails (elastic#165382)
  • Loading branch information
PhilippeOberti authored and sphilipse committed Sep 4, 2023
1 parent a99a410 commit 431c111
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ import { LeftPanelContext } from '../context';
import { PrevalenceDetails } from './prevalence_details';
import {
PREVALENCE_DETAILS_LOADING_TEST_ID,
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_ERROR_TEST_ID,
PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_TEST_ID,
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
} from './test_ids';
import { usePrevalence } from '../../shared/hooks/use_prevalence';
import { TestProviders } from '../../../common/mock';
import { licenseService } from '../../../common/hooks/use_license';

jest.mock('../../shared/hooks/use_prevalence');

Expand All @@ -27,6 +34,17 @@ jest.mock('react-redux', () => {
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../common/hooks/use_license', () => {
const licenseServiceInstance = {
isPlatinumPlus: jest.fn(),
};
return {
licenseService: licenseServiceInstance,
useLicense: () => {
return licenseServiceInstance;
},
};
});

const panelContextValue = {
eventId: 'event id',
Expand All @@ -36,7 +54,13 @@ const panelContextValue = {
} as unknown as LeftPanelContext;

describe('PrevalenceDetails', () => {
it('should render the table', () => {
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;

beforeEach(() => {
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
});

it('should render the table with all columns if license is platinum', () => {
const field1 = 'field1';
const field2 = 'field2';
(usePrevalence as jest.Mock).mockReturnValue({
Expand All @@ -62,7 +86,7 @@ describe('PrevalenceDetails', () => {
],
});

const { getByTestId } = render(
const { getByTestId, getAllByTestId, queryByTestId } = render(
<TestProviders>
<LeftPanelContext.Provider value={panelContextValue}>
<PrevalenceDetails />
Expand All @@ -71,6 +95,74 @@ describe('PrevalenceDetails', () => {
);

expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument();
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1);
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan(
1
);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(queryByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).not.toBeInTheDocument();
});

it('should render the table with only basic columns if license is not platinum', () => {
const field1 = 'field1';
const field2 = 'field2';
(usePrevalence as jest.Mock).mockReturnValue({
loading: false,
error: false,
data: [
{
field: field1,
value: 'value1',
alertCount: 1,
docCount: 1,
hostPrevalence: 0.05,
userPrevalence: 0.1,
},
{
field: field2,
value: 'value2',
alertCount: 1,
docCount: 1,
hostPrevalence: 0.5,
userPrevalence: 0.05,
},
],
});
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);

const { getByTestId, getAllByTestId } = render(
<TestProviders>
<LeftPanelContext.Provider value={panelContextValue}>
<PrevalenceDetails />
</LeftPanelContext.Provider>
</TestProviders>
);

expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument();
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1);
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID).length).toBeGreaterThan(1);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID).length).toBeGreaterThan(
1
);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(
getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(getByTestId(`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`)).toBeInTheDocument();
});

it('should render loading', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
* 2.0.
*/

import React, { useState } from 'react';
import dateMath from '@elastic/datemath';
import React, { useMemo, useState } from 'react';
import type { EuiBasicTableColumn, OnTimeChangeProps } from '@elastic/eui';
import {
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiSuperDatePicker,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useLicense } from '../../../common/hooks/use_license';
import { InvestigateInTimelineButton } from '../../../common/components/event_details/table/investigate_in_timeline_button';
import type { PrevalenceData } from '../../shared/hooks/use_prevalence';
import { usePrevalence } from '../../shared/hooks/use_prevalence';
Expand Down Expand Up @@ -63,16 +69,31 @@ export const PREVALENCE_TAB_ID = 'prevalence-details';
const DEFAULT_FROM = 'now-30d';
const DEFAULT_TO = 'now';

const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
interface PrevalenceDetailsRow extends PrevalenceData {
/**
* From datetime selected in the date picker to pass to timeline
*/
from: string;
/**
* To datetime selected in the date picker to pass to timeline
*/
to: string;
}

const columns: Array<EuiBasicTableColumn<PrevalenceDetailsRow>> = [
{
field: 'field',
name: PREVALENCE_TABLE_FIELD_COLUMN_TITLE,
'data-test-subj': PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
render: (field: string) => <EuiText size="xs">{field}</EuiText>,
width: '20%',
},
{
field: 'value',
name: PREVALENCE_TABLE_VALUE_COLUMN_TITLE,
'data-test-subj': PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
render: (value: string) => <EuiText size="xs">{value}</EuiText>,
width: '20%',
},
{
name: (
Expand All @@ -84,7 +105,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
</EuiToolTip>
),
'data-test-subj': PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
render: (data: PrevalenceData) => {
render: (data: PrevalenceDetailsRow) => {
const dataProviders = [
getDataProvider(data.field, `timeline-indicator-${data.field}-${data.value}`, data.value),
];
Expand All @@ -93,6 +114,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
asEmptyButton={true}
dataProviders={dataProviders}
filters={[]}
timeRange={{ kind: 'absolute', from: data.from, to: data.to }}
>
<>{data.alertCount}</>
</InvestigateInTimelineButton>
Expand All @@ -112,7 +134,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
</EuiToolTip>
),
'data-test-subj': PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
render: (data: PrevalenceData) => {
render: (data: PrevalenceDetailsRow) => {
const dataProviders = [
{
...getDataProvider(
Expand All @@ -136,6 +158,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
asEmptyButton={true}
dataProviders={dataProviders}
filters={[]}
timeRange={{ kind: 'absolute', from: data.from, to: data.to }}
keepDataView // changing dataview from only detections to include non-alerts docs
>
<>{data.docCount}</>
Expand All @@ -158,10 +181,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
),
'data-test-subj': PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
render: (hostPrevalence: number) => (
<>
{Math.round(hostPrevalence * 100)}
{'%'}
</>
<EuiText size="xs">{`${Math.round(hostPrevalence * 100)}%`}</EuiText>
),
width: '10%',
},
Expand All @@ -177,10 +197,7 @@ const columns: Array<EuiBasicTableColumn<PrevalenceData>> = [
),
'data-test-subj': PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
render: (userPrevalence: number) => (
<>
{Math.round(userPrevalence * 100)}
{'%'}
</>
<EuiText size="xs">{`${Math.round(userPrevalence * 100)}%`}</EuiText>
),
width: '10%',
},
Expand All @@ -193,12 +210,38 @@ export const PrevalenceDetails: React.FC = () => {
const { browserFields, dataFormattedForFieldBrowser, eventId, investigationFields } =
useLeftPanelContext();

const isPlatinumPlus = useLicense().isPlatinumPlus();

// these two are used by the usePrevalence hook to fetch the data
const [start, setStart] = useState(DEFAULT_FROM);
const [end, setEnd] = useState(DEFAULT_TO);

const onTimeChange = ({ start: s, end: e }: OnTimeChangeProps) => {
// these two are used to pass to timeline
const [absoluteStart, setAbsoluteStart] = useState(
(dateMath.parse(DEFAULT_FROM) || new Date()).toISOString()
);
const [absoluteEnd, setAbsoluteEnd] = useState(
(dateMath.parse(DEFAULT_TO) || new Date()).toISOString()
);

// TODO update the logic to use a single set of start/end dates
// currently as we're using this InvestigateInTimelineButton component we need to pass the timeRange
// as an AbsoluteTimeRange, which requires from/to values
const onTimeChange = ({ start: s, end: e, isInvalid }: OnTimeChangeProps) => {
if (isInvalid) return;

setStart(s);
setEnd(e);

const from = dateMath.parse(s);
if (from && from.isValid()) {
setAbsoluteStart(from.toISOString());
}

const to = dateMath.parse(e);
if (to && to.isValid()) {
setAbsoluteEnd(to.toISOString());
}
};

const { loading, error, data } = usePrevalence({
Expand All @@ -210,6 +253,12 @@ export const PrevalenceDetails: React.FC = () => {
},
});

// add timeRange to pass it down to timeline
const items = useMemo(
() => data.map((item) => ({ ...item, from: absoluteStart, to: absoluteEnd })),
[data, absoluteStart, absoluteEnd]
);

if (loading) {
return (
<EuiFlexGroup
Expand All @@ -235,8 +284,31 @@ export const PrevalenceDetails: React.FC = () => {
);
}

const upsell = (
<>
<EuiCallOut data-test-subj={`${PREVALENCE_DETAILS_TABLE_TEST_ID}UpSell`}>
<FormattedMessage
id="xpack.securitySolution.flyout.documentDetails.prevalenceTableAlertUpsell"
defaultMessage="Preview of a {subscription} feature showing host and user prevalence."
values={{
subscription: (
<EuiLink href="https://www.elastic.co/pricing/" target="_blank">
<FormattedMessage
id="xpack.securitySolution.flyout.documentDetails.prevalenceTableAlertUpsellLink"
defaultMessage="Platinum"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer size="s" />
</>
);

return (
<>
{!isPlatinumPlus && upsell}
<EuiPanel>
<EuiSuperDatePicker
start={start}
Expand All @@ -247,7 +319,7 @@ export const PrevalenceDetails: React.FC = () => {
<EuiSpacer size="m" />
{data.length > 0 ? (
<EuiInMemoryTable
items={data}
items={items}
columns={columns}
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const ResponseDetails: React.FC = () => {
values={{
editRuleLink: (
<EuiLink
href="https://www.elastic.co/guide/en/security/master/rules-ui-management.html#edit-rules-settings"
href="https://www.elastic.co/guide/en/security/current/rules-ui-management.html#edit-rules-settings"
target="_blank"
>
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
field: 'field',
name: HIGHLIGHTED_FIELDS_FIELD_COLUMN,
'data-test-subj': 'fieldCell',
width: '50%',
},
{
field: 'description',
name: HIGHLIGHTED_FIELDS_VALUE_COLUMN,
'data-test-subj': 'valueCell',
width: '50%',
render: (description: {
field: string;
values: string[] | null | undefined;
Expand Down
Loading

0 comments on commit 431c111

Please sign in to comment.