mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Enable alert preview in document details flyout (#186857)
## Summary
This PR enables alert preview in the document flyout.
How to test:
- Enable feature flag `entityAlertPreviewEnabled`
- Generate some alerts and open alert flyout
- Go to correlations details (expanded section)
- Click on any hyperlinked rule to open an alert preview
118a3e22
-94d2-4b68-bf23-0f77ad5e8cfd
### 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
This commit is contained in:
parent
e105a34924
commit
7c6186ab91
33 changed files with 691 additions and 200 deletions
|
@ -14,6 +14,7 @@ import {
|
|||
EuiText,
|
||||
useEuiTheme,
|
||||
EuiSplitPanel,
|
||||
transparentize,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -120,8 +121,8 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
|
|||
<div
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 12px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
right: 4px;
|
||||
left: ${left}px;
|
||||
z-index: 1000;
|
||||
|
@ -130,7 +131,7 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
|
|||
<EuiSplitPanel.Outer
|
||||
css={css`
|
||||
margin: ${euiTheme.size.xs};
|
||||
box-shadow: 0 0 4px 4px ${euiTheme.colors.darkShade};
|
||||
box-shadow: 0 0 16px 0px ${transparentize(euiTheme.colors.mediumShade, 0.5)};
|
||||
`}
|
||||
data-test-subj={PREVIEW_SECTION_TEST_ID}
|
||||
className="eui-fullHeight"
|
||||
|
@ -139,13 +140,13 @@ export const PreviewSection: React.FC<PreviewSectionProps> = ({
|
|||
<EuiSplitPanel.Inner
|
||||
grow={false}
|
||||
color={banner.backgroundColor}
|
||||
paddingSize="none"
|
||||
paddingSize="xs"
|
||||
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerPanel`}
|
||||
>
|
||||
<EuiText
|
||||
textAlign="center"
|
||||
color={banner.textColor}
|
||||
size="s"
|
||||
size="xs"
|
||||
data-test-subj={`${PREVIEW_SECTION_TEST_ID}BannerText`}
|
||||
>
|
||||
{banner.title}
|
||||
|
|
|
@ -8,24 +8,49 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { CorrelationsDetailsAlertsTable, columns } from './correlations_details_alerts_table';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { CorrelationsDetailsAlertsTable } from './correlations_details_alerts_table';
|
||||
import { usePaginatedAlerts } from '../hooks/use_paginated_alerts';
|
||||
import { CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID } from './test_ids';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys';
|
||||
import { ALERT_PREVIEW_BANNER } from '../../preview';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
|
||||
jest.mock('../hooks/use_paginated_alerts');
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
EuiBasicTable: jest.fn(() => <div data-testid="mock-euibasictable" />),
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
jest.mock('@kbn/expandable-flyout', () => ({
|
||||
useExpandableFlyoutApi: jest.fn(),
|
||||
ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}</>,
|
||||
}));
|
||||
|
||||
const TEST_ID = 'TEST';
|
||||
const scopeId = 'scopeId';
|
||||
const eventId = 'eventId';
|
||||
const alertIds = ['id1', 'id2', 'id3'];
|
||||
|
||||
const renderCorrelationsTable = (panelContext: DocumentDetailsContext) =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<DocumentDetailsContext.Provider value={panelContext}>
|
||||
<CorrelationsDetailsAlertsTable
|
||||
title={<p>{'title'}</p>}
|
||||
loading={false}
|
||||
alertIds={alertIds}
|
||||
scopeId={mockContextValue.scopeId}
|
||||
eventId={mockContextValue.eventId}
|
||||
data-test-subj={TEST_ID}
|
||||
/>
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('CorrelationsDetailsAlertsTable', () => {
|
||||
const alertIds = ['id1', 'id2', 'id3'];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
jest.mocked(usePaginatedAlerts).mockReturnValue({
|
||||
setPagination: jest.fn(),
|
||||
setSorting: jest.fn(),
|
||||
|
@ -64,44 +89,45 @@ describe('CorrelationsDetailsAlertsTable', () => {
|
|||
});
|
||||
|
||||
it('renders EuiBasicTable with correct props', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CorrelationsDetailsAlertsTable
|
||||
title={<p>{'title'}</p>}
|
||||
loading={false}
|
||||
alertIds={alertIds}
|
||||
scopeId={scopeId}
|
||||
eventId={eventId}
|
||||
data-test-subj={TEST_ID}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId, queryByTestId, queryAllByRole } =
|
||||
renderCorrelationsTable(mockContextValue);
|
||||
|
||||
expect(getByTestId(`${TEST_ID}InvestigateInTimeline`)).toBeInTheDocument();
|
||||
expect(getByTestId(`${TEST_ID}Table`)).toBeInTheDocument();
|
||||
expect(
|
||||
queryByTestId(CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID)
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(jest.mocked(usePaginatedAlerts)).toHaveBeenCalled();
|
||||
|
||||
expect(jest.mocked(EuiBasicTable)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
loading: false,
|
||||
items: [
|
||||
{
|
||||
'@timestamp': '2022-01-01',
|
||||
'kibana.alert.rule.name': 'Rule1',
|
||||
'kibana.alert.reason': 'Reason1',
|
||||
'kibana.alert.severity': 'Severity1',
|
||||
},
|
||||
{
|
||||
'@timestamp': '2022-01-02',
|
||||
'kibana.alert.rule.name': 'Rule2',
|
||||
'kibana.alert.reason': 'Reason2',
|
||||
'kibana.alert.severity': 'Severity2',
|
||||
},
|
||||
],
|
||||
columns,
|
||||
pagination: { pageIndex: 0, pageSize: 5, totalItemCount: 10, pageSizeOptions: [5, 10, 20] },
|
||||
sorting: { sort: { field: '@timestamp', direction: 'asc' }, enableAllColumns: true },
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
expect(queryAllByRole('columnheader').length).toBe(4);
|
||||
expect(queryAllByRole('row').length).toBe(3); // 1 header row and 2 data rows
|
||||
expect(queryAllByRole('row')[1].textContent).toContain('Jan 1, 2022 @ 00:00:00.000');
|
||||
expect(queryAllByRole('row')[1].textContent).toContain('Reason1');
|
||||
expect(queryAllByRole('row')[1].textContent).toContain('Rule1');
|
||||
expect(queryAllByRole('row')[1].textContent).toContain('Severity1');
|
||||
});
|
||||
|
||||
it('renders open preview button when feature flag is on', () => {
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
|
||||
const { getByTestId, getAllByTestId } = renderCorrelationsTable({
|
||||
...mockContextValue,
|
||||
isPreviewMode: true,
|
||||
});
|
||||
|
||||
expect(getByTestId(`${TEST_ID}InvestigateInTimeline`)).toBeInTheDocument();
|
||||
expect(getAllByTestId(CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID).length).toBe(2);
|
||||
|
||||
getAllByTestId(CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID)[0].click();
|
||||
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
|
||||
id: DocumentDetailsPreviewPanelKey,
|
||||
params: {
|
||||
id: '1',
|
||||
indexName: 'index',
|
||||
scopeId: mockContextValue.scopeId,
|
||||
banner: ALERT_PREVIEW_BANNER,
|
||||
isPreviewMode: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,13 +7,17 @@
|
|||
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import React, { type FC, useMemo, useCallback } from 'react';
|
||||
import { type Criteria, EuiBasicTable, formatDate } from '@elastic/eui';
|
||||
import { type Criteria, EuiBasicTable, formatDate, EuiButtonIcon } from '@elastic/eui';
|
||||
import { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import { CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID } from './test_ids';
|
||||
import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper';
|
||||
import type { DataProvider } from '../../../../../common/types';
|
||||
import { SeverityBadge } from '../../../../common/components/severity_badge';
|
||||
|
@ -22,80 +26,50 @@ import { ExpandablePanel } from '../../../shared/components/expandable_panel';
|
|||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
||||
import { getDataProvider } from '../../../../common/components/event_details/table/use_action_cell_data_provider';
|
||||
import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys';
|
||||
import { ALERT_PREVIEW_BANNER } from '../../preview';
|
||||
|
||||
export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
const dataProviderLimit = 5;
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
field: '@timestamp',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.timestampColumnLabel"
|
||||
defaultMessage="Timestamp"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
dataType: 'date' as const,
|
||||
render: (value: string) => {
|
||||
const date = formatDate(value, TIMESTAMP_DATE_FORMAT);
|
||||
return (
|
||||
<CellTooltipWrapper tooltip={date}>
|
||||
<span>{date}</span>
|
||||
</CellTooltipWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: ALERT_RULE_NAME,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.ruleColumnLabel"
|
||||
defaultMessage="Rule"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => (
|
||||
<CellTooltipWrapper tooltip={value}>
|
||||
<span>{value}</span>
|
||||
</CellTooltipWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: ALERT_REASON,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.reasonColumnLabel"
|
||||
defaultMessage="Reason"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => (
|
||||
<CellTooltipWrapper tooltip={value} anchorPosition="left">
|
||||
<span>{value}</span>
|
||||
</CellTooltipWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'kibana.alert.severity',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.severityColumnLabel"
|
||||
defaultMessage="Severity"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => {
|
||||
const decodedSeverity = Severity.decode(value);
|
||||
const renderValue = isRight(decodedSeverity) ? (
|
||||
<SeverityBadge value={decodedSeverity.right} />
|
||||
) : (
|
||||
<p>{value}</p>
|
||||
);
|
||||
return <CellTooltipWrapper tooltip={value}>{renderValue}</CellTooltipWrapper>;
|
||||
},
|
||||
},
|
||||
];
|
||||
interface AlertPreviewButtonProps {
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name of the index used in the parent's page
|
||||
*/
|
||||
indexName: string;
|
||||
}
|
||||
|
||||
const AlertPreviewButton: FC<AlertPreviewButtonProps> = ({ id, indexName }) => {
|
||||
const { openPreviewPanel } = useExpandableFlyoutApi();
|
||||
const { scopeId } = useDocumentDetailsContext();
|
||||
|
||||
const openAlertPreview = useCallback(
|
||||
() =>
|
||||
openPreviewPanel({
|
||||
id: DocumentDetailsPreviewPanelKey,
|
||||
params: {
|
||||
id,
|
||||
indexName,
|
||||
scopeId,
|
||||
isPreviewMode: true,
|
||||
banner: ALERT_PREVIEW_BANNER,
|
||||
},
|
||||
}),
|
||||
[openPreviewPanel, id, indexName, scopeId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
iconType="expand"
|
||||
data-test-subj={CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID}
|
||||
onClick={openAlertPreview}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface CorrelationsDetailsAlertsTableProps {
|
||||
/**
|
||||
|
@ -149,6 +123,7 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
|
|||
sorting,
|
||||
error,
|
||||
} = usePaginatedAlerts(alertIds || []);
|
||||
const isPreviewEnabled = useIsExperimentalFeatureEnabled('entityAlertPreviewEnabled');
|
||||
|
||||
const onTableChange = useCallback(
|
||||
({ page, sort }: Criteria<Record<string, unknown>>) => {
|
||||
|
@ -166,13 +141,17 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
|
|||
|
||||
const mappedData = useMemo(() => {
|
||||
return data
|
||||
.map((hit) => hit.fields)
|
||||
.map((fields = {}) =>
|
||||
Object.keys(fields).reduce((result, fieldName) => {
|
||||
result[fieldName] = fields?.[fieldName]?.[0] || fields?.[fieldName];
|
||||
.map((hit) => ({ fields: hit.fields ?? {}, id: hit._id, index: hit._index }))
|
||||
.map((dataWithMeta) => {
|
||||
const res = Object.keys(dataWithMeta.fields).reduce((result, fieldName) => {
|
||||
result[fieldName] =
|
||||
dataWithMeta.fields?.[fieldName]?.[0] || dataWithMeta.fields?.[fieldName];
|
||||
return result;
|
||||
}, {} as Record<string, unknown>)
|
||||
);
|
||||
}, {} as Record<string, unknown>);
|
||||
res.id = dataWithMeta.id;
|
||||
res.index = dataWithMeta.index;
|
||||
return res;
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
const shouldUseFilters = Boolean(
|
||||
|
@ -187,6 +166,90 @@ export const CorrelationsDetailsAlertsTable: FC<CorrelationsDetailsAlertsTablePr
|
|||
[alertIds, shouldUseFilters]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
...(isPreviewEnabled
|
||||
? [
|
||||
{
|
||||
render: (row: Record<string, unknown>) => (
|
||||
<AlertPreviewButton id={row.id as string} indexName={row.index as string} />
|
||||
),
|
||||
width: '5%',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
field: '@timestamp',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.timestampColumnLabel"
|
||||
defaultMessage="Timestamp"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
dataType: 'date' as const,
|
||||
render: (value: string) => {
|
||||
const date = formatDate(value, TIMESTAMP_DATE_FORMAT);
|
||||
return (
|
||||
<CellTooltipWrapper tooltip={date}>
|
||||
<span>{date}</span>
|
||||
</CellTooltipWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: ALERT_RULE_NAME,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.ruleColumnLabel"
|
||||
defaultMessage="Rule"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => (
|
||||
<CellTooltipWrapper tooltip={value}>
|
||||
<span>{value}</span>
|
||||
</CellTooltipWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: ALERT_REASON,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.reasonColumnLabel"
|
||||
defaultMessage="Reason"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => (
|
||||
<CellTooltipWrapper tooltip={value} anchorPosition="left">
|
||||
<span>{value}</span>
|
||||
</CellTooltipWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'kibana.alert.severity',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.left.insights.correlations.severityColumnLabel"
|
||||
defaultMessage="Severity"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
render: (value: string) => {
|
||||
const decodedSeverity = Severity.decode(value);
|
||||
const renderValue = isRight(decodedSeverity) ? (
|
||||
<SeverityBadge value={decodedSeverity.right} />
|
||||
) : (
|
||||
<p>{value}</p>
|
||||
);
|
||||
return <CellTooltipWrapper tooltip={value}>{renderValue}</CellTooltipWrapper>;
|
||||
},
|
||||
},
|
||||
],
|
||||
[isPreviewEnabled]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
import {
|
||||
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID,
|
||||
CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID,
|
||||
|
@ -41,7 +43,9 @@ const TITLE_TEXT = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
|
|||
const renderRelatedAlertsByAncestry = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
import {
|
||||
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
|
||||
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID,
|
||||
|
@ -41,11 +43,13 @@ const TITLE_TEXT = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
|
|||
const renderRelatedAlertsBySameSourceEvent = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<RelatedAlertsBySameSourceEvent
|
||||
originalEventId={originalEventId}
|
||||
scopeId={scopeId}
|
||||
eventId={eventId}
|
||||
/>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<RelatedAlertsBySameSourceEvent
|
||||
originalEventId={originalEventId}
|
||||
scopeId={scopeId}
|
||||
eventId={eventId}
|
||||
/>
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
import {
|
||||
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID,
|
||||
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID,
|
||||
|
@ -41,7 +43,9 @@ const TITLE_TEXT = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
|
|||
const renderRelatedAlertsBySession = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} eventId={eventId} />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -69,6 +69,9 @@ export const THREAT_INTELLIGENCE_DETAILS_LOADING_TEST_ID =
|
|||
|
||||
export const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const;
|
||||
|
||||
export const CORRELATIONS_DETAILS_ALERT_PREVIEW_BUTTON_TEST_ID =
|
||||
`${CORRELATIONS_DETAILS_TEST_ID}AlertPreviewButton` as const;
|
||||
|
||||
export const CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID =
|
||||
`${CORRELATIONS_DETAILS_TEST_ID}AlertsByAncestrySection` as const;
|
||||
export const CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID =
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys';
|
||||
import { mockFlyoutApi } from '../shared/mocks/mock_flyout_context';
|
||||
import { mockContextValue } from '../shared/mocks/mock_context';
|
||||
import { DocumentDetailsContext } from '../shared/context';
|
||||
import { PreviewPanelFooter } from './footer';
|
||||
import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout', () => ({
|
||||
useExpandableFlyoutApi: jest.fn(),
|
||||
ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}</>,
|
||||
}));
|
||||
|
||||
describe('<PreviewPanelFooter />', () => {
|
||||
beforeAll(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
});
|
||||
|
||||
it('should render footer', () => {
|
||||
const { getByTestId } = render(
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<PreviewPanelFooter />
|
||||
</DocumentDetailsContext.Provider>
|
||||
);
|
||||
expect(getByTestId(PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open document details flyout when clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<PreviewPanelFooter />
|
||||
</DocumentDetailsContext.Provider>
|
||||
);
|
||||
|
||||
getByTestId(PREVIEW_FOOTER_LINK_TEST_ID).click();
|
||||
expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: mockContextValue.eventId,
|
||||
indexName: mockContextValue.indexName,
|
||||
scopeId: mockContextValue.scopeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FlyoutFooter } from '../../shared/components/flyout_footer';
|
||||
import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys';
|
||||
import { useDocumentDetailsContext } from '../shared/context';
|
||||
import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
|
||||
|
||||
/**
|
||||
* Footer at the bottom of preview panel with a link to open document details flyout
|
||||
*/
|
||||
export const PreviewPanelFooter = () => {
|
||||
const { eventId, indexName, scopeId } = useDocumentDetailsContext();
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const openDocumentFlyout = useCallback(() => {
|
||||
openFlyout({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [openFlyout, eventId, indexName, scopeId]);
|
||||
|
||||
return (
|
||||
<FlyoutFooter data-test-subj={PREVIEW_FOOTER_TEST_ID}>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
onClick={openDocumentFlyout}
|
||||
target="_blank"
|
||||
data-test-subj={PREVIEW_FOOTER_LINK_TEST_ID}
|
||||
>
|
||||
{i18n.translate('xpack.securitySolution.flyout.preview.openFlyoutLabel', {
|
||||
defaultMessage: 'Show full alert details',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FlyoutFooter>
|
||||
);
|
||||
};
|
|
@ -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 type { FC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DocumentDetailsPreviewPanelKey } from '../shared/constants/panel_keys';
|
||||
import { useTabs } from '../right/hooks/use_tabs';
|
||||
import { useFlyoutIsExpandable } from '../right/hooks/use_flyout_is_expandable';
|
||||
import { useDocumentDetailsContext } from '../shared/context';
|
||||
import type { DocumentDetailsProps } from '../shared/types';
|
||||
import { PanelHeader } from '../right/header';
|
||||
import { PanelContent } from '../right/content';
|
||||
import { PreviewPanelFooter } from './footer';
|
||||
import type { RightPanelTabType } from '../right/tabs';
|
||||
|
||||
export const ALERT_PREVIEW_BANNER = {
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.correlations.alertPreviewTitle',
|
||||
{
|
||||
defaultMessage: 'Preview alert details',
|
||||
}
|
||||
),
|
||||
backgroundColor: 'warning',
|
||||
textColor: 'warning',
|
||||
};
|
||||
|
||||
/**
|
||||
* Panel to be displayed in the document details expandable flyout on top of right section
|
||||
*/
|
||||
export const PreviewPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) => {
|
||||
const { openPreviewPanel } = useExpandableFlyoutApi();
|
||||
const { eventId, indexName, scopeId, getFieldsData, dataAsNestedObject } =
|
||||
useDocumentDetailsContext();
|
||||
const flyoutIsExpandable = useFlyoutIsExpandable({ getFieldsData, dataAsNestedObject });
|
||||
|
||||
const { tabsDisplayed, selectedTabId } = useTabs({ flyoutIsExpandable, path });
|
||||
|
||||
const setSelectedTabId = (tabId: RightPanelTabType['id']) => {
|
||||
openPreviewPanel({
|
||||
id: DocumentDetailsPreviewPanelKey,
|
||||
path: {
|
||||
tab: tabId,
|
||||
},
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
isPreviewMode: true,
|
||||
banner: ALERT_PREVIEW_BANNER,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelHeader
|
||||
tabs={tabsDisplayed}
|
||||
selectedTabId={selectedTabId}
|
||||
setSelectedTabId={setSelectedTabId}
|
||||
style={{ marginTop: '-15px' }}
|
||||
/>
|
||||
<PanelContent tabs={tabsDisplayed} selectedTabId={selectedTabId} />
|
||||
<PreviewPanelFooter />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PreviewPanel.displayName = 'PreviewPanel';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { PREFIX } from '../../shared/test_ids';
|
||||
|
||||
export const PREVIEW_FOOTER_TEST_ID = `${PREFIX}PreviewFooter` as const;
|
||||
export const PREVIEW_FOOTER_LINK_TEST_ID = `${PREVIEW_FOOTER_TEST_ID}Link` as const;
|
|
@ -141,6 +141,26 @@ describe('<CorrelationsOverview />', () => {
|
|||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render link when isPreviewMode is true', () => {
|
||||
jest
|
||||
.mocked(useShowRelatedAlertsByAncestry)
|
||||
.mockReturnValue({ show: false, documentId: 'event-id' });
|
||||
jest
|
||||
.mocked(useShowRelatedAlertsBySameSourceEvent)
|
||||
.mockReturnValue({ show: false, originalEventId });
|
||||
jest.mocked(useShowRelatedAlertsBySession).mockReturnValue({ show: false });
|
||||
jest.mocked(useShowRelatedCases).mockReturnValue(false);
|
||||
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
renderCorrelationsOverview({ ...panelContextValue, isPreviewMode: true })
|
||||
);
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_LINK_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_TEXT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show component with all rows in expandable panel', () => {
|
||||
jest
|
||||
.mocked(useShowRelatedAlertsByAncestry)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -42,8 +42,15 @@ import {
|
|||
* and the SummaryPanel component for data rendering.
|
||||
*/
|
||||
export const CorrelationsOverview: React.FC = () => {
|
||||
const { dataAsNestedObject, eventId, indexName, getFieldsData, scopeId, isPreview } =
|
||||
useDocumentDetailsContext();
|
||||
const {
|
||||
dataAsNestedObject,
|
||||
eventId,
|
||||
indexName,
|
||||
getFieldsData,
|
||||
scopeId,
|
||||
isPreview,
|
||||
isPreviewMode,
|
||||
} = useDocumentDetailsContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
const { isTourShown, activeStep } = useTourContext();
|
||||
|
||||
|
@ -95,6 +102,22 @@ export const CorrelationsOverview: React.FC = () => {
|
|||
|
||||
const ruleType = get(dataAsNestedObject, ALERT_RULE_TYPE)?.[0];
|
||||
|
||||
const link = useMemo(
|
||||
() =>
|
||||
!isPreviewMode
|
||||
? {
|
||||
callback: goToCorrelationsTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.correlations.overviewTooltip"
|
||||
defaultMessage="Show all correlations"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[isPreviewMode, goToCorrelationsTab]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
|
@ -104,16 +127,8 @@ export const CorrelationsOverview: React.FC = () => {
|
|||
defaultMessage="Correlations"
|
||||
/>
|
||||
),
|
||||
link: {
|
||||
callback: goToCorrelationsTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.correlations.overviewTooltip"
|
||||
defaultMessage="Show all correlations"
|
||||
/>
|
||||
),
|
||||
},
|
||||
iconType: 'arrowStart',
|
||||
link,
|
||||
iconType: !isPreviewMode ? 'arrowStart' : undefined,
|
||||
}}
|
||||
data-test-subj={CORRELATIONS_TEST_ID}
|
||||
>
|
||||
|
|
|
@ -97,6 +97,18 @@ describe('<EntitiesOverview />', () => {
|
|||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render link if isPreviewMode is true', () => {
|
||||
const { getByTestId, queryByTestId } = renderEntitiesOverview({
|
||||
...mockContextValue,
|
||||
isPreviewMode: true,
|
||||
});
|
||||
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_LINK_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_TEXT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render user and host', () => {
|
||||
const { getByTestId, queryByText } = renderEntitiesOverview(mockContextValue);
|
||||
expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -23,7 +23,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
|
|||
* Entities section under Insights section, overview tab. It contains a preview of host and user information.
|
||||
*/
|
||||
export const EntitiesOverview: React.FC = () => {
|
||||
const { eventId, getFieldsData, indexName, scopeId } = useDocumentDetailsContext();
|
||||
const { eventId, getFieldsData, indexName, scopeId, isPreviewMode } = useDocumentDetailsContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
const hostName = getField(getFieldsData('host.name'));
|
||||
const userName = getField(getFieldsData('user.name'));
|
||||
|
@ -43,6 +43,22 @@ export const EntitiesOverview: React.FC = () => {
|
|||
});
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
const link = useMemo(
|
||||
() =>
|
||||
!isPreviewMode
|
||||
? {
|
||||
callback: goToEntitiesTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.entities.entitiesTooltip"
|
||||
defaultMessage="Show all entities"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[goToEntitiesTab, isPreviewMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpandablePanel
|
||||
|
@ -53,16 +69,8 @@ export const EntitiesOverview: React.FC = () => {
|
|||
defaultMessage="Entities"
|
||||
/>
|
||||
),
|
||||
link: {
|
||||
callback: goToEntitiesTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.entities.entitiesTooltip"
|
||||
defaultMessage="Show all entities"
|
||||
/>
|
||||
),
|
||||
},
|
||||
iconType: 'arrowStart',
|
||||
link,
|
||||
iconType: !isPreviewMode ? 'arrowStart' : undefined,
|
||||
}}
|
||||
data-test-subj={INSIGHTS_ENTITIES_TEST_ID}
|
||||
>
|
||||
|
|
|
@ -27,8 +27,9 @@ jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn() }
|
|||
|
||||
const mockFlyoutContextValue = { openLeftPanel: jest.fn() };
|
||||
|
||||
const NO_DATA_MESSAGE = 'Investigation guideThere’s no investigation guide for this rule.';
|
||||
const NO_DATA_MESSAGE = "Investigation guideThere's no investigation guide for this rule.";
|
||||
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';
|
||||
const OPEN_FLYOUT_MESSAGE = 'Open alert details to access investigation guides.';
|
||||
|
||||
const renderInvestigationGuide = () =>
|
||||
render(
|
||||
|
@ -107,6 +108,12 @@ describe('<InvestigationGuide />', () => {
|
|||
});
|
||||
|
||||
it('should render preview message when flyout is in preview', () => {
|
||||
(useInvestigationGuide as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
basicAlertData: { ruleId: 'ruleId' },
|
||||
ruleNote: 'test note',
|
||||
});
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<DocumentDetailsContext.Provider value={{ ...mockContextValue, isPreview: true }}>
|
||||
|
@ -119,6 +126,19 @@ describe('<InvestigationGuide />', () => {
|
|||
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
|
||||
});
|
||||
|
||||
it('should render open flyout message if isPreviewMode is true', () => {
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<DocumentDetailsContext.Provider value={{ ...mockContextValue, isPreviewMode: true }}>
|
||||
<InvestigationGuide />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(OPEN_FLYOUT_MESSAGE);
|
||||
});
|
||||
|
||||
it('should navigate to investigation guide when clicking on button', () => {
|
||||
(useInvestigationGuide as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSkeletonText } from '@elastic/eui';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
*/
|
||||
export const InvestigationGuide: React.FC = () => {
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, isPreview } =
|
||||
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, isPreview, isPreviewMode } =
|
||||
useDocumentDetailsContext();
|
||||
|
||||
const { loading, error, basicAlertData, ruleNote } = useInvestigationGuide({
|
||||
|
@ -46,6 +46,11 @@ export const InvestigationGuide: React.FC = () => {
|
|||
});
|
||||
}, [eventId, indexName, openLeftPanel, scopeId]);
|
||||
|
||||
const hasInvesigationGuide = useMemo(
|
||||
() => !error && basicAlertData && basicAlertData.ruleId && ruleNote,
|
||||
[error, basicAlertData, ruleNote]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
|
||||
<EuiFlexItem>
|
||||
|
@ -71,7 +76,12 @@ export const InvestigationGuide: React.FC = () => {
|
|||
{ defaultMessage: 'investigation guide' }
|
||||
)}
|
||||
/>
|
||||
) : !error && basicAlertData.ruleId && ruleNote ? (
|
||||
) : hasInvesigationGuide && isPreviewMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.investigation.investigationGuide.openFlyoutMessage"
|
||||
defaultMessage="Open alert details to access investigation guides."
|
||||
/>
|
||||
) : hasInvesigationGuide ? (
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
onClick={goToInvestigationsTab}
|
||||
|
@ -93,7 +103,7 @@ export const InvestigationGuide: React.FC = () => {
|
|||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.investigation.investigationGuide.noDataDescription"
|
||||
defaultMessage="There’s no investigation guide for this rule."
|
||||
defaultMessage="There's no investigation guide for this rule."
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -72,6 +72,23 @@ describe('<PrevalenceOverview />', () => {
|
|||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render link and icon if isPreviewMode is true', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = renderPrevalenceOverview({
|
||||
...mockContextValue,
|
||||
isPreviewMode: true,
|
||||
});
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_LINK_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_TEXT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading', () => {
|
||||
(usePrevalence as jest.Mock).mockReturnValue({
|
||||
loading: true,
|
||||
|
|
|
@ -28,8 +28,14 @@ const DEFAULT_TO = 'now';
|
|||
* The component fetches the necessary data at once. The loading and error states are handled by the ExpandablePanel component.
|
||||
*/
|
||||
export const PrevalenceOverview: FC = () => {
|
||||
const { eventId, indexName, dataFormattedForFieldBrowser, scopeId, investigationFields } =
|
||||
useDocumentDetailsContext();
|
||||
const {
|
||||
eventId,
|
||||
indexName,
|
||||
dataFormattedForFieldBrowser,
|
||||
scopeId,
|
||||
investigationFields,
|
||||
isPreviewMode,
|
||||
} = useDocumentDetailsContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
|
||||
const goPrevalenceTab = useCallback(() => {
|
||||
|
@ -67,6 +73,21 @@ export const PrevalenceOverview: FC = () => {
|
|||
),
|
||||
[data]
|
||||
);
|
||||
const link = useMemo(
|
||||
() =>
|
||||
!isPreviewMode
|
||||
? {
|
||||
callback: goPrevalenceTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTooltip"
|
||||
defaultMessage="Show all prevalence"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[goPrevalenceTab, isPreviewMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
|
@ -77,16 +98,8 @@ export const PrevalenceOverview: FC = () => {
|
|||
defaultMessage="Prevalence"
|
||||
/>
|
||||
),
|
||||
link: {
|
||||
callback: goPrevalenceTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.prevalence.prevalenceTooltip"
|
||||
defaultMessage="Show all prevalence"
|
||||
/>
|
||||
),
|
||||
},
|
||||
iconType: 'arrowStart',
|
||||
link,
|
||||
iconType: !isPreviewMode ? 'arrowStart' : undefined,
|
||||
}}
|
||||
content={{ loading, error }}
|
||||
data-test-subj={PREVALENCE_TEST_ID}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { useExpandSection } from '../hooks/use_expand_section';
|
|||
jest.mock('../hooks/use_expand_section');
|
||||
|
||||
const PREVIEW_MESSAGE = 'Response is not available in alert preview.';
|
||||
const OPEN_FLYOUT_MESSAGE = 'Open alert details to access response actions.';
|
||||
|
||||
const renderResponseSection = () =>
|
||||
render(
|
||||
|
@ -99,6 +100,21 @@ describe('<ResponseSection />', () => {
|
|||
expect(getByTestId(RESPONSE_SECTION_CONTENT_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
|
||||
});
|
||||
|
||||
it('should render open details flyout message if flyout is in preview', () => {
|
||||
(useExpandSection as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<TestProvider>
|
||||
<DocumentDetailsContext.Provider value={{ ...mockContextValue, isPreviewMode: true }}>
|
||||
<ResponseSection />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(getByTestId(RESPONSE_SECTION_CONTENT_TEST_ID)).toHaveTextContent(OPEN_FLYOUT_MESSAGE);
|
||||
});
|
||||
|
||||
it('should render empty component if document is not signal', () => {
|
||||
(useExpandSection as jest.Mock).mockReturnValue(true);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ const KEY = 'response';
|
|||
* Most bottom section of the overview tab. It contains a summary of the response tab.
|
||||
*/
|
||||
export const ResponseSection = memo(() => {
|
||||
const { isPreview, getFieldsData } = useDocumentDetailsContext();
|
||||
const { isPreview, getFieldsData, isPreviewMode } = useDocumentDetailsContext();
|
||||
|
||||
const expanded = useExpandSection({ title: KEY, defaultValue: false });
|
||||
|
||||
|
@ -47,6 +47,11 @@ export const ResponseSection = memo(() => {
|
|||
id="xpack.securitySolution.flyout.right.response.previewMessage"
|
||||
defaultMessage="Response is not available in alert preview."
|
||||
/>
|
||||
) : isPreviewMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.response.openFlyoutMessage"
|
||||
defaultMessage="Open alert details to access response actions."
|
||||
/>
|
||||
) : (
|
||||
<ResponseButton />
|
||||
)}
|
||||
|
|
|
@ -85,6 +85,21 @@ describe('<ThreatIntelligenceOverview />', () => {
|
|||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render link if isPrenviewMode is true', () => {
|
||||
(useFetchThreatIntelligence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
renderThreatIntelligenceOverview({ ...panelContextValue, isPreviewMode: true })
|
||||
);
|
||||
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_LINK_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_TEXT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render 1 match detected and 1 field enriched', () => {
|
||||
(useFetchThreatIntelligence as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -25,7 +25,8 @@ import { THREAT_INTELLIGENCE_TAB_ID } from '../../left/components/threat_intelli
|
|||
* and the SummaryPanel component for data rendering.
|
||||
*/
|
||||
export const ThreatIntelligenceOverview: FC = () => {
|
||||
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser } = useDocumentDetailsContext();
|
||||
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, isPreviewMode } =
|
||||
useDocumentDetailsContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutApi();
|
||||
|
||||
const goToThreatIntelligenceTab = useCallback(() => {
|
||||
|
@ -47,6 +48,22 @@ export const ThreatIntelligenceOverview: FC = () => {
|
|||
dataFormattedForFieldBrowser,
|
||||
});
|
||||
|
||||
const link = useMemo(
|
||||
() =>
|
||||
!isPreviewMode
|
||||
? {
|
||||
callback: goToThreatIntelligenceTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.threatIntelligence.threatIntelligenceTooltip"
|
||||
defaultMessage="Show all threat intelligence"
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
[isPreviewMode, goToThreatIntelligenceTab]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
|
@ -56,16 +73,8 @@ export const ThreatIntelligenceOverview: FC = () => {
|
|||
defaultMessage="Threat intelligence"
|
||||
/>
|
||||
),
|
||||
link: {
|
||||
callback: goToThreatIntelligenceTab,
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.insights.threatIntelligence.threatIntelligenceTooltip"
|
||||
defaultMessage="Show all threat intelligence"
|
||||
/>
|
||||
),
|
||||
},
|
||||
iconType: 'arrowStart',
|
||||
link,
|
||||
iconType: !isPreviewMode ? 'arrowStart' : undefined,
|
||||
}}
|
||||
data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}
|
||||
content={{ loading }}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiFlyoutHeader } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiTab } from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
|
@ -23,7 +24,7 @@ import {
|
|||
} from '../../../common/components/guided_onboarding_tour/tour_config';
|
||||
import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step';
|
||||
|
||||
export interface PanelHeaderProps {
|
||||
export interface PanelHeaderProps extends React.ComponentProps<typeof EuiFlyoutHeader> {
|
||||
/**
|
||||
* Id of the tab selected in the parent component to display its content
|
||||
*/
|
||||
|
@ -40,7 +41,7 @@ export interface PanelHeaderProps {
|
|||
}
|
||||
|
||||
export const PanelHeader: FC<PanelHeaderProps> = memo(
|
||||
({ selectedTabId, setSelectedTabId, tabs }) => {
|
||||
({ selectedTabId, setSelectedTabId, tabs, ...flyoutHeaderProps }) => {
|
||||
const { dataFormattedForFieldBrowser } = useDocumentDetailsContext();
|
||||
const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
const onSelectedTabChanged = (id: RightPanelPaths) => setSelectedTabId(id);
|
||||
|
@ -88,7 +89,7 @@ export const PanelHeader: FC<PanelHeaderProps> = memo(
|
|||
);
|
||||
|
||||
return (
|
||||
<FlyoutHeader>
|
||||
<FlyoutHeader {...flyoutHeaderProps}>
|
||||
{isAlert ? <AlertHeaderTitle /> : <EventHeaderTitle />}
|
||||
<EuiSpacer size="m" />
|
||||
<FlyoutHeaderTabs>{renderTabs}</FlyoutHeaderTabs>
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('<RulePreviewFooter />', () => {
|
|||
expect(getByTestId(RULE_OVERVIEW_FOOTER_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID)).toHaveTextContent(
|
||||
'Show rule details'
|
||||
'Show full rule details'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export const RuleFooter = memo(() => {
|
|||
data-test-subj={RULE_OVERVIEW_NAVIGATE_TO_RULE_TEST_ID}
|
||||
>
|
||||
{i18n.translate('xpack.securitySolution.flyout.preview.rule.viewDetailsLabel', {
|
||||
defaultMessage: 'Show rule details',
|
||||
defaultMessage: 'Show full rule details',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { memo } from 'react';
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { EuiFlyoutBody } from '@elastic/eui';
|
||||
import { FlyoutBody } from '../../shared/components/flyout_body';
|
||||
import type { DocumentDetailsRuleOverviewPanelKey } from '../shared/constants/panel_keys';
|
||||
import { RuleOverview } from './components/rule_overview';
|
||||
import { RuleFooter } from './components/footer';
|
||||
|
@ -25,11 +25,11 @@ export interface RuleOverviewPanelProps extends FlyoutPanelProps {
|
|||
export const RuleOverviewPanel: React.FC = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutBody>
|
||||
<FlyoutBody>
|
||||
<div style={{ marginTop: '-15px' }}>
|
||||
<RuleOverview />
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</FlyoutBody>
|
||||
<RuleFooter />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -60,11 +60,18 @@ export interface DocumentDetailsContext {
|
|||
*/
|
||||
getFieldsData: GetFieldsData;
|
||||
/**
|
||||
* Boolean to indicate whether it is a preview flyout
|
||||
* Boolean to indicate whether flyout is opened in rule preview
|
||||
*/
|
||||
isPreview: boolean;
|
||||
/**
|
||||
* Boolean to indicate whether it is a preview panel
|
||||
*/
|
||||
isPreviewMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A context provider shared by the right, left and preview panels in expandable document details flyout
|
||||
*/
|
||||
export const DocumentDetailsContext = createContext<DocumentDetailsContext | undefined>(undefined);
|
||||
|
||||
export type DocumentDetailsProviderProps = {
|
||||
|
@ -75,7 +82,7 @@ export type DocumentDetailsProviderProps = {
|
|||
} & Partial<DocumentDetailsProps['params']>;
|
||||
|
||||
export const DocumentDetailsProvider = memo(
|
||||
({ id, indexName, scopeId, children }: DocumentDetailsProviderProps) => {
|
||||
({ id, indexName, scopeId, isPreviewMode, children }: DocumentDetailsProviderProps) => {
|
||||
const {
|
||||
browserFields,
|
||||
dataAsNestedObject,
|
||||
|
@ -109,6 +116,7 @@ export const DocumentDetailsProvider = memo(
|
|||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
isPreview: scopeId === TableId.rulePreview,
|
||||
isPreviewMode: Boolean(isPreviewMode),
|
||||
}
|
||||
: undefined,
|
||||
[
|
||||
|
@ -122,6 +130,7 @@ export const DocumentDetailsProvider = memo(
|
|||
searchHit,
|
||||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
isPreviewMode,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -27,4 +27,5 @@ export const mockContextValue: DocumentDetailsContext = {
|
|||
investigationFields: [],
|
||||
refetchFlyoutData: jest.fn(),
|
||||
isPreview: false,
|
||||
isPreviewMode: false,
|
||||
};
|
||||
|
|
|
@ -18,5 +18,6 @@ export interface DocumentDetailsProps extends FlyoutPanelProps {
|
|||
id: string;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
isPreviewMode?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
DocumentDetailsIsolateHostPanelKey,
|
||||
DocumentDetailsLeftPanelKey,
|
||||
DocumentDetailsRightPanelKey,
|
||||
DocumentDetailsPreviewPanelKey,
|
||||
DocumentDetailsAlertReasonPanelKey,
|
||||
DocumentDetailsRuleOverviewPanelKey,
|
||||
} from './document_details/shared/constants/panel_keys';
|
||||
|
@ -22,6 +23,7 @@ import type { DocumentDetailsProps } from './document_details/shared/types';
|
|||
import { DocumentDetailsProvider } from './document_details/shared/context';
|
||||
import { RightPanel } from './document_details/right';
|
||||
import { LeftPanel } from './document_details/left';
|
||||
import { PreviewPanel } from './document_details/preview';
|
||||
import type { AlertReasonPanelProps } from './document_details/alert_reason';
|
||||
import { AlertReasonPanel } from './document_details/alert_reason';
|
||||
import { AlertReasonPanelProvider } from './document_details/alert_reason/context';
|
||||
|
@ -58,6 +60,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
</DocumentDetailsProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DocumentDetailsPreviewPanelKey,
|
||||
component: (props) => (
|
||||
<DocumentDetailsProvider {...(props as DocumentDetailsProps).params}>
|
||||
<PreviewPanel path={props.path as DocumentDetailsProps['path']} />
|
||||
</DocumentDetailsProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: DocumentDetailsAlertReasonPanelKey,
|
||||
component: (props) => (
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { FC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlyoutBody, EuiPanel } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
interface FlyoutBodyProps extends React.ComponentProps<typeof EuiFlyoutBody> {
|
||||
children: React.ReactNode;
|
||||
|
@ -18,7 +19,16 @@ interface FlyoutBodyProps extends React.ComponentProps<typeof EuiFlyoutBody> {
|
|||
*/
|
||||
export const FlyoutBody: FC<FlyoutBodyProps> = memo(({ children, ...flyoutBodyProps }) => {
|
||||
return (
|
||||
<EuiFlyoutBody {...flyoutBodyProps}>
|
||||
<EuiFlyoutBody
|
||||
{...flyoutBodyProps}
|
||||
css={css`
|
||||
.euiFlyoutBody__overflow {
|
||||
// fix a bug with red overlay when position was not set
|
||||
// remove when changes in EUI are merged
|
||||
transform: translateZ(0);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiPanel hasShadow={false} color="transparent">
|
||||
{children}
|
||||
</EuiPanel>
|
||||
|
|
|
@ -120,7 +120,7 @@ describe(
|
|||
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_RULE_PREVIEW_FOOTER_LINK).should(
|
||||
'contain.text',
|
||||
'Show rule details'
|
||||
'Show full rule details'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue