[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:
christineweng 2024-07-02 09:41:37 -05:00 committed by GitHub
parent e105a34924
commit 7c6186ab91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 691 additions and 200 deletions

View file

@ -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}

View file

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

View file

@ -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={{

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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 =

View file

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

View file

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

View file

@ -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';

View file

@ -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;

View file

@ -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)

View file

@ -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}
>

View file

@ -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();

View file

@ -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}
>

View file

@ -27,8 +27,9 @@ jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn() }
const mockFlyoutContextValue = { openLeftPanel: jest.fn() };
const NO_DATA_MESSAGE = 'Investigation guideTheres 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,

View file

@ -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="Theres no investigation guide for this rule."
defaultMessage="There's no investigation guide for this rule."
/>
)}
</EuiFlexGroup>

View file

@ -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,

View file

@ -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}

View file

@ -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);

View file

@ -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 />
)}

View file

@ -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,

View file

@ -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 }}

View file

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

View file

@ -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'
);
});

View file

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

View file

@ -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 />
</>
);

View file

@ -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,
]
);

View file

@ -27,4 +27,5 @@ export const mockContextValue: DocumentDetailsContext = {
investigationFields: [],
refetchFlyoutData: jest.fn(),
isPreview: false,
isPreviewMode: false,
};

View file

@ -18,5 +18,6 @@ export interface DocumentDetailsProps extends FlyoutPanelProps {
id: string;
indexName: string;
scopeId: string;
isPreviewMode?: boolean;
};
}

View file

@ -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) => (

View file

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

View file

@ -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'
);
});
});