[8.10] [Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111) (#164735)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Security Solution] expandable flyout - hide visualize tab in left
section and open session view and analyzer in timeline
(#164111)](https://github.com/elastic/kibana/pull/164111)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Philippe
Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2023-08-24T15:11:00Z","message":"[Security
Solution] expandable flyout - hide visualize tab in left section and
open session view and analyzer in timeline
(#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Threat
Hunting:Investigations","v8.10.0","v8.11.0"],"number":164111,"url":"https://github.com/elastic/kibana/pull/164111","mergeCommit":{"message":"[Security
Solution] expandable flyout - hide visualize tab in left section and
open session view and analyzer in timeline
(#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164111","number":164111,"mergeCommit":{"message":"[Security
Solution] expandable flyout - hide visualize tab in left section and
open session view and analyzer in timeline
(#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df"}}]}]
BACKPORT-->

Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
Kibana Machine 2023-08-24 13:27:39 -04:00 committed by GitHub
parent 017fda6497
commit 070b357a67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 684 additions and 539 deletions

View file

@ -23,6 +23,7 @@ import {
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID,
CORRELATIONS_DETAILS_TEST_ID,
} from './test_ids';
import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session';
import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry';
@ -103,7 +104,7 @@ describe('CorrelationsDetails', () => {
expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
});
it('should render no section if show values are false', () => {
it('should render no section and show error message if show values are false', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: false, documentId: 'documentId', indices: ['index1'] });
@ -115,12 +116,13 @@ describe('CorrelationsDetails', () => {
.mockReturnValue({ show: false, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
const { queryByTestId } = renderCorrelationDetails();
const { getByTestId, queryByTestId } = renderCorrelationDetails();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(`${CORRELATIONS_DETAILS_TEST_ID}Error`)).toBeInTheDocument();
});
it('should render no section if values are null', () => {

View file

@ -7,6 +7,8 @@
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { CORRELATIONS_ERROR_MESSAGE } from './translations';
import { CORRELATIONS_DETAILS_TEST_ID } from './test_ids';
import { RelatedAlertsBySession } from './related_alerts_by_session';
import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event';
import { RelatedCases } from './related_cases';
@ -42,27 +44,38 @@ export const CorrelationsDetails: React.FC = () => {
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
const showCases = useShowRelatedCases();
const canShowAtLeastOneInsight =
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
return (
<>
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
{canShowAtLeastOneInsight ? (
<>
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
)}
<EuiSpacer />
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
)}
<EuiSpacer />
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
<EuiSpacer />
{showCases && <RelatedCases eventId={eventId} />}
</>
) : (
<div data-test-subj={`${CORRELATIONS_DETAILS_TEST_ID}Error`}>
{CORRELATIONS_ERROR_MESSAGE}
</div>
)}
<EuiSpacer />
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
)}
<EuiSpacer />
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
<EuiSpacer />
{showCases && <RelatedCases eventId={eventId} />}
</>
);
};

View file

@ -21,6 +21,13 @@ export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate(
}
);
export const CORRELATIONS_ERROR_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.correlationsErrorMessage',
{
defaultMessage: 'No correlations data available',
}
);
export const USER_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.userTitle', {
defaultMessage: 'User',
});

View file

@ -20,11 +20,11 @@ export interface PanelContentProps {
/**
* Document details expandable flyout left section. Appears after the user clicks on the expand details button in the right section.
* Will display the content of the visualize, investigation and insights tabs.
* Displays the content of investigation and insights tabs (visualize is hidden for 8.9).
*/
export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId }) => {
const selectedTabContent = useMemo(() => {
return tabs.find((tab) => tab.id === selectedTabId)?.content;
return tabs.filter((tab) => tab.visible).find((tab) => tab.id === selectedTabId)?.content;
}, [selectedTabId]);
return <EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>;

View file

@ -13,15 +13,26 @@ import type { LeftPanelPaths } from '.';
import { tabs } from './tabs';
export interface PanelHeaderProps {
/**
* Id of the tab selected in the parent component to display its content
*/
selectedTabId: LeftPanelPaths;
/**
* Callback to set the selected tab id in the parent component
* @param selected
*/
setSelectedTabId: (selected: LeftPanelPaths) => void;
handleOnEventClosed?: () => void;
}
export const PanelHeader: VFC<PanelHeaderProps> = memo(
({ selectedTabId, setSelectedTabId, handleOnEventClosed }) => {
const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id);
const renderTabs = tabs.map((tab, index) => (
/**
* Header at the top of the left section.
* Displays the investigation and insights tabs (visualize is hidden for 8.9).
*/
export const PanelHeader: VFC<PanelHeaderProps> = memo(({ selectedTabId, setSelectedTabId }) => {
const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id);
const renderTabs = tabs
.filter((tab) => tab.visible)
.map((tab, index) => (
<EuiTab
onClick={() => onSelectedTabChanged(tab.id)}
isSelected={tab.id === selectedTabId}
@ -32,20 +43,19 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(
</EuiTab>
));
return (
<EuiFlyoutHeader hasBorder>
<EuiTabs
size="l"
expand
css={css`
margin-bottom: -25px;
`}
>
{renderTabs}
</EuiTabs>
</EuiFlyoutHeader>
);
}
);
return (
<EuiFlyoutHeader hasBorder>
<EuiTabs
size="l"
expand
css={css`
margin-bottom: -25px;
`}
>
{renderTabs}
</EuiTabs>
</EuiFlyoutHeader>
);
});
PanelHeader.displayName = 'PanelHeader';

View file

@ -39,9 +39,10 @@ export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
const { eventId, indexName, scopeId } = useLeftPanelContext();
const selectedTabId = useMemo(() => {
const defaultTab = tabs[0].id;
const visibleTabs = tabs.filter((tab) => tab.visible);
const defaultTab = visibleTabs[0].id;
if (!path) return defaultTab;
return tabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
return visibleTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
}, [path]);
const setSelectedTabId = (tabId: LeftPanelTabsType[number]['id']) => {

View file

@ -24,6 +24,7 @@ export type LeftPanelTabsType = Array<{
'data-test-subj': string;
name: string;
content: React.ReactElement;
visible: boolean;
}>;
export const tabs: LeftPanelTabsType = [
@ -32,23 +33,27 @@ export const tabs: LeftPanelTabsType = [
'data-test-subj': VISUALIZE_TAB_TEST_ID,
name: VISUALIZE_TAB,
content: <VisualizeTab />,
visible: false,
},
{
id: 'insights',
'data-test-subj': INSIGHTS_TAB_TEST_ID,
name: INSIGHTS_TAB,
content: <InsightsTab />,
visible: true,
},
{
id: 'investigation',
'data-test-subj': INVESTIGATION_TAB_TEST_ID,
name: INVESTIGATIONS_TAB,
content: <InvestigationTab />,
visible: true,
},
{
id: 'response',
'data-test-subj': RESPONSE_TAB_TEST_ID,
name: RESPONSE_TAB,
content: <ResponseTab />,
visible: true,
},
];

View file

@ -15,7 +15,6 @@ import { RightPanelContext } from '../context';
import { AnalyzerPreview } from './analyzer_preview';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import * as mock from '../mocks/mock_analyzer_data';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../shared/components/test_ids';
jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({
useAlertPrevalenceFromProcessTree: jest.fn(),
@ -65,9 +64,7 @@ describe('<AnalyzerPreview />', () => {
documentId: 'ancestors-id',
indices: ['rule-parameters-index'],
});
expect(
wrapper.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('does not show analyzer preview when documentid and index are not present', () => {
@ -90,8 +87,6 @@ describe('<AnalyzerPreview />', () => {
documentId: '',
indices: [],
});
expect(
queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -4,13 +4,16 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { find } from 'lodash/fp';
import { EuiTreeView } from '@elastic/eui';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { getTreeNodes } from '../utils/analyzer_helpers';
import { ANALYZER_PREVIEW_TITLE } from './translations';
import { ANCESTOR_ID, RULE_PARAMETERS_INDEX } from '../../shared/constants/field_names';
import { useRightPanelContext } from '../context';
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import { AnalyzerTree } from './analyzer_tree';
import { isActiveTimeline } from '../../../helpers';
const CHILD_COUNT_LIMIT = 3;
@ -38,7 +41,7 @@ export const AnalyzerPreview: React.FC = () => {
const index = find({ category: 'kibana', field: RULE_PARAMETERS_INDEX }, data);
const indices = index?.values ?? [];
const { loading, error, statsNodes } = useAlertPrevalenceFromProcessTree({
const { statsNodes } = useAlertPrevalenceFromProcessTree({
isActiveTimeline: isActiveTimeline(scopeId),
documentId: processDocumentId,
indices,
@ -50,19 +53,24 @@ export const AnalyzerPreview: React.FC = () => {
}
}, [statsNodes, setCache]);
if (!documentId || !index) {
const items = useMemo(
() => getTreeNodes(cache.statsNodes ?? [], CHILD_COUNT_LIMIT, ANCESTOR_LEVEL, DESCENDANT_LEVEL),
[cache.statsNodes]
);
if (!documentId || !index || !items || items.length === 0) {
return null;
}
return (
<AnalyzerTree
statsNodes={cache.statsNodes}
loading={loading}
error={error}
childCountLimit={CHILD_COUNT_LIMIT}
ancestorLevel={ANCESTOR_LEVEL}
descendantLevel={DESCENDANT_LEVEL}
/>
<div data-test-subj={ANALYZER_PREVIEW_TEST_ID}>
<EuiTreeView
items={items}
display="compressed"
aria-label={ANALYZER_PREVIEW_TITLE}
showExpansionArrows
/>
</div>
);
};

View file

@ -0,0 +1,126 @@
/*
* 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 { render, screen } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import React from 'react';
import { RightPanelContext } from '../context';
import { AnalyzerPreviewContainer } from './analyzer_preview_container';
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import * as mock from '../mocks/mock_analyzer_data';
import {
EXPANDABLE_PANEL_CONTENT_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
} from '../../shared/components/test_ids';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_context';
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
jest.mock('../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver');
jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree');
jest.mock(
'../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'
);
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
const panelContextValue = {
dataAsNestedObject: null,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
} as unknown as RightPanelContext;
const TEST_ID = ANALYZER_PREVIEW_TEST_ID;
const ERROR_TEST_ID = `${ANALYZER_PREVIEW_TEST_ID}Error`;
const renderAnalyzerPreview = () =>
render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<AnalyzerPreviewContainer />
</RightPanelContext.Provider>
</TestProviders>
);
describe('AnalyzerPreviewContainer', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should render component and link in header', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
loading: false,
error: false,
alertIds: ['alertid'],
statsNodes: mock.mockStatsNodes,
});
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
investigateInTimelineAlertClick: jest.fn(),
});
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
expect(getByTestId(TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
});
it('should render error message and text in header', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(false);
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
investigateInTimelineAlertClick: jest.fn(),
});
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(ERROR_TEST_ID)).toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
});
it('should navigate to left section Visualize tab when clicking on title', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
loading: false,
error: false,
alertIds: ['alertid'],
statsNodes: mock.mockStatsNodes,
});
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
investigateInTimelineAlertClick: jest.fn(),
});
const { getByTestId } = renderAnalyzerPreview();
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({});
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click();
expect(investigateInTimelineAlertClick).toHaveBeenCalled();
});
});

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 React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { TimelineTabs } from '@kbn/securitysolution-data-table';
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions';
import { getScopedActions } from '../../../helpers';
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
import { useRightPanelContext } from '../context';
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { AnalyzerPreview } from './analyzer_preview';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ANALYZER_PREVIEW_ERROR, ANALYZER_PREVIEW_TITLE } from './translations';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
const timelineId = 'timeline-1';
/**
* Analyzer preview under Overview, Visualizations. It shows a tree representation of analyzer.
*/
export const AnalyzerPreviewContainer: React.FC = () => {
const { dataAsNestedObject } = useRightPanelContext();
// decide whether to show the analyzer preview or not
const isEnabled = isInvestigateInResolverActionEnabled(dataAsNestedObject || undefined);
const dispatch = useDispatch();
const { startTransaction } = useStartTransaction();
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
ecsRowData: dataAsNestedObject,
});
// open timeline to the analyzer tab because the expandable flyout left panel Visualize => Analyzer tab is not ready
const goToAnalyzerTab = useCallback(() => {
// open timeline
investigateInTimelineAlertClick();
// open analyzer tab
startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER });
const scopedActions = getScopedActions(timelineId);
if (scopedActions && dataAsNestedObject) {
dispatch(
scopedActions.updateGraphEventId({ id: timelineId, graphEventId: dataAsNestedObject._id })
);
}
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }));
}, [dataAsNestedObject, dispatch, investigateInTimelineAlertClick, startTransaction]);
return (
<ExpandablePanel
header={{
title: ANALYZER_PREVIEW_TITLE,
iconType: 'timeline',
...(isEnabled && { callback: goToAnalyzerTab }),
}}
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
>
{isEnabled ? (
<AnalyzerPreview />
) : (
<div data-test-subj={`${ANALYZER_PREVIEW_TEST_ID}Error`}>{ANALYZER_PREVIEW_ERROR}</div>
)}
</ExpandablePanel>
);
};
AnalyzerPreviewContainer.displayName = 'AnalyzerPreviewContainer';

View file

@ -1,111 +0,0 @@
/*
* 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 type { Story } from '@storybook/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { AnalyzerTree } from './analyzer_tree';
import * as mock from '../mocks/mock_analyzer_data';
import { RightPanelContext } from '../context';
export default {
component: AnalyzerTree,
title: 'Flyout/AnalyzerTree',
};
const defaultProps = {
loading: false,
error: false,
};
const flyoutContextValue = {
openLeftPanel: () => {},
} as unknown as ExpandableFlyoutContext;
const contextValue = {
eventId: 'eventId',
indexName: 'indexName',
scopeId: 'alerts-page',
} as unknown as RightPanelContext;
const wrapper = (children: React.ReactNode) => (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>{children}</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
export const Default: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodes} />);
};
export const SingleNode: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={[mock.mockStatsNode]} />);
};
export const ShowParent: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasParent} />);
};
export const ShowGrandparent: Story<void> = () => {
return wrapper(
<AnalyzerTree
{...defaultProps}
statsNodes={mock.mockStatsNodesHasGrandparent}
ancestorLevel={2}
/>
);
};
export const HideGrandparent: Story<void> = () => {
return wrapper(
<AnalyzerTree
{...defaultProps}
statsNodes={mock.mockStatsNodesHasGrandparent}
ancestorLevel={1}
/>
);
};
export const ShowChildren: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasChildren} />);
};
export const ShowOnlyOneChild: Story<void> = () => {
return wrapper(
<AnalyzerTree
{...defaultProps}
statsNodes={mock.mockStatsNodesHasChildren}
childCountLimit={1}
/>
);
};
export const ShowGrandchildren: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasChildren} />);
};
export const HideGrandchildren: Story<void> = () => {
return wrapper(
<AnalyzerTree
{...defaultProps}
statsNodes={mock.mockStatsNodesHasChildren}
descendantLevel={1}
/>
);
};
export const Loading: Story<void> = () => {
return wrapper(<AnalyzerTree loading={true} error={false} descendantLevel={3} />);
};
export const Error: Story<void> = () => {
return wrapper(<AnalyzerTree loading={false} error={true} descendantLevel={3} />);
};
export const Empty: Story<void> = () => {
return wrapper(<AnalyzerTree {...defaultProps} />);
};

View file

@ -1,112 +0,0 @@
/*
* 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 { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ANALYZE_GRAPH_ID } from '../../left/components/analyze_graph';
import { ANALYZER_PREVIEW_TITLE } from './translations';
import * as mock from '../mocks/mock_analyzer_data';
import type { AnalyzerTreeProps } from './analyzer_tree';
import { AnalyzerTree } from './analyzer_tree';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
import { RightPanelContext } from '../context';
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
import {
EXPANDABLE_PANEL_CONTENT_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
EXPANDABLE_PANEL_LOADING_TEST_ID,
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
} from '../../shared/components/test_ids';
const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
const defaultProps: AnalyzerTreeProps = {
statsNodes: mock.mockStatsNodes,
loading: false,
error: false,
};
const flyoutContextValue = {
openLeftPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {
eventId: 'event id',
indexName: 'indexName',
browserFields: {},
dataFormattedForFieldBrowser: [],
} as unknown as RightPanelContext;
const renderAnalyzerTree = (children: React.ReactNode) =>
render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContextValue}>
{children}
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
describe('<AnalyzerTree />', () => {
it('should render wrapper component', () => {
const { getByTestId, queryByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(TITLE_LINK_TEST_ID)).toBeInTheDocument();
expect(getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
});
it('should render the component when data is passed', () => {
const { getByTestId, getByText } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
expect(getByText(ANALYZER_PREVIEW_TITLE)).toBeInTheDocument();
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
});
it('should render blank when data is not passed', () => {
const { queryByTestId, queryByText } = renderAnalyzerTree(
<AnalyzerTree {...defaultProps} statsNodes={undefined} />
);
expect(queryByText(ANALYZER_PREVIEW_TITLE)).not.toBeInTheDocument();
expect(queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument();
});
it('should render loading spinner when loading is true', () => {
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} loading={true} />);
expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument();
});
it('should not render when error is true', () => {
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} error={true} />);
expect(getByTestId(CONTENT_TEST_ID)).toBeEmptyDOMElement();
});
it('should navigate to left section Visualize tab when clicking on title', () => {
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
getByTestId(TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: { tab: LeftPanelVisualizeTab, subTab: ANALYZE_GRAPH_ID },
params: {
id: panelContextValue.eventId,
indexName: panelContextValue.indexName,
scopeId: panelContextValue.scopeId,
},
});
});
});

View file

@ -1,102 +0,0 @@
/*
* 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, { useCallback, useMemo } from 'react';
import { EuiTreeView } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { useRightPanelContext } from '../context';
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
import { ANALYZER_PREVIEW_TITLE } from './translations';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ANALYZE_GRAPH_ID } from '../../left/components/analyze_graph';
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import { getTreeNodes } from '../utils/analyzer_helpers';
export interface AnalyzerTreeProps {
/**
* statsNode data from resolver tree api
*/
statsNodes?: StatsNode[];
/**
* Boolean value of whether data is in loading
*/
loading: boolean;
/**
* Boolean value of whether there is error in data fetching
*/
error: boolean;
/**
* Optional parameter to limit the number of child nodes to be displayed
*/
childCountLimit?: number;
/**
* Optional parameter to limit the depth of ancestors
*/
ancestorLevel?: number;
/**
* Optional parameter to limit the depth of descendants
*/
descendantLevel?: number;
}
/**
* Analyzer tree that represent a summary view of analyzer. It shows current process, and its parent and child processes
*/
export const AnalyzerTree: React.FC<AnalyzerTreeProps> = ({
statsNodes,
loading,
error,
childCountLimit = 3,
ancestorLevel = 1,
descendantLevel = 1,
}) => {
const { eventId, indexName, scopeId } = useRightPanelContext();
const { openLeftPanel } = useExpandableFlyoutContext();
const items = useMemo(
() => getTreeNodes(statsNodes ?? [], childCountLimit, ancestorLevel, descendantLevel),
[statsNodes, childCountLimit, ancestorLevel, descendantLevel]
);
const goToAnalyserTab = useCallback(() => {
openLeftPanel({
id: LeftPanelKey,
path: {
tab: LeftPanelVisualizeTab,
subTab: ANALYZE_GRAPH_ID,
},
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, openLeftPanel, indexName, scopeId]);
if (items && items.length !== 0) {
return (
<ExpandablePanel
header={{
title: ANALYZER_PREVIEW_TITLE,
callback: goToAnalyserTab,
iconType: 'arrowStart',
}}
content={{ loading, error }}
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
>
<EuiTreeView
items={items}
display="compressed"
aria-label={ANALYZER_PREVIEW_TITLE}
showExpansionArrows
/>
</ExpandablePanel>
);
}
return null;
};
AnalyzerTree.displayName = 'AnalyzerTree';

View file

@ -68,6 +68,7 @@ const RELATED_ALERTS_BY_SESSION_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
const RELATED_CASES_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID
);
const CORRELATIONS_ERROR_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Error`;
const panelContextValue = {
eventId: 'event id',
@ -139,7 +140,7 @@ describe('<CorrelationsOverview />', () => {
expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument();
});
it('should hide rows if show values are false', () => {
it('should hide rows and show error message if show values are false', () => {
jest
.mocked(useShowRelatedAlertsByAncestry)
.mockReturnValue({ show: false, documentId: 'documentId', indices: ['index1'] });
@ -151,11 +152,12 @@ describe('<CorrelationsOverview />', () => {
.mockReturnValue({ show: false, entityId: 'entityId' });
jest.mocked(useShowRelatedCases).mockReturnValue(false);
const { queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(CORRELATIONS_ERROR_TEST_ID)).toBeInTheDocument();
});
it('should hide rows if values are null', () => {

View file

@ -19,7 +19,7 @@ import { RelatedCases } from './related_cases';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { INSIGHTS_CORRELATIONS_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { CORRELATIONS_TITLE } from './translations';
import { CORRELATIONS_ERROR, CORRELATIONS_TITLE } from './translations';
import { LeftPanelKey, LeftPanelInsightsTab } from '../../left';
import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details';
@ -69,6 +69,9 @@ export const CorrelationsOverview: React.FC = () => {
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
const showCases = useShowRelatedCases();
const canShowAtLeastOneInsight =
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
return (
<ExpandablePanel
header={{
@ -78,18 +81,22 @@ export const CorrelationsOverview: React.FC = () => {
}}
data-test-subj={INSIGHTS_CORRELATIONS_TEST_ID}
>
<EuiFlexGroup direction="column" gutterSize="none">
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
)}
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
)}
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
{showCases && <RelatedCases eventId={eventId} />}
</EuiFlexGroup>
{canShowAtLeastOneInsight ? (
<EuiFlexGroup direction="column" gutterSize="none">
{showAlertsByAncestry && documentId && indices && (
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
)}
{showSameSourceAlerts && originalEventId && (
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
)}
{showAlertsBySession && entityId && (
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
)}
{showCases && <RelatedCases eventId={eventId} />}
</EuiFlexGroup>
) : (
<div data-test-subj={`${INSIGHTS_CORRELATIONS_TEST_ID}Error`}>{CORRELATIONS_ERROR}</div>
)}
</ExpandablePanel>
);
};

View file

@ -12,25 +12,9 @@ import { TestProviders } from '../../../common/mock';
import React from 'react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
import { SESSION_VIEW_ID } from '../../left/components/session_view';
import {
EXPANDABLE_PANEL_CONTENT_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
} from '../../shared/components/test_ids';
jest.mock('../hooks/use_process_data');
const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID);
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID);
const CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID);
const flyoutContextValue = {
openLeftPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
@ -58,26 +42,6 @@ describe('SessionPreview', () => {
jest.resetAllMocks();
});
it('should render wrapper component', () => {
jest.mocked(useProcessData).mockReturnValue({
processName: 'process1',
userName: 'user1',
startAt: '2022-01-01T00:00:00.000Z',
ruleName: 'rule1',
ruleId: 'id',
workdir: '/path/to/workdir',
command: 'command1',
});
renderSessionPreview();
expect(screen.queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
expect(screen.getByTestId(TITLE_LINK_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
expect(screen.queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
});
it('renders session preview with all data', () => {
jest.mocked(useProcessData).mockReturnValue({
processName: 'process1',
@ -95,7 +59,7 @@ describe('SessionPreview', () => {
expect(screen.getByText('started')).toBeInTheDocument();
expect(screen.getByText('process1')).toBeInTheDocument();
expect(screen.getByText('at')).toBeInTheDocument();
expect(screen.getByText('2022-01-01T00:00:00Z')).toBeInTheDocument();
expect(screen.getByText('Jan 1, 2022 @ 00:00:00.000')).toBeInTheDocument();
expect(screen.getByText('with rule')).toBeInTheDocument();
expect(screen.getByText('rule1')).toBeInTheDocument();
expect(screen.getByText('by')).toBeInTheDocument();
@ -122,29 +86,4 @@ describe('SessionPreview', () => {
expect(screen.queryByText('with rule')).not.toBeInTheDocument();
expect(screen.queryByText('by')).not.toBeInTheDocument();
});
it('should navigate to left section Visualize tab when clicking on title', () => {
jest.mocked(useProcessData).mockReturnValue({
processName: 'process1',
userName: 'user1',
startAt: '2022-01-01T00:00:00.000Z',
ruleName: 'rule1',
ruleId: 'id',
workdir: '/path/to/workdir',
command: 'command1',
});
const { getByTestId } = renderSessionPreview();
getByTestId(TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: { tab: LeftPanelVisualizeTab, subTab: SESSION_VIEW_ID },
params: {
id: panelContextValue.eventId,
indexName: panelContextValue.indexName,
scopeId: panelContextValue.scopeId,
},
});
});
});

View file

@ -6,24 +6,19 @@
*/
import { EuiCode, EuiIcon, useEuiTheme } from '@elastic/eui';
import React, { useMemo, type FC, useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import React, { useMemo, type FC } from 'react';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
import { useProcessData } from '../hooks/use_process_data';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
SESSION_PREVIEW_COMMAND_TEXT,
SESSION_PREVIEW_PROCESS_TEXT,
SESSION_PREVIEW_RULE_TEXT,
SESSION_PREVIEW_TIME_TEXT,
SESSION_PREVIEW_TITLE,
} from './translations';
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
import { RenderRuleName } from '../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
import { SESSION_VIEW_ID } from '../../left/components/session_view';
/**
* One-off helper to make sure that inline values are rendered consistently
@ -42,26 +37,10 @@ const ValueContainer: FC<{ text?: string }> = ({ text, children }) => (
);
/**
* Renders session preview under visualistions section in the flyout right EuiPanel
* Renders session preview under Visualizations section in the flyout right EuiPanel
*/
export const SessionPreview: FC = () => {
const { eventId, indexName, scopeId } = useRightPanelContext();
const { openLeftPanel } = useExpandableFlyoutContext();
const goToSessionViewTab = useCallback(() => {
openLeftPanel({
id: LeftPanelKey,
path: {
tab: LeftPanelVisualizeTab,
subTab: SESSION_VIEW_ID,
},
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, openLeftPanel, indexName, scopeId]);
const { eventId, scopeId } = useRightPanelContext();
const { processName, userName, startAt, ruleName, ruleId, workdir, command } = useProcessData();
const { euiTheme } = useEuiTheme();
@ -124,25 +103,16 @@ export const SessionPreview: FC = () => {
}, [command, workdir]);
return (
<ExpandablePanel
header={{
title: SESSION_PREVIEW_TITLE,
iconType: 'arrowStart',
callback: goToSessionViewTab,
}}
data-test-subj={SESSION_PREVIEW_TEST_ID}
>
<div>
<ValueContainer>
<EuiIcon type="user" />
&nbsp;
<span style={emphasisStyles}>{userName}</span>
</ValueContainer>
{processNameFragment}
{timeFragment}
{ruleFragment}
{commandFragment}
</div>
</ExpandablePanel>
<div data-test-subj={SESSION_PREVIEW_TEST_ID}>
<ValueContainer>
<EuiIcon type="user" />
&nbsp;
<span style={emphasisStyles}>{userName}</span>
</ValueContainer>
{processNameFragment}
{timeFragment}
{ruleFragment}
{commandFragment}
</div>
);
};

View file

@ -0,0 +1,110 @@
/*
* 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 { render, screen } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import React from 'react';
import { RightPanelContext } from '../context';
import { SessionPreviewContainer } from './session_preview_container';
import { useSessionPreview } from '../hooks/use_session_preview';
import { useLicense } from '../../../common/hooks/use_license';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
EXPANDABLE_PANEL_CONTENT_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
} from '../../shared/components/test_ids';
import { mockGetFieldsData } from '../mocks/mock_context';
jest.mock('../hooks/use_session_preview');
jest.mock('../../../common/hooks/use_license');
const panelContextValue = {
dataAsNestedObject: null,
getFieldsData: mockGetFieldsData,
} as unknown as RightPanelContext;
const sessionViewConfig = {
index: {},
sessionEntityId: 'sessionEntityId',
sessionStartTime: 'sessionStartTime',
};
const TEST_ID = SESSION_PREVIEW_TEST_ID;
const ERROR_TEST_ID = `${SESSION_PREVIEW_TEST_ID}Error`;
const UPSELL_TEST_ID = `${SESSION_PREVIEW_TEST_ID}UpSell`;
const renderSessionPreview = () =>
render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<SessionPreviewContainer />
</RightPanelContext.Provider>
</TestProviders>
);
describe('SessionPreviewContainer', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should render component and link in header', () => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview();
expect(getByTestId(TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
});
it('should render error message and text in header if no sessionConfig', () => {
(useSessionPreview as jest.Mock).mockReturnValue(null);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview();
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(ERROR_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
});
it('should render error message and text in header if no correct license', () => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => false });
const { getByTestId, queryByTestId } = renderSessionPreview();
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(UPSELL_TEST_ID)).toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,86 @@
/*
* 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, { type FC, useCallback } from 'react';
import { TimelineTabs } from '@kbn/securitysolution-data-table';
import { useDispatch } from 'react-redux';
import { useLicense } from '../../../common/hooks/use_license';
import { SessionPreview } from './session_preview';
import { useSessionPreview } from '../hooks/use_session_preview';
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
import { useRightPanelContext } from '../context';
import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
SESSION_PREVIEW_ERROR,
SESSION_PREVIEW_TITLE,
SESSION_PREVIEW_UPSELL,
} from './translations';
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
import { getScopedActions } from '../../../helpers';
const timelineId = 'timeline-1';
/**
* Checks if the SessionView component is available, if so render it or else render an error message
*/
export const SessionPreviewContainer: FC = () => {
const { dataAsNestedObject, getFieldsData } = useRightPanelContext();
// decide whether to show the session view or not
const sessionViewConfig = useSessionPreview({ getFieldsData });
const isEnterprisePlus = useLicense().isEnterprise();
const isEnabled = sessionViewConfig && isEnterprisePlus;
const dispatch = useDispatch();
const { startTransaction } = useStartTransaction();
const scopedActions = getScopedActions(timelineId);
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
ecsRowData: dataAsNestedObject,
});
const goToSessionViewTab = useCallback(() => {
// open timeline
investigateInTimelineAlertClick();
// open session view tab
startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW });
if (sessionViewConfig !== null) {
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session }));
if (scopedActions) {
dispatch(scopedActions.updateSessionViewConfig({ id: timelineId, sessionViewConfig }));
}
}
}, [
dispatch,
investigateInTimelineAlertClick,
scopedActions,
sessionViewConfig,
startTransaction,
]);
const noSessionMessage = !isEnterprisePlus ? (
<div data-test-subj={`${SESSION_PREVIEW_TEST_ID}UpSell`}>{SESSION_PREVIEW_UPSELL}</div>
) : !sessionViewConfig ? (
<div data-test-subj={`${SESSION_PREVIEW_TEST_ID}Error`}>{SESSION_PREVIEW_ERROR}</div>
) : null;
return (
<ExpandablePanel
header={{
title: SESSION_PREVIEW_TITLE,
iconType: 'timeline',
...(isEnabled && { callback: goToSessionViewTab }),
}}
data-test-subj={SESSION_PREVIEW_TEST_ID}
>
{isEnabled ? <SessionPreview /> : noSessionMessage}
</ExpandablePanel>
);
};

View file

@ -80,8 +80,6 @@ export const SUMMARY_ROW_VALUE_TEST_ID = (dataTestSubj: string) => `${dataTestSu
/* Insights Entities */
export const INSIGHTS_ENTITIES_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsEntities';
export const TECHNICAL_PREVIEW_ICON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutTechnicalPreviewIcon';
export const ENTITIES_USER_OVERVIEW_TEST_ID =
'securitySolutionDocumentDetailsFlyoutEntitiesUserOverview';
export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link`;
@ -126,13 +124,7 @@ export const INSIGHTS_PREVALENCE_ROW_TEST_ID =
export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle';
export const VISUALIZATIONS_SECTION_HEADER_TEST_ID =
'securitySolutionDocumentDetailsVisualizationsTitleHeader';
/* Visualizations analyzer preview */
export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerPreview';
/* Visualizations session preview */
export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalyzerPreview';
export const SESSION_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsSessionPreview';
/* Response section */

View file

@ -145,6 +145,13 @@ export const CORRELATIONS_TITLE = i18n.translate(
{ defaultMessage: 'Correlations' }
);
export const CORRELATIONS_ERROR = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.correlations.error',
{
defaultMessage: 'No correlations data available',
}
);
export const PREVALENCE_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.prevalenceTitle',
{ defaultMessage: 'Prevalence' }
@ -195,6 +202,13 @@ export const ANALYZER_PREVIEW_TITLE = i18n.translate(
{ defaultMessage: 'Analyzer preview' }
);
export const ANALYZER_PREVIEW_ERROR = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.analyzerPreview.error',
{
defaultMessage: 'No analyzer graph data available',
}
);
export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', {
defaultMessage: 'Share Alert',
});
@ -213,6 +227,21 @@ export const SESSION_PREVIEW_TITLE = i18n.translate(
}
);
export const SESSION_PREVIEW_UPSELL = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.sessionPreview.upsell',
{
defaultMessage:
'Session preview is disabled because your license does not support it. Please upgrade your license.',
}
);
export const SESSION_PREVIEW_ERROR = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.sessionPreview.error',
{
defaultMessage: 'No session view data available',
}
);
export const SESSION_PREVIEW_PROCESS_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.sessionPreview.processText',
{

View file

@ -7,13 +7,13 @@
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { AnalyzerPreviewContainer } from './analyzer_preview_container';
import { SessionPreviewContainer } from './session_preview_container';
import { ExpandableSection } from './expandable_section';
import { VISUALIZATIONS_SECTION_TEST_ID } from './test_ids';
import { VISUALIZATIONS_TITLE } from './translations';
import { AnalyzerPreview } from './analyzer_preview';
import { SessionPreview } from './session_preview';
export interface VisualizatioinsSectionProps {
export interface VisualizationsSectionProps {
/**
* Boolean to allow the component to be expanded or collapsed on first render
*/
@ -23,7 +23,7 @@ export interface VisualizatioinsSectionProps {
/**
* Visualizations section in overview. It contains analyzer preview and session view preview.
*/
export const VisualizationsSection: React.FC<VisualizatioinsSectionProps> = ({
export const VisualizationsSection: React.FC<VisualizationsSectionProps> = ({
expanded = false,
}) => {
return (
@ -32,11 +32,11 @@ export const VisualizationsSection: React.FC<VisualizatioinsSectionProps> = ({
title={VISUALIZATIONS_TITLE}
data-test-subj={VISUALIZATIONS_SECTION_TEST_ID}
>
<SessionPreview />
<SessionPreviewContainer />
<EuiSpacer />
<AnalyzerPreview />
<AnalyzerPreviewContainer />
</ExpandableSection>
);
};

View file

@ -0,0 +1,44 @@
/*
* 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 { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import type { UseSessionPreviewParams } from './use_session_preview';
import { useSessionPreview } from './use_session_preview';
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
describe('useSessionPreview', () => {
let hookResult: RenderHookResult<UseSessionPreviewParams, SessionViewConfig | null>;
it(`should return a session view config object`, () => {
const getFieldsData: GetFieldsData = (field: string) => field;
hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
initialProps: { getFieldsData },
});
expect(hookResult.result.current).toEqual({
index: 'kibana.alert.ancestors.index',
investigatedAlertId: '_id',
jumpToCursor: 'kibana.alert.original_time',
jumpToEntityId: 'process.entity_id',
sessionEntityId: 'process.entry_leader.entity_id',
sessionStartTime: 'process.entry_leader.start',
});
});
it(`should return null if data isn't ready for session view`, () => {
const getFieldsData: GetFieldsData = (field: string) => '';
hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
initialProps: { getFieldsData },
});
expect(hookResult.result.current).toEqual(null);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
import { getField } from '../../shared/utils';
export interface UseSessionPreviewParams {
/**
* Retrieves searchHit values for the provided field
*/
getFieldsData: GetFieldsData;
}
/**
* Hook that returns the session view configuration if the session view is available for the alert
*/
export const useSessionPreview = ({
getFieldsData,
}: UseSessionPreviewParams): SessionViewConfig | null => {
const _id = getField(getFieldsData('_id'));
const index =
getField(getFieldsData('kibana.alert.ancestors.index')) || getField(getFieldsData('_index'));
const entryLeaderEntityId = getField(getFieldsData('process.entry_leader.entity_id'));
const entryLeaderStart = getField(getFieldsData('process.entry_leader.start'));
const entityId = getField(getFieldsData('process.entity_id'));
const time =
getField(getFieldsData('kibana.alert.original_time')) || getField(getFieldsData('timestamp'));
if (!index || !entryLeaderEntityId || !entryLeaderStart) {
return null;
}
return {
index,
sessionEntityId: entryLeaderEntityId,
sessionStartTime: entryLeaderStart,
...(entityId && { jumpToEntityId: entityId }),
...(time && { jumpToCursor: time }),
...(_id && { investigatedAlertId: _id }),
};
};

View file

@ -26,7 +26,8 @@ import { getNewRule } from '../../../../objects/rule';
import { ALERTS_URL } from '../../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
describe(
// TODO enable once the visualize tabs are back
describe.skip(
'Alert details expandable flyout left panel analyzer graph',
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
() => {

View file

@ -42,7 +42,7 @@ describe(
openEntitiesTab();
});
it('should display analyzer graph and node list under Insights Entities', () => {
it('should display host details and user details under Insights Entities', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB)
.should('be.visible')
.and('have.text', 'Insights');

View file

@ -23,7 +23,8 @@ import { getNewRule } from '../../../../objects/rule';
import { ALERTS_URL } from '../../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
describe(
// TODO enable once the visualize tabs are back
describe.skip(
'Alert details expandable flyout left panel session view',
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
() => {

View file

@ -16,7 +16,7 @@ import {
import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
@ -42,7 +42,7 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE,
@ -142,13 +142,15 @@ describe(
cy.log('analyzer graph preview');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER).should(
'be.visible'
);
cy.log('session view preview');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER).should('be.visible');
});
});

View file

@ -32,15 +32,15 @@ import {
INSIGHTS_THREAT_INTELLIGENCE_TEST_ID,
INSIGHTS_CORRELATIONS_TEST_ID,
INSIGHTS_PREVALENCE_TEST_ID,
ANALYZER_PREVIEW_TEST_ID,
SUMMARY_ROW_VALUE_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID,
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID,
INSIGHTS_ENTITIES_TEST_ID,
SESSION_PREVIEW_TEST_ID,
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
ANALYZER_PREVIEW_TEST_ID,
SESSION_PREVIEW_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/right/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
@ -154,9 +154,9 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES =
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER =
getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT =
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER =
getDataTestSubjectSelector(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID));
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT =
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER =
getDataTestSubjectSelector(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID));
/* Response section */