mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] expandable flyout - add paywall to prevalence details (#165382)
This commit is contained in:
parent
88c04e5c94
commit
8267c56322
5 changed files with 196 additions and 21 deletions
|
@ -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');
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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({
|
||||
|
@ -62,7 +86,7 @@ describe('PrevalenceDetails', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
const { getByTestId, getAllByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<LeftPanelContext.Provider value={panelContextValue}>
|
||||
<PrevalenceDetails />
|
||||
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
@ -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: (
|
||||
|
@ -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),
|
||||
];
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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}</>
|
||||
|
@ -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%',
|
||||
},
|
||||
|
@ -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%',
|
||||
},
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,6 +11,9 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { createFetchData } from '../utils/fetch_data';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
const QUERY_KEY = 'useFetchFieldValuePairWithAggregation';
|
||||
|
||||
|
@ -99,7 +102,10 @@ export const useFetchPrevalence = ({
|
|||
},
|
||||
} = useKibana();
|
||||
|
||||
const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to);
|
||||
// retrieves detections and non-detections indices (for example, the alert security index from the current space and 'logs-*' indices)
|
||||
const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline(SourcererScopeName.default));
|
||||
|
||||
const searchRequest = buildSearchRequest(highlightedFieldsFilters, from, to, selectedPatterns);
|
||||
|
||||
const { data, isLoading, isError } = useQuery(
|
||||
[QUERY_KEY, highlightedFieldsFilters, from, to],
|
||||
|
@ -120,7 +126,8 @@ export const useFetchPrevalence = ({
|
|||
const buildSearchRequest = (
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>,
|
||||
from: string,
|
||||
to: string
|
||||
to: string,
|
||||
selectedPatterns: string[]
|
||||
): IEsSearchRequest => {
|
||||
const query = buildEsQuery(
|
||||
undefined,
|
||||
|
@ -146,14 +153,16 @@ const buildSearchRequest = (
|
|||
]
|
||||
);
|
||||
|
||||
return buildAggregationSearchRequest(query, highlightedFieldsFilters);
|
||||
return buildAggregationSearchRequest(query, highlightedFieldsFilters, selectedPatterns);
|
||||
};
|
||||
|
||||
const buildAggregationSearchRequest = (
|
||||
query: QueryDslQueryContainer,
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>
|
||||
highlightedFieldsFilters: Record<string, QueryDslQueryContainer>,
|
||||
selectedPatterns: string[]
|
||||
): IEsSearchRequest => ({
|
||||
params: {
|
||||
index: selectedPatterns,
|
||||
body: {
|
||||
query,
|
||||
aggs: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue