[7.x] [Security Solution][Timeline] - Open Host & Network details in side panel (#90064) (#91190)

# Conflicts:
#	x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx
This commit is contained in:
Michael Olorunnisola 2021-02-11 16:14:26 -05:00 committed by GitHub
parent 852ef0b643
commit b121769bb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 3002 additions and 618 deletions

View file

@ -14,6 +14,7 @@ import {
success,
success_count as successCount,
} from '../../detection_engine/schemas/common/schemas';
import { FlowTarget } from '../../search_strategy/security_solution/network';
import { PositiveInteger } from '../../detection_engine/schemas/types';
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
@ -423,11 +424,38 @@ type EmptyObject = Record<any, never>;
export type TimelineExpandedEventType =
| {
eventId: string;
indexName: string;
panelView?: 'eventDetail';
params?: {
eventId: string;
indexName: string;
};
}
| EmptyObject;
export type TimelineExpandedEvent = {
[tab in TimelineTabs]?: TimelineExpandedEventType;
export type TimelineExpandedHostType =
| {
panelView?: 'hostDetail';
params?: {
hostName: string;
};
}
| EmptyObject;
export type TimelineExpandedNetworkType =
| {
panelView?: 'networkDetail';
params?: {
ip: string;
flowTarget: FlowTarget;
};
}
| EmptyObject;
export type TimelineExpandedDetailType =
| TimelineExpandedEventType
| TimelineExpandedHostType
| TimelineExpandedNetworkType;
export type TimelineExpandedDetail = {
[tab in TimelineTabs]?: TimelineExpandedDetailType;
};

View file

@ -615,7 +615,7 @@ describe('CaseView ', () => {
type: 'x-pack/security_solution/local/timeline/CREATE_TIMELINE',
payload: {
columns: [],
expandedEvent: {},
expandedDetail: {},
id: 'timeline-case',
indexNames: [],
show: false,
@ -661,9 +661,10 @@ describe('CaseView ', () => {
.first()
.simulate('click');
expect(mockDispatch).toHaveBeenCalledWith({
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
payload: {
event: { eventId: 'alert-id-1', indexName: 'alert-index-1' },
panelView: 'eventDetail',
params: { eventId: 'alert-id-1', indexName: 'alert-index-1' },
timelineId: 'timeline-case',
},
});

View file

@ -44,7 +44,7 @@ import {
} from '../configure_cases/utils';
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
import { buildAlertsQuery, getRuleIdsFromComments } from './helpers';
import { EventDetailsFlyout } from '../../../common/components/events_viewer/event_details_flyout';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { TimelineId } from '../../../../common/types/timeline';
@ -368,9 +368,10 @@ export const CaseComponent = React.memo<CaseProps>(
const showAlert = useCallback(
(alertId: string, index: string) => {
dispatch(
timelineActions.toggleExpandedEvent({
timelineActions.toggleDetailPanel({
panelView: 'eventDetail',
timelineId: TimelineId.casePage,
event: {
params: {
eventId: alertId,
indexName: index,
},
@ -390,7 +391,7 @@ export const CaseComponent = React.memo<CaseProps>(
id: TimelineId.casePage,
columns: [],
indexNames: [],
expandedEvent: {},
expandedDetail: {},
show: false,
})
);
@ -500,9 +501,10 @@ export const CaseComponent = React.memo<CaseProps>(
</EuiFlexGroup>
</MyWrapper>
</WhitePageWrapper>
<EventDetailsFlyout
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
isFlyoutView
timelineId={TimelineId.casePage}
/>
<SpyRoute state={spyState} pageName={SecurityPageName.case} />

View file

@ -1,106 +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 { some } from 'lodash/fp';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import { BrowserFields, DocValueFields } from '../../containers/source';
import {
ExpandableEvent,
ExpandableEventTitle,
} from '../../../timelines/components/timeline/expandable_event';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useTimelineEventsDetails } from '../../../timelines/containers/details';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
const StyledEuiFlyout = styled(EuiFlyout)`
z-index: ${({ theme }) => theme.eui.euiZLevel7};
`;
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow: hidden;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow: hidden;
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`};
}
}
`;
interface EventDetailsFlyoutProps {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
timelineId: string;
}
const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
browserFields,
docValueFields,
timelineId,
}) => {
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const expandedEvent = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent?.query ?? {}
);
const handleClearSelection = useCallback(() => {
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
}, [dispatch, timelineId]);
const [loading, detailsData] = useTimelineEventsDetails({
docValueFields,
indexName: expandedEvent?.indexName ?? '',
eventId: expandedEvent?.eventId ?? '',
skip: !expandedEvent.eventId,
});
const isAlert = useMemo(
() => some({ category: 'signal', field: 'signal.rule.id' }, detailsData),
[detailsData]
);
if (!expandedEvent.eventId) {
return null;
}
return (
<StyledEuiFlyout size="s" onClose={handleClearSelection}>
<EuiFlyoutHeader hasBorder>
<ExpandableEventTitle isAlert={isAlert} loading={loading} />
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<ExpandableEvent
browserFields={browserFields}
detailsData={detailsData}
event={expandedEvent}
isAlert={isAlert}
loading={loading}
timelineId={timelineId}
timelineTabType="flyout"
/>
</StyledEuiFlyoutBody>
</StyledEuiFlyout>
);
};
export const EventDetailsFlyout = React.memo(
EventDetailsFlyoutComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
prevProps.timelineId === nextProps.timelineId
);

View file

@ -86,7 +86,6 @@ const eventsViewerDefaultProps = {
deletedEventIds: [],
docValueFields: [],
end: to,
expandedEvent: {},
filters: [],
id: TimelineId.detectionsPage,
indexNames: mockIndexNames,
@ -100,7 +99,6 @@ const eventsViewerDefaultProps = {
query: '',
language: 'kql',
},
handleCloseExpandedEvent: jest.fn(),
start: from,
sort: [
{
@ -150,14 +148,15 @@ describe('EventsViewer', () => {
expect(mockDispatch).toBeCalledTimes(2);
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: {
event: {
panelView: 'eventDetail',
params: {
eventId: 'yb8TkHYBRgU82_bJu_rY',
indexName: 'auditbeat-7.10.1-2020.12.18-000001',
},
tabType: 'query',
timelineId: TimelineId.test,
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
});
});
});

View file

@ -40,11 +40,7 @@ import { inputsModel } from '../../store';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import {
TimelineExpandedEventType,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
@ -113,7 +109,6 @@ interface Props {
deletedEventIds: Readonly<string[]>;
docValueFields: DocValueFields[];
end: string;
expandedEvent: TimelineExpandedEventType;
filters: Filter[];
headerFilterGroup?: React.ReactNode;
height?: number;
@ -141,7 +136,6 @@ const EventsViewerComponent: React.FC<Props> = ({
deletedEventIds,
docValueFields,
end,
expandedEvent,
filters,
headerFilterGroup,
id,

View file

@ -21,7 +21,7 @@ import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { EventDetailsFlyout } from './event_details_flyout';
import { DetailsPanel } from '../../../timelines/components/side_panel';
const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;
@ -46,6 +46,11 @@ export interface OwnProps {
type Props = OwnProps & PropsFromRedux;
/**
* The stateful events viewer component is the highest level component that is utilized across the security_solution pages layer where
* timeline is used BESIDES the flyout. The flyout makes use of the `EventsViewer` component which is a subcomponent here
* NOTE: As of writting, it is not used in the Case_View component
*/
const StatefulEventsViewerComponent: React.FC<Props> = ({
createTimeline,
columns,
@ -53,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
deletedEventIds,
deleteEventQuery,
end,
expandedEvent,
excludedRowRendererIds,
filters,
headerFilterGroup,
@ -114,7 +118,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
dataProviders={dataProviders!}
deletedEventIds={deletedEventIds}
end={end}
expandedEvent={expandedEvent}
isLoadingIndexPattern={isLoadingIndexPattern}
filters={globalFilters}
headerFilterGroup={headerFilterGroup}
@ -133,9 +136,10 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
/>
</InspectButtonContainer>
</FullScreenContainer>
<EventDetailsFlyout
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
isFlyoutView
timelineId={id}
/>
</>
@ -155,7 +159,6 @@ const makeMapStateToProps = () => {
dataProviders,
deletedEventIds,
excludedRowRendererIds,
expandedEvent,
graphEventId,
itemsPerPage,
itemsPerPageOptions,
@ -168,7 +171,6 @@ const makeMapStateToProps = () => {
columns,
dataProviders,
deletedEventIds,
expandedEvent: expandedEvent?.query ?? {},
excludedRowRendererIds,
filters: getGlobalFiltersQuerySelector(state),
id,

View file

@ -55,10 +55,11 @@ export const LinkAnchor: React.FC<EuiLinkProps> = ({ children, ...props }) => (
);
// Internal Links
const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({
children,
hostName,
}) => {
const HostDetailsLinkComponent: React.FC<{
children?: React.ReactNode;
hostName: string;
isButton?: boolean;
}> = ({ children, hostName, isButton }) => {
const { formatUrl, search } = useFormatUrl(SecurityPageName.hosts);
const { navigateToApp } = useKibana().services.application;
const goToHostDetails = useCallback(
@ -71,7 +72,14 @@ const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName:
[hostName, navigateToApp, search]
);
return (
return isButton ? (
<LinkButton
onClick={goToHostDetails}
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
>
{children ? children : hostName}
</LinkButton>
) : (
<LinkAnchor
onClick={goToHostDetails}
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
@ -80,6 +88,7 @@ const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName:
</LinkAnchor>
);
};
export const HostDetailsLink = React.memo(HostDetailsLinkComponent);
const allowedUrlSchemes = ['http://', 'https://'];
@ -119,7 +128,8 @@ const NetworkDetailsLinkComponent: React.FC<{
children?: React.ReactNode;
ip: string;
flowTarget?: FlowTarget | FlowTargetSourceDest;
}> = ({ children, ip, flowTarget = FlowTarget.source }) => {
isButton?: boolean;
}> = ({ children, ip, flowTarget = FlowTarget.source, isButton }) => {
const { formatUrl, search } = useFormatUrl(SecurityPageName.network);
const { navigateToApp } = useKibana().services.application;
const goToNetworkDetails = useCallback(
@ -132,7 +142,14 @@ const NetworkDetailsLinkComponent: React.FC<{
[flowTarget, ip, navigateToApp, search]
);
return (
return isButton ? (
<LinkButton
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}
onClick={goToNetworkDetails}
>
{children ? children : ip}
</LinkButton>
) : (
<LinkAnchor
onClick={goToNetworkDetails}
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}

View file

@ -0,0 +1,26 @@
/*
* 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 { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { DescriptionList } from '../../../../common/utility_types';
import { DescriptionListStyled } from '../../../common/components/page';
export const OverviewDescriptionList = ({
dataTestSubj,
descriptionList,
isInDetailsSidePanel = false,
}: {
dataTestSubj?: string;
descriptionList: DescriptionList[];
isInDetailsSidePanel: boolean;
}) => (
<EuiFlexItem grow={!isInDetailsSidePanel}>
<DescriptionListStyled data-test-subj={dataTestSubj} listItems={descriptionList} />
</EuiFlexItem>
);

View file

@ -214,7 +214,7 @@ export const mockGlobalState: State = {
description: '',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
highlightedDropAndProviderId: '',
historyIds: [],
isFavorite: false,

View file

@ -2109,7 +2109,7 @@ export const mockTimelineModel: TimelineModel = {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [
{
$state: {
@ -2232,7 +2232,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],

View file

@ -156,7 +156,7 @@ describe('alert actions', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [
{
$state: {

View file

@ -151,6 +151,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
docValueFields={docValueFields}
id={id}
inspect={inspect}
isInDetailsSidePanel={false}
refetch={refetch}
setQuery={setQuery}
data={hostOverview as HostItem}

View file

@ -141,6 +141,158 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`]
flowTarget="source"
id="ipOverview"
ip="10.10.10.10"
isInDetailsSidePanel={false}
isLoadingAnomaliesData={false}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"
type="details"
updateFlowTargetAction={[MockFunction]}
/>
`;
exports[`IP Overview Component rendering it renders the side panel IP overview 1`] = `
<IpOverview
anomaliesData={
Object {
"anomalies": Array [
Object {
"detectorIndex": 0,
"entityName": "process.name",
"entityValue": "du",
"influencers": Array [
Object {
"host.name": "zeek-iowa",
},
Object {
"process.name": "du",
},
Object {
"user.name": "root",
},
],
"jobId": "job-1",
"rowId": "1561157194802_0",
"severity": 16.193669439507826,
"source": Object {
"actual": Array [
1,
],
"bucket_span": 900,
"by_field_name": "process.name",
"by_field_value": "du",
"detector_index": 0,
"function": "rare",
"function_description": "rare",
"influencers": Array [
Object {
"influencer_field_name": "user.name",
"influencer_field_values": Array [
"root",
],
},
Object {
"influencer_field_name": "process.name",
"influencer_field_values": Array [
"du",
],
},
Object {
"influencer_field_name": "host.name",
"influencer_field_values": Array [
"zeek-iowa",
],
},
],
"initial_record_score": 16.193669439507826,
"is_interim": false,
"job_id": "job-1",
"multi_bucket_impact": 0,
"partition_field_name": "host.name",
"partition_field_value": "zeek-iowa",
"probability": 0.024041164411288146,
"record_score": 16.193669439507826,
"result_type": "record",
"timestamp": 1560664800000,
"typical": Array [
0.024041164411288146,
],
},
"time": 1560664800000,
},
Object {
"detectorIndex": 0,
"entityName": "process.name",
"entityValue": "ls",
"influencers": Array [
Object {
"host.name": "zeek-iowa",
},
Object {
"process.name": "ls",
},
Object {
"user.name": "root",
},
],
"jobId": "job-2",
"rowId": "1561157194802_1",
"severity": 16.193669439507826,
"source": Object {
"actual": Array [
1,
],
"bucket_span": 900,
"by_field_name": "process.name",
"by_field_value": "ls",
"detector_index": 0,
"function": "rare",
"function_description": "rare",
"influencers": Array [
Object {
"influencer_field_name": "user.name",
"influencer_field_values": Array [
"root",
],
},
Object {
"influencer_field_name": "process.name",
"influencer_field_values": Array [
"ls",
],
},
Object {
"influencer_field_name": "host.name",
"influencer_field_values": Array [
"zeek-iowa",
],
},
],
"initial_record_score": 16.193669439507826,
"is_interim": false,
"job_id": "job-2",
"multi_bucket_impact": 0,
"partition_field_name": "host.name",
"partition_field_value": "zeek-iowa",
"probability": 0.024041164411288146,
"record_score": 16.193669439507826,
"result_type": "record",
"timestamp": 1560664800000,
"typical": Array [
0.024041164411288146,
],
},
"time": 1560664800000,
},
],
"interval": "day",
}
}
endDate="2019-06-18T06:00:00.000Z"
flowTarget="source"
id="ipOverview"
ip="10.10.10.10"
isInDetailsSidePanel={true}
isLoadingAnomaliesData={false}
loading={false}
narrowDateRange={[MockFunction]}

View file

@ -58,6 +58,7 @@ describe('IP Overview Component', () => {
loading: false,
id: 'ipOverview',
ip: '10.10.10.10',
isInDetailsSidePanel: false,
isLoadingAnomaliesData: false,
narrowDateRange: (jest.fn() as unknown) as NarrowDateRange,
startDate: '2019-06-15T06:00:00.000Z',
@ -76,5 +77,19 @@ describe('IP Overview Component', () => {
expect(wrapper.find('IpOverview')).toMatchSnapshot();
});
test('it renders the side panel IP overview', () => {
const panelViewProps = {
...mockProps,
isInDetailsSidePanel: true,
};
const wrapper = shallow(
<TestProviders store={store}>
<IpOverview {...panelViewProps} />
</TestProviders>
);
expect(wrapper.find('IpOverview')).toMatchSnapshot();
});
});
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { EuiFlexItem } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
@ -27,39 +26,38 @@ import {
whoisRenderer,
} from '../../../timelines/components/field_renderers/field_renderers';
import * as i18n from './translations';
import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page';
import { OverviewWrapper } from '../../../common/components/page';
import { Loader } from '../../../common/components/loader';
import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types';
import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions';
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
import { OverviewDescriptionList } from '../../../common/components/overview_description_list';
export interface IpOverviewProps {
anomaliesData: Anomalies | null;
contextID?: string; // used to provide unique draggable context when viewing in the side panel
data: NetworkDetailsStrategyResponse['networkDetails'];
endDate: string;
flowTarget: FlowTarget;
id: string;
ip: string;
loading: boolean;
isInDetailsSidePanel: boolean;
isLoadingAnomaliesData: boolean;
anomaliesData: Anomalies | null;
startDate: string;
endDate: string;
type: networkModel.NetworkType;
loading: boolean;
narrowDateRange: NarrowDateRange;
startDate: string;
type: networkModel.NetworkType;
}
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
<EuiFlexItem key={key}>
<DescriptionListStyled listItems={descriptionList} />
</EuiFlexItem>
);
export const IpOverview = React.memo<IpOverviewProps>(
({
contextID,
id,
ip,
data,
isInDetailsSidePanel = false, // Rather than duplicate the component, alter the structure based on it's location
loading,
flowTarget,
startDate,
@ -77,13 +75,14 @@ export const IpOverview = React.memo<IpOverviewProps>(
title: i18n.LOCATION,
description: locationRenderer(
[`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`],
data
data,
contextID
),
},
{
title: i18n.AUTONOMOUS_SYSTEM,
description: typeData
? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget)
? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget, contextID)
: getEmptyTagValue(),
},
];
@ -123,12 +122,13 @@ export const IpOverview = React.memo<IpOverviewProps>(
title: i18n.HOST_ID,
description:
typeData && data.host
? hostIdRenderer({ host: data.host, ipFilter: ip })
? hostIdRenderer({ host: data.host, ipFilter: ip, contextID })
: getEmptyTagValue(),
},
{
title: i18n.HOST_NAME,
description: typeData && data.host ? hostNameRenderer(data.host, ip) : getEmptyTagValue(),
description:
typeData && data.host ? hostNameRenderer(data.host, ip, contextID) : getEmptyTagValue(),
},
],
[
@ -139,12 +139,17 @@ export const IpOverview = React.memo<IpOverviewProps>(
return (
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
<OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}>
{!isInDetailsSidePanel && (
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
)}
{descriptionLists.map((descriptionList, index) => (
<OverviewDescriptionList
descriptionList={descriptionList}
isInDetailsSidePanel={isInDetailsSidePanel}
key={index}
/>
))}
{loading && (
<Loader

View file

@ -45,7 +45,7 @@ describe('Port', () => {
expect(wrapper.find('[data-test-subj="formatted-ip"]').first().text()).toEqual('10.1.2.3');
});
test('it hyperlinks to the network/ip page', () => {
test('it dispalys a button which opens the network/ip side panel', () => {
const wrapper = mount(
<TestProviders>
<Ip contextId="test" eventId="abcd" fieldName="destination.ip" value="10.1.2.3" />
@ -53,8 +53,7 @@ describe('Port', () => {
);
expect(
wrapper.find('[data-test-subj="draggable-truncatable-content"]').find('a').first().props()
.href
).toEqual('/ip/10.1.2.3/source');
wrapper.find('[data-test-subj="draggable-truncatable-content"]').find('a').first().text()
).toEqual('10.1.2.3');
});
});

View file

@ -147,6 +147,7 @@ const NetworkDetailsComponent: React.FC = () => {
id={id}
inspect={inspect}
ip={ip}
isInDetailsSidePanel={false}
data={networkDetails}
anomaliesData={anomaliesData}
loading={loading}

View file

@ -196,6 +196,211 @@ exports[`Host Summary Component rendering it renders the default Host Summary 1`
endDate="2019-06-18T06:00:00.000Z"
id="hostOverview"
indexNames={Array []}
isInDetailsSidePanel={false}
isLoadingAnomaliesData={false}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"
/>
`;
exports[`Host Summary Component rendering it renders the panel view Host Summary 1`] = `
<HostOverview
anomaliesData={
Object {
"anomalies": Array [
Object {
"detectorIndex": 0,
"entityName": "process.name",
"entityValue": "du",
"influencers": Array [
Object {
"host.name": "zeek-iowa",
},
Object {
"process.name": "du",
},
Object {
"user.name": "root",
},
],
"jobId": "job-1",
"rowId": "1561157194802_0",
"severity": 16.193669439507826,
"source": Object {
"actual": Array [
1,
],
"bucket_span": 900,
"by_field_name": "process.name",
"by_field_value": "du",
"detector_index": 0,
"function": "rare",
"function_description": "rare",
"influencers": Array [
Object {
"influencer_field_name": "user.name",
"influencer_field_values": Array [
"root",
],
},
Object {
"influencer_field_name": "process.name",
"influencer_field_values": Array [
"du",
],
},
Object {
"influencer_field_name": "host.name",
"influencer_field_values": Array [
"zeek-iowa",
],
},
],
"initial_record_score": 16.193669439507826,
"is_interim": false,
"job_id": "job-1",
"multi_bucket_impact": 0,
"partition_field_name": "host.name",
"partition_field_value": "zeek-iowa",
"probability": 0.024041164411288146,
"record_score": 16.193669439507826,
"result_type": "record",
"timestamp": 1560664800000,
"typical": Array [
0.024041164411288146,
],
},
"time": 1560664800000,
},
Object {
"detectorIndex": 0,
"entityName": "process.name",
"entityValue": "ls",
"influencers": Array [
Object {
"host.name": "zeek-iowa",
},
Object {
"process.name": "ls",
},
Object {
"user.name": "root",
},
],
"jobId": "job-2",
"rowId": "1561157194802_1",
"severity": 16.193669439507826,
"source": Object {
"actual": Array [
1,
],
"bucket_span": 900,
"by_field_name": "process.name",
"by_field_value": "ls",
"detector_index": 0,
"function": "rare",
"function_description": "rare",
"influencers": Array [
Object {
"influencer_field_name": "user.name",
"influencer_field_values": Array [
"root",
],
},
Object {
"influencer_field_name": "process.name",
"influencer_field_values": Array [
"ls",
],
},
Object {
"influencer_field_name": "host.name",
"influencer_field_values": Array [
"zeek-iowa",
],
},
],
"initial_record_score": 16.193669439507826,
"is_interim": false,
"job_id": "job-2",
"multi_bucket_impact": 0,
"partition_field_name": "host.name",
"partition_field_value": "zeek-iowa",
"probability": 0.024041164411288146,
"record_score": 16.193669439507826,
"result_type": "record",
"timestamp": 1560664800000,
"typical": Array [
0.024041164411288146,
],
},
"time": 1560664800000,
},
],
"interval": "day",
}
}
data={
Object {
"_id": "yneHlmgBjVl2VqDlAjPR",
"cloud": Object {
"instance": Object {
"id": Array [
"423232333829362673777",
],
},
"machine": Object {
"type": Array [
"custom-4-16384",
],
},
"provider": Array [
"gce",
],
"region": Array [
"us-east-1",
],
},
"host": Object {
"architecture": Array [
"x86_64",
],
"id": Array [
"aa7ca589f1b8220002f2fc61c64cfbf1",
],
"ip": Array [
"10.142.0.7",
"fe80::4001:aff:fe8e:7",
],
"mac": Array [
"42:01:0a:8e:00:07",
],
"name": Array [
"siem-kibana",
],
"os": Object {
"family": Array [
"debian",
],
"name": Array [
"Debian GNU/Linux",
],
"platform": Array [
"debian",
],
"version": Array [
"9 (stretch)",
],
},
},
}
}
docValueFields={Array []}
endDate="2019-06-18T06:00:00.000Z"
id="hostOverview"
indexNames={Array []}
isInDetailsSidePanel={true}
isLoadingAnomaliesData={false}
loading={false}
narrowDateRange={[MockFunction]}

View file

@ -5,87 +5,94 @@
* 2.0.
*/
import { EuiFlexItem, EuiHealth } from '@elastic/eui';
import { EuiHealth } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { OverviewDescriptionList } from '../../../../common/components/overview_description_list';
import { DescriptionList } from '../../../../../common/utility_types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { EndpointFields, HostPolicyResponseActionStatus } from '../../../../graphql/types';
import { DescriptionListStyled } from '../../../../common/components/page';
import * as i18n from './translations';
interface Props {
contextID?: string;
data: EndpointFields | null;
isInDetailsSidePanel?: boolean;
}
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
<EuiFlexItem key={key}>
<DescriptionListStyled data-test-subj="endpoint-overview" listItems={descriptionList} />
</EuiFlexItem>
export const EndpointOverview = React.memo<Props>(
({ contextID, data, isInDetailsSidePanel = false }) => {
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
<DefaultFieldRenderer
rowItems={[getOr('', fieldName, fieldData)]}
attrName={attrName}
idPrefix={contextID ? `endpoint-overview-${contextID}` : 'endpoint-overview'}
/>
),
[contextID]
);
const descriptionLists: Readonly<DescriptionList[][]> = useMemo(
() => [
[
{
title: i18n.ENDPOINT_POLICY,
description:
data != null && data.endpointPolicy != null
? data.endpointPolicy
: getEmptyTagValue(),
},
],
[
{
title: i18n.POLICY_STATUS,
description:
data != null && data.policyStatus != null ? (
<EuiHealth
aria-label={data.policyStatus}
color={
data.policyStatus === HostPolicyResponseActionStatus.failure
? 'danger'
: data.policyStatus
}
>
{data.policyStatus}
</EuiHealth>
) : (
getEmptyTagValue()
),
},
],
[
{
title: i18n.SENSORVERSION,
description:
data != null && data.sensorVersion != null
? getDefaultRenderer('sensorVersion', data, 'agent.version')
: getEmptyTagValue(),
},
],
[], // needs 4 columns for design
],
[data, getDefaultRenderer]
);
return (
<>
{descriptionLists.map((descriptionList, index) => (
<OverviewDescriptionList
dataTestSubj="endpoint-overview"
descriptionList={descriptionList}
isInDetailsSidePanel={isInDetailsSidePanel}
key={index}
/>
))}
</>
);
}
);
export const EndpointOverview = React.memo<Props>(({ data }) => {
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
<DefaultFieldRenderer
rowItems={[getOr('', fieldName, fieldData)]}
attrName={attrName}
idPrefix="endpoint-overview"
/>
),
[]
);
const descriptionLists: Readonly<DescriptionList[][]> = useMemo(
() => [
[
{
title: i18n.ENDPOINT_POLICY,
description:
data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(),
},
],
[
{
title: i18n.POLICY_STATUS,
description:
data != null && data.policyStatus != null ? (
<EuiHealth
aria-label={data.policyStatus}
color={
data.policyStatus === HostPolicyResponseActionStatus.failure
? 'danger'
: data.policyStatus
}
>
{data.policyStatus}
</EuiHealth>
) : (
getEmptyTagValue()
),
},
],
[
{
title: i18n.SENSORVERSION,
description:
data != null && data.sensorVersion != null
? getDefaultRenderer('sensorVersion', data, 'agent.version')
: getEmptyTagValue(),
},
],
[], // needs 4 columns for design
],
[data, getDefaultRenderer]
);
return (
<>
{descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))}
</>
);
});
EndpointOverview.displayName = 'EndpointOverview';

View file

@ -15,21 +15,39 @@ import { mockData } from './mock';
import { mockAnomalies } from '../../../common/components/ml/mock';
describe('Host Summary Component', () => {
describe('rendering', () => {
const mockProps = {
anomaliesData: mockAnomalies,
data: mockData.Hosts.edges[0].node,
docValueFields: [],
endDate: '2019-06-18T06:00:00.000Z',
id: 'hostOverview',
indexNames: [],
isInDetailsSidePanel: false,
isLoadingAnomaliesData: false,
loading: false,
narrowDateRange: jest.fn(),
startDate: '2019-06-15T06:00:00.000Z',
};
test('it renders the default Host Summary', () => {
const wrapper = shallow(
<TestProviders>
<HostOverview
anomaliesData={mockAnomalies}
data={mockData.Hosts.edges[0].node}
docValueFields={[]}
endDate="2019-06-18T06:00:00.000Z"
id="hostOverview"
indexNames={[]}
isLoadingAnomaliesData={false}
loading={false}
narrowDateRange={jest.fn()}
startDate="2019-06-15T06:00:00.000Z"
/>
<HostOverview {...mockProps} />
</TestProviders>
);
expect(wrapper.find('HostOverview')).toMatchSnapshot();
});
test('it renders the panel view Host Summary', () => {
const panelViewProps = {
...mockProps,
isInDetailsSidePanel: true,
};
const wrapper = shallow(
<TestProviders>
<HostOverview {...panelViewProps} />
</TestProviders>
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { EuiHorizontalRule } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { getOr } from 'lodash/fp';
@ -27,7 +27,7 @@ import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores';
import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types';
import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page';
import { OverviewWrapper } from '../../../common/components/page';
import {
FirstLastSeenHost,
FirstLastSeenHostType,
@ -35,11 +35,14 @@ import {
import * as i18n from './translations';
import { EndpointOverview } from './endpoint_overview';
import { OverviewDescriptionList } from '../../../common/components/overview_description_list';
interface HostSummaryProps {
contextID?: string; // used to provide unique draggable context when viewing in the side panel
data: HostItem;
docValueFields: DocValueFields[];
id: string;
isInDetailsSidePanel: boolean;
loading: boolean;
isLoadingAnomaliesData: boolean;
indexNames: string[];
@ -49,19 +52,15 @@ interface HostSummaryProps {
narrowDateRange: NarrowDateRange;
}
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
<EuiFlexItem key={key}>
<DescriptionListStyled listItems={descriptionList} />
</EuiFlexItem>
);
export const HostOverview = React.memo<HostSummaryProps>(
({
anomaliesData,
contextID,
data,
docValueFields,
endDate,
id,
isInDetailsSidePanel = false, // Rather than duplicate the component, alter the structure based on it's location
isLoadingAnomaliesData,
indexNames,
loading,
@ -77,10 +76,10 @@ export const HostOverview = React.memo<HostSummaryProps>(
<DefaultFieldRenderer
rowItems={getOr([], fieldName, fieldData)}
attrName={fieldName}
idPrefix="host-overview"
idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'}
/>
),
[]
[contextID]
);
const column: DescriptionList[] = useMemo(
@ -162,7 +161,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
<DefaultFieldRenderer
rowItems={getOr([], 'host.ip', data)}
attrName={'host.ip'}
idPrefix="host-overview"
idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'}
render={(ip) => (ip != null ? <NetworkDetailsLink ip={ip} /> : getEmptyTagValue())}
/>
),
@ -198,17 +197,22 @@ export const HostOverview = React.memo<HostSummaryProps>(
},
],
],
[data, firstColumn, getDefaultRenderer]
[contextID, data, firstColumn, getDefaultRenderer]
);
return (
<>
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
<OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}>
{!isInDetailsSidePanel && (
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
)}
{descriptionLists.map((descriptionList, index) => (
<OverviewDescriptionList
descriptionList={descriptionList}
isInDetailsSidePanel={isInDetailsSidePanel}
key={index}
/>
))}
{loading && (
<Loader
@ -224,8 +228,12 @@ export const HostOverview = React.memo<HostSummaryProps>(
{data.endpoint != null ? (
<>
<EuiHorizontalRule />
<OverviewWrapper>
<EndpointOverview data={data.endpoint} />
<OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}>
<EndpointOverview
contextID={contextID}
data={data.endpoint}
isInDetailsSidePanel={isInDetailsSidePanel}
/>
{loading && (
<Loader

View file

@ -41,7 +41,8 @@ export const DEFAULT_MORE_MAX_HEIGHT = '200px';
export const locationRenderer = (
fieldNames: string[],
data: NetworkDetailsStrategyResponse['networkDetails']
data: NetworkDetailsStrategyResponse['networkDetails'],
contextID?: string
): React.ReactElement =>
fieldNames.length > 0 && fieldNames.every((fieldName) => getOr(null, fieldName, data)) ? (
<EuiFlexGroup alignItems="center" gutterSize="none" data-test-subj="location-field">
@ -52,7 +53,9 @@ export const locationRenderer = (
{index ? ',\u00A0' : ''}
<EuiFlexItem grow={false}>
<DefaultDraggable
id={`location-renderer-default-draggable-${IpOverviewId}-${fieldName}`}
id={`location-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${fieldName}`}
field={fieldName}
value={locationValue}
/>
@ -71,13 +74,16 @@ export const dateRenderer = (timestamp?: string | null): React.ReactElement => (
export const autonomousSystemRenderer = (
as: AutonomousSystem,
flowTarget: FlowTarget
flowTarget: FlowTarget,
contextID?: string
): React.ReactElement =>
as && as.organization && as.organization.name && as.number ? (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<DefaultDraggable
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${flowTarget}.as.organization.name`}
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${flowTarget}.as.organization.name`}
field={`${flowTarget}.as.organization.name`}
value={as.organization.name}
/>
@ -85,7 +91,9 @@ export const autonomousSystemRenderer = (
<EuiFlexItem grow={false}>{'/'}</EuiFlexItem>
<EuiFlexItem grow={false}>
<DefaultDraggable
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${flowTarget}.as.number`}
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${flowTarget}.as.number`}
field={`${flowTarget}.as.number`}
value={`${as.number}`}
/>
@ -96,12 +104,14 @@ export const autonomousSystemRenderer = (
);
interface HostIdRendererTypes {
contextID?: string;
host: HostEcs;
ipFilter?: string;
noLink?: boolean;
}
export const hostIdRenderer = ({
contextID,
host,
ipFilter,
noLink,
@ -110,7 +120,9 @@ export const hostIdRenderer = ({
<>
{host.name && host.name[0] != null ? (
<DefaultDraggable
id={`host-id-renderer-default-draggable-${IpOverviewId}-host-id`}
id={`host-id-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}host-id`}
field="host.id"
value={host.id[0]}
>
@ -128,14 +140,20 @@ export const hostIdRenderer = ({
getEmptyTagValue()
);
export const hostNameRenderer = (host?: HostEcs, ipFilter?: string): React.ReactElement =>
export const hostNameRenderer = (
host?: HostEcs,
ipFilter?: string,
contextID?: string
): React.ReactElement =>
host &&
host.name &&
host.name[0] &&
host.ip &&
(!(ipFilter != null) || host.ip.includes(ipFilter)) ? (
<DefaultDraggable
id={`host-name-renderer-default-draggable-${IpOverviewId}-host-name`}
id={`host-name-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}host-name`}
field={'host.name'}
value={host.name[0]}
>

View file

@ -6,9 +6,11 @@
*/
import { isArray, isEmpty, isString, uniq } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useContext } from 'react';
import { useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { FlowTarget } from '../../../../common/search_strategy/security_solution/network';
import {
DragEffects,
DraggableWrapper,
@ -16,13 +18,21 @@ import {
import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers';
import { Content } from '../../../common/components/draggables';
import { getOrEmptyTagFromValue } from '../../../common/components/empty_value';
import { NetworkDetailsLink } from '../../../common/components/links';
import { parseQueryValue } from '../../../timelines/components/timeline/body/renderers/parse_query_value';
import {
DataProvider,
IS_OPERATOR,
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { Provider } from '../../../timelines/components/timeline/data_providers/provider';
import {
TimelineExpandedDetailType,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { activeTimeline } from '../../containers/active_timeline_context';
import { timelineActions } from '../../store/timeline';
import { StatefulEventContext } from '../timeline/body/events/stateful_event_context';
import { LinkAnchor } from '../../../common/components/links';
const getUniqueId = ({
contextId,
@ -128,22 +138,52 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
fieldName,
truncate,
}) => {
const key = useMemo(
() =>
`address-links-draggable-wrapper-${getUniqueId({
contextId,
eventId,
fieldName,
address,
})}`,
[address, contextId, eventId, fieldName]
);
const key = `address-links-draggable-wrapper-${getUniqueId({
contextId,
eventId,
fieldName,
address,
})}`;
const dataProviderProp = useMemo(
() => getDataProvider({ contextId, eventId, fieldName, address }),
[address, contextId, eventId, fieldName]
);
const dispatch = useDispatch();
const eventContext = useContext(StatefulEventContext);
const openNetworkDetailsSidePanel = useCallback(
(e) => {
e.preventDefault();
if (address && eventContext?.timelineID && eventContext?.tabType) {
const { tabType, timelineID } = eventContext;
const updatedExpandedDetail: TimelineExpandedDetailType = {
panelView: 'networkDetail',
params: {
ip: address,
flowTarget: fieldName.includes(FlowTarget.destination)
? FlowTarget.destination
: FlowTarget.source,
},
};
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
timelineId: timelineID,
})
);
if (timelineID === TimelineId.active && tabType === TimelineTabs.query) {
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
}
}
},
[dispatch, eventContext, address, fieldName]
);
const render = useCallback(
(_props, _provided, snapshot) =>
snapshot.isDragging ? (
@ -152,10 +192,16 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
</DragEffects>
) : (
<Content field={fieldName} tooltipContent={address}>
<NetworkDetailsLink data-test-subj="network-details" ip={address} />
<LinkAnchor
href="#"
data-test-subj="network-details"
onClick={openNetworkDetailsSidePanel}
>
{address}
</LinkAnchor>
</Content>
),
[address, dataProviderProp, fieldName]
[address, dataProviderProp, openNetworkDetailsSidePanel, fieldName]
);
return (

View file

@ -294,7 +294,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -397,7 +397,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -500,7 +500,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -601,7 +601,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -740,7 +740,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -868,7 +868,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [
{
$state: {
@ -1012,7 +1012,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],
@ -1115,7 +1115,7 @@ describe('helpers', () => {
eventIdToNoteIds: {},
eventType: 'all',
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
filters: [],
highlightedDropAndProviderId: '',
historyIds: [],

View file

@ -46,10 +46,11 @@ const ToggleEventDetailsButtonComponent: React.FC<ToggleEventDetailsButtonProps>
const handleClick = useCallback(() => {
dispatch(
timelineActions.toggleExpandedEvent({
timelineActions.toggleDetailPanel({
panelView: 'eventDetail',
tabType: TimelineTabs.notes,
timelineId,
event: {
params: {
eventId,
indexName: existingIndexNames.join(','),
},

View file

@ -21,7 +21,7 @@ import {
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { TimelineExpandedEventType, TimelineTabs } from '../../../../../common/types/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import {
EventDetails,
@ -36,7 +36,7 @@ export type HandleOnEventClosed = () => void;
interface Props {
browserFields: BrowserFields;
detailsData: TimelineEventsDetailsItem[] | null;
event: TimelineExpandedEventType;
event: { eventId: string; indexName: string };
isAlert: boolean;
loading: boolean;
messageHeight?: number;

View file

@ -0,0 +1,109 @@
/*
* 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 { some } from 'lodash/fp';
import { EuiFlyoutHeader, EuiFlyoutBody, EuiSpacer } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { BrowserFields, DocValueFields } from '../../../../common/containers/source';
import { ExpandableEvent, ExpandableEventTitle } from './expandable_event';
import { useTimelineEventsDetails } from '../../../containers/details';
import { TimelineTabs } from '../../../../../common/types/timeline';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow: hidden;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow: hidden;
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`};
}
}
`;
interface EventDetailsPanelProps {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
expandedEvent: { eventId: string; indexName: string };
handleOnEventClosed: () => void;
isFlyoutView?: boolean;
tabType: TimelineTabs;
timelineId: string;
}
const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
browserFields,
docValueFields,
expandedEvent,
handleOnEventClosed,
isFlyoutView,
tabType,
timelineId,
}) => {
const [loading, detailsData] = useTimelineEventsDetails({
docValueFields,
indexName: expandedEvent.indexName ?? '',
eventId: expandedEvent.eventId ?? '',
skip: !expandedEvent.eventId,
});
const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, detailsData);
if (!expandedEvent?.eventId) {
return null;
}
return isFlyoutView ? (
<>
<EuiFlyoutHeader hasBorder>
<ExpandableEventTitle isAlert={isAlert} loading={loading} />
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<ExpandableEvent
browserFields={browserFields}
detailsData={detailsData}
event={expandedEvent}
isAlert={isAlert}
loading={loading}
timelineId={timelineId}
timelineTabType="flyout"
/>
</StyledEuiFlyoutBody>
</>
) : (
<>
<ExpandableEventTitle
isAlert={isAlert}
loading={loading}
handleOnEventClosed={handleOnEventClosed}
/>
<EuiSpacer size="m" />
<ExpandableEvent
browserFields={browserFields}
detailsData={detailsData}
event={expandedEvent}
isAlert={isAlert}
loading={loading}
timelineId={timelineId}
timelineTabType={tabType}
/>
</>
);
};
export const EventDetailsPanel = React.memo(
EventDetailsPanelComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
prevProps.timelineId === nextProps.timelineId
);

View file

@ -14,13 +14,6 @@ export const MESSAGE = i18n.translate(
}
);
export const COPY_TO_CLIPBOARD = i18n.translate(
'xpack.securitySolution.timeline.expandableEvent.copyToClipboardToolTip',
{
defaultMessage: 'Copy to Clipboard',
}
);
export const CLOSE = i18n.translate(
'xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel',
{
@ -28,13 +21,6 @@ export const CLOSE = i18n.translate(
}
);
export const EVENT = i18n.translate(
'xpack.securitySolution.timeline.expandableEvent.eventToolTipTitle',
{
defaultMessage: 'Event',
}
);
export const EVENT_DETAILS_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.timeline.expandableEvent.placeholder',
{

View file

@ -0,0 +1,94 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiTitle } from '@elastic/eui';
import { HostDetailsLink } from '../../../../common/components/links';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { HostOverview } from '../../../../overview/components/host_overview';
import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions';
import { HostItem } from '../../../../../common/search_strategy';
import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/anomaly_table_provider';
import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria';
import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime';
import { HostOverviewByNameQuery } from '../../../../hosts/containers/hosts/details';
interface ExpandableHostProps {
hostName: string;
}
export const ExpandableHostDetailsTitle = ({ hostName }: ExpandableHostProps) => (
<EuiTitle size="s">
<h4>
{i18n.translate('xpack.securitySolution.timeline.sidePanel.hostDetails.title', {
defaultMessage: 'Host details',
})}
{`: ${hostName}`}
</h4>
</EuiTitle>
);
export const ExpandableHostDetailsPageLink = ({ hostName }: ExpandableHostProps) => (
<HostDetailsLink hostName={hostName} isButton>
{i18n.translate('xpack.securitySolution.timeline.sidePanel.hostDetails.hostDetailsPageLink', {
defaultMessage: 'View details page',
})}
</HostDetailsLink>
);
export const ExpandableHostDetails = ({
contextID,
hostName,
}: ExpandableHostProps & { contextID: string }) => {
const { to, from, isInitializing } = useGlobalTime();
const { docValueFields, selectedPatterns } = useSourcererScope();
return (
<HostOverviewByNameQuery
indexNames={selectedPatterns}
sourceId="default"
hostName={hostName}
skip={isInitializing}
startDate={from}
endDate={to}
>
{({ hostOverview, loading, id }) => (
<AnomalyTableProvider
criteriaFields={hostToCriteria(hostOverview)}
startDate={from}
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
<HostOverview
contextID={contextID}
docValueFields={docValueFields}
id={id}
isInDetailsSidePanel
data={hostOverview as HostItem}
anomaliesData={anomaliesData}
isLoadingAnomaliesData={isLoadingAnomaliesData}
indexNames={selectedPatterns}
loading={loading}
startDate={from}
endDate={to}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
)}
</AnomalyTableProvider>
)}
</HostOverviewByNameQuery>
);
};

View file

@ -0,0 +1,116 @@
/*
* 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.
*/
/* eslint-disable react/display-name */
import {
EuiFlexGroup,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlexItem,
EuiButtonIcon,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import {
ExpandableHostDetails,
ExpandableHostDetailsPageLink,
ExpandableHostDetailsTitle,
} from './expandable_host';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow-x: hidden;
overflow-y: scroll;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow-x: hidden;
overflow-y: scroll;
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`};
}
}
`;
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
flex: 0;
`;
const StyledEuiFlexItem = styled(EuiFlexItem)`
&.euiFlexItem {
flex: 1 0 0;
overflow-y: scroll;
overflow-x: hidden;
}
`;
const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)`
align-self: flex-start;
`;
interface HostDetailsProps {
contextID: string;
expandedHost: { hostName: string };
handleOnHostClosed: () => void;
isFlyoutView?: boolean;
}
export const HostDetailsPanel: React.FC<HostDetailsProps> = React.memo(
({ contextID, expandedHost, handleOnHostClosed, isFlyoutView }) => {
const { hostName } = expandedHost;
if (!hostName) {
return null;
}
return isFlyoutView ? (
<>
<EuiFlyoutHeader hasBorder>
<ExpandableHostDetailsTitle hostName={hostName} />
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<EuiSpacer size="m" />
<ExpandableHostDetailsPageLink hostName={hostName} />
<EuiSpacer size="m" />
<ExpandableHostDetails contextID={contextID} hostName={hostName} />
</StyledEuiFlyoutBody>
</>
) : (
<>
<StyledEuiFlexGroup justifyContent="spaceBetween" wrap={false}>
<EuiFlexItem grow={false}>
<ExpandableHostDetailsTitle hostName={hostName} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.translate(
'xpack.securitySolution.timeline.sidePanel.hostDetails.close',
{
defaultMessage: 'close',
}
)}
onClick={handleOnHostClosed}
/>
</EuiFlexItem>
</StyledEuiFlexGroup>
<EuiSpacer size="m" />
<StyledEuiFlexButtonWrapper grow={false}>
<ExpandableHostDetailsPageLink hostName={hostName} />
</StyledEuiFlexButtonWrapper>
<EuiSpacer size="m" />
<StyledEuiFlexItem grow={true}>
<ExpandableHostDetails contextID={contextID} hostName={hostName} />
</StyledEuiFlexItem>
</>
);
}
);

View file

@ -0,0 +1,204 @@
/*
* 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 { mount } from 'enzyme';
import React from 'react';
import '../../../common/mock/match_media';
import {
apolloClientObservable,
mockGlobalState,
TestProviders,
SUB_PLUGINS_REDUCER,
kibanaObservable,
createSecuritySolutionStorageMock,
} from '../../../common/mock';
import { createStore, State } from '../../../common/store';
import { DetailsPanel } from './index';
import { TimelineExpandedDetail, TimelineTabs } from '../../../../common/types/timeline';
import { FlowTarget } from '../../../../common/search_strategy/security_solution/network';
describe('Details Panel Component', () => {
const state: State = { ...mockGlobalState };
const { storage } = createSecuritySolutionStorageMock();
let store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
kibanaObservable,
storage
);
const dataLessExpandedDetail = {
[TimelineTabs.query]: {
panelView: 'hostDetail',
params: {},
},
};
const hostExpandedDetail: TimelineExpandedDetail = {
[TimelineTabs.query]: {
panelView: 'hostDetail',
params: {
hostName: 'woohoo!',
},
},
};
const networkExpandedDetail: TimelineExpandedDetail = {
[TimelineTabs.query]: {
panelView: 'networkDetail',
params: {
ip: 'woohoo!',
flowTarget: FlowTarget.source,
},
},
};
const eventExpandedDetail: TimelineExpandedDetail = {
[TimelineTabs.query]: {
panelView: 'eventDetail',
params: {
eventId: 'my-id',
indexName: 'my-index',
},
},
};
const mockProps = {
browserFields: {},
docValueFields: [],
handleOnPanelClosed: jest.fn(),
isFlyoutView: false,
tabType: TimelineTabs.query,
timelineId: 'test',
};
describe('DetailsPanel: rendering', () => {
beforeEach(() => {
store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
kibanaObservable,
storage
);
});
test('it should not render the DetailsPanel if no expanded detail has been set in the reducer', () => {
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('DetailsPanel')).toMatchSnapshot();
});
test('it should not render the DetailsPanel if an expanded detail with a panelView, but not params have been set', () => {
state.timeline.timelineById.test.expandedDetail = dataLessExpandedDetail as TimelineExpandedDetail; // Casting as the dataless doesn't meet the actual type requirements
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('DetailsPanel')).toMatchSnapshot();
});
});
describe('DetailsPanel:EventDetails: rendering', () => {
beforeEach(() => {
state.timeline.timelineById.test.expandedDetail = eventExpandedDetail;
store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
kibanaObservable,
storage
);
});
test('it should render the Details Panel when the panelView is set and the associated params are set', () => {
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('DetailsPanel')).toMatchSnapshot();
});
test('it should render the Event Details view of the Details Panel in the flyout when the panelView is eventDetail and the eventId is set', () => {
const currentProps = { ...mockProps, isFlyoutView: true };
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...currentProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="timeline:details-panel:flyout"]')).toMatchSnapshot();
});
test('it should render the Event Details view in the Details Panel when the panelView is eventDetail and the eventId is set', () => {
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('EventDetails')).toMatchSnapshot();
});
});
describe('DetailsPanel:HostDetails: rendering', () => {
beforeEach(() => {
state.timeline.timelineById.test.expandedDetail = hostExpandedDetail;
store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
kibanaObservable,
storage
);
});
test('it should render the Host Details view in the Details Panel when the panelView is hostDetail and the hostName is set', () => {
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('HostDetails')).toMatchSnapshot();
});
});
describe('DetailsPanel:NetworkDetails: rendering', () => {
beforeEach(() => {
state.timeline.timelineById.test.expandedDetail = networkExpandedDetail;
store = createStore(
state,
SUB_PLUGINS_REDUCER,
apolloClientObservable,
kibanaObservable,
storage
);
});
test('it should render the Network Details view in the Details Panel when the panelView is networkDetail and the ip is set', () => {
const wrapper = mount(
<TestProviders store={store}>
<DetailsPanel {...mockProps} />
</TestProviders>
);
expect(wrapper.find('NetworkDetails')).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,120 @@
/*
* 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 { useDispatch } from 'react-redux';
import { EuiFlyout } from '@elastic/eui';
import styled from 'styled-components';
import { timelineActions, timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../store/timeline/defaults';
import { BrowserFields, DocValueFields } from '../../../common/containers/source';
import { TimelineTabs } from '../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { EventDetailsPanel } from './event_details';
import { HostDetailsPanel } from './host_details';
import { NetworkDetailsPanel } from './network_details';
const StyledEuiFlyout = styled(EuiFlyout)`
z-index: ${({ theme }) => theme.eui.euiZLevel7};
`;
interface DetailsPanelProps {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
handleOnPanelClosed?: () => void;
isFlyoutView?: boolean;
tabType?: TimelineTabs;
timelineId: string;
}
/**
* This panel is used in both the main timeline as well as the flyouts on the host, detection, cases, and network pages.
* To prevent duplication the `isFlyoutView` prop is passed to determine the layout that should be used
* `tabType` defaults to query and `handleOnPanelClosed` defaults to unsetting the default query tab which is used for the flyout panel
*/
export const DetailsPanel = React.memo(
({
browserFields,
docValueFields,
handleOnPanelClosed,
isFlyoutView,
tabType,
timelineId,
}: DetailsPanelProps) => {
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const expandedDetail = useDeepEqualSelector((state) => {
return (getTimeline(state, timelineId) ?? timelineDefaults).expandedDetail;
});
// To be used primarily in the flyout scenario where we don't want to maintain the tabType
const defaultOnPanelClose = useCallback(() => {
dispatch(timelineActions.toggleDetailPanel({ timelineId }));
}, [dispatch, timelineId]);
const activeTab = tabType ?? TimelineTabs.query;
const closePanel = useCallback(() => {
if (handleOnPanelClosed) handleOnPanelClosed();
else defaultOnPanelClose();
}, [defaultOnPanelClose, handleOnPanelClosed]);
if (!expandedDetail) return null;
const currentTabDetail = expandedDetail[activeTab];
if (!currentTabDetail?.panelView) return null;
let visiblePanel = null; // store in variable to make return statement more readable
const contextID = `${timelineId}-${activeTab}`;
if (currentTabDetail?.panelView === 'eventDetail' && currentTabDetail?.params?.eventId) {
visiblePanel = (
<EventDetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
expandedEvent={currentTabDetail?.params}
handleOnEventClosed={closePanel}
isFlyoutView={isFlyoutView}
tabType={activeTab}
timelineId={timelineId}
/>
);
}
if (currentTabDetail?.panelView === 'hostDetail' && currentTabDetail?.params?.hostName) {
visiblePanel = (
<HostDetailsPanel
contextID={contextID}
expandedHost={currentTabDetail?.params}
handleOnHostClosed={closePanel}
isFlyoutView={isFlyoutView}
/>
);
}
if (currentTabDetail?.panelView === 'networkDetail' && currentTabDetail?.params?.ip) {
visiblePanel = (
<NetworkDetailsPanel
contextID={contextID}
expandedNetwork={currentTabDetail?.params}
handleOnNetworkClosed={closePanel}
isFlyoutView={isFlyoutView}
/>
);
}
return isFlyoutView ? (
<StyledEuiFlyout data-test-subj="timeline:details-panel:flyout" size="s" onClose={closePanel}>
{visiblePanel}
</StyledEuiFlyout>
) : (
visiblePanel
);
}
);
DetailsPanel.displayName = 'DetailsPanel';

View file

@ -0,0 +1,134 @@
/*
* 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 { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { FlowTarget } from '../../../../../common/search_strategy';
import { NetworkDetailsLink } from '../../../../common/components/links';
import { IpOverview } from '../../../../network/components/details';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { networkToCriteria } from '../../../../common/components/ml/criteria/network_to_criteria';
import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime';
import { useKibana } from '../../../../common/lib/kibana';
import { convertToBuildEsQuery } from '../../../../common/lib/keury';
import { inputsSelectors } from '../../../../common/store';
import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions';
import { OverviewEmpty } from '../../../../overview/components/overview_empty';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useNetworkDetails } from '../../../../network/containers/details';
import { networkModel } from '../../../../network/store';
import { useAnomaliesTableData } from '../../../../common/components/ml/anomaly/use_anomalies_table_data';
interface ExpandableNetworkProps {
expandedNetwork: { ip: string; flowTarget: FlowTarget };
}
export const ExpandableNetworkDetailsTitle = ({ ip }: { ip: string }) => (
<EuiTitle size="s">
<h4>
{i18n.translate('xpack.securitySolution.timeline.sidePanel.networkDetails.title', {
defaultMessage: 'Network details',
})}
{`: ${ip}`}
</h4>
</EuiTitle>
);
export const ExpandableNetworkDetailsPageLink = ({
expandedNetwork: { ip, flowTarget },
}: ExpandableNetworkProps) => (
<NetworkDetailsLink ip={ip} flowTarget={flowTarget} isButton>
{i18n.translate(
'xpack.securitySolution.timeline.sidePanel.networkDetails.networkDetailsPageLink',
{
defaultMessage: 'View details page',
}
)}
</NetworkDetailsLink>
);
export const ExpandableNetworkDetails = ({
contextID,
expandedNetwork,
}: ExpandableNetworkProps & { contextID: string }) => {
const { ip, flowTarget } = expandedNetwork;
const dispatch = useDispatch();
const { to, from, isInitializing } = useGlobalTime();
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const getGlobalFiltersQuerySelector = useMemo(
() => inputsSelectors.globalFiltersQuerySelector(),
[]
);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
const type = networkModel.NetworkType.details;
const narrowDateRange = useCallback(
(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
dispatch(
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
})
);
},
[dispatch]
);
const {
services: { uiSettings },
} = useKibana();
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const filterQuery = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern,
queries: [query],
filters,
});
const [loading, { id, networkDetails }] = useNetworkDetails({
docValueFields,
skip: isInitializing,
filterQuery,
indexNames: selectedPatterns,
ip,
});
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
criteriaFields: networkToCriteria(ip, flowTarget),
startDate: from,
endDate: to,
skip: isInitializing,
});
return indicesExist ? (
<IpOverview
contextID={contextID}
id={id}
ip={ip}
data={networkDetails}
anomaliesData={anomaliesData}
loading={loading}
isInDetailsSidePanel
isLoadingAnomaliesData={isLoadingAnomaliesData}
type={type}
flowTarget={flowTarget}
startDate={from}
endDate={to}
narrowDateRange={narrowDateRange}
/>
) : (
<OverviewEmpty />
);
};

View file

@ -0,0 +1,113 @@
/*
* 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.
*/
/* eslint-disable react/display-name */
import {
EuiFlexGroup,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlexItem,
EuiButtonIcon,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import { FlowTarget } from '../../../../../common/search_strategy';
import {
ExpandableNetworkDetailsTitle,
ExpandableNetworkDetailsPageLink,
ExpandableNetworkDetails,
} from './expandable_network';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow-x: hidden;
overflow-y: scroll;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow-x: hidden;
overflow-y: scroll;
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`};
}
}
`;
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
flex: 0;
`;
const StyledEuiFlexItem = styled(EuiFlexItem)`
&.euiFlexItem {
flex: 1 0 0;
overflow-y: scroll;
overflow-x: hidden;
}
`;
const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)`
align-self: flex-start;
`;
interface NetworkDetailsProps {
contextID: string;
expandedNetwork: { ip: string; flowTarget: FlowTarget };
handleOnNetworkClosed: () => void;
isFlyoutView?: boolean;
}
export const NetworkDetailsPanel = React.memo(
({ contextID, expandedNetwork, handleOnNetworkClosed, isFlyoutView }: NetworkDetailsProps) => {
const { ip } = expandedNetwork;
return isFlyoutView ? (
<>
<EuiFlyoutHeader hasBorder>
<ExpandableNetworkDetailsTitle ip={ip} />
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<EuiSpacer size="m" />
<ExpandableNetworkDetailsPageLink expandedNetwork={expandedNetwork} />
<EuiSpacer size="m" />
<ExpandableNetworkDetails contextID={contextID} expandedNetwork={expandedNetwork} />
</StyledEuiFlyoutBody>
</>
) : (
<>
<StyledEuiFlexGroup justifyContent="spaceBetween" wrap={false}>
<EuiFlexItem grow={false}>
<ExpandableNetworkDetailsTitle ip={ip} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
aria-label={i18n.translate(
'xpack.securitySolution.timeline.sidePanel.networkDetails.close',
{
defaultMessage: 'close',
}
)}
onClick={handleOnNetworkClosed}
/>
</EuiFlexItem>
</StyledEuiFlexGroup>
<EuiSpacer size="m" />
<StyledEuiFlexButtonWrapper grow={false}>
<ExpandableNetworkDetailsPageLink expandedNetwork={expandedNetwork} />
</StyledEuiFlexButtonWrapper>
<EuiSpacer size="m" />
<StyledEuiFlexItem grow={true}>
<ExpandableNetworkDetails contextID={contextID} expandedNetwork={expandedNetwork} />
</StyledEuiFlexItem>
</>
);
}
);

View file

@ -30,10 +30,9 @@ describe('Actions', () => {
ariaRowindex={2}
checked={false}
columnValues={'abc def'}
expanded={false}
eventId="abc"
loadingEventIds={[]}
onEventToggled={jest.fn()}
onEventDetailsPanelOpened={jest.fn()}
onRowSelected={jest.fn()}
showCheckboxes={true}
/>
@ -52,9 +51,8 @@ describe('Actions', () => {
checked={false}
columnValues={'abc def'}
eventId="abc"
expanded={false}
loadingEventIds={[]}
onEventToggled={jest.fn()}
onEventDetailsPanelOpened={jest.fn()}
onRowSelected={jest.fn()}
showCheckboxes={false}
/>

View file

@ -20,10 +20,9 @@ interface Props {
columnValues: string;
checked: boolean;
onRowSelected: OnRowSelected;
expanded: boolean;
eventId: string;
loadingEventIds: Readonly<string[]>;
onEventToggled: () => void;
onEventDetailsPanelOpened: () => void;
showCheckboxes: boolean;
}
@ -33,10 +32,9 @@ const ActionsComponent: React.FC<Props> = ({
additionalActions,
checked,
columnValues,
expanded,
eventId,
loadingEventIds,
onEventToggled,
onEventDetailsPanelOpened,
onRowSelected,
showCheckboxes,
}) => {
@ -78,9 +76,8 @@ const ActionsComponent: React.FC<Props> = ({
<EuiButtonIcon
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
data-test-subj="expand-event"
disabled={expanded}
iconType="arrowRight"
onClick={onEventToggled}
onClick={onEventDetailsPanelOpened}
/>
</EuiToolTip>
</EventsTdContent>

View file

@ -51,7 +51,7 @@ describe('EventColumnView', () => {
loading: false,
loadingEventIds: [],
notesCount: 0,
onEventToggled: jest.fn(),
onEventDetailsPanelOpened: jest.fn(),
onPinEvent: jest.fn(),
onRowSelected: jest.fn(),
onUnPinEvent: jest.fn(),

View file

@ -42,12 +42,11 @@ interface Props {
data: TimelineNonEcsData[];
ecsData: Ecs;
eventIdToNoteIds: Readonly<Record<string, string[]>>;
expanded: boolean;
isEventPinned: boolean;
isEventViewer?: boolean;
loadingEventIds: Readonly<string[]>;
notesCount: number;
onEventToggled: () => void;
onEventDetailsPanelOpened: () => void;
onPinEvent: OnPinEvent;
onRowSelected: OnRowSelected;
onUnPinEvent: OnUnPinEvent;
@ -74,12 +73,11 @@ export const EventColumnView = React.memo<Props>(
data,
ecsData,
eventIdToNoteIds,
expanded,
isEventPinned = false,
isEventViewer = false,
loadingEventIds,
notesCount,
onEventToggled,
onEventDetailsPanelOpened,
onPinEvent,
onRowSelected,
onUnPinEvent,
@ -220,14 +218,12 @@ export const EventColumnView = React.memo<Props>(
checked={Object.keys(selectedEventIds).includes(id)}
columnValues={columnValues}
onRowSelected={onRowSelected}
expanded={expanded}
data-test-subj="actions"
eventId={id}
loadingEventIds={loadingEventIds}
onEventToggled={onEventToggled}
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
showCheckboxes={showCheckboxes}
/>
<DataDrivenColumns
_id={id}
ariaRowindex={ariaRowindex}

View file

@ -9,7 +9,11 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import {
TimelineExpandedDetailType,
TimelineId,
TimelineTabs,
} from '../../../../../../common/types/timeline';
import { BrowserFields } from '../../../../../common/containers/source';
import {
TimelineItem,
@ -33,6 +37,8 @@ import { getRowRenderer } from '../renderers/get_row_renderer';
import { StatefulRowRenderer } from './stateful_row_renderer';
import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers';
import { timelineDefaults } from '../../../../store/timeline/defaults';
import { getMappedNonEcsValue } from '../data_driven_columns';
import { StatefulEventContext } from './stateful_event_context';
interface Props {
actionsColumnWidth: number;
@ -90,21 +96,39 @@ const StatefulEventComponent: React.FC<Props> = ({
}) => {
const trGroupRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
const [activeStatefulEventContext] = useState({ timelineID: timelineId, tabType });
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const expandedEvent = useDeepEqualSelector(
(state) =>
(getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[
tabType ?? TimelineTabs.query
] ?? {}
const expandedDetail = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedDetail ?? {}
);
const hostName = useMemo(() => {
const hostNameArr = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' });
return hostNameArr && hostNameArr.length > 0 ? hostNameArr[0] : null;
}, [event?.data]);
const hostIPAddresses = useMemo(() => {
const ipList = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' });
return ipList;
}, [event?.data]);
const activeTab = tabType ?? TimelineTabs.query;
const activeExpandedDetail = expandedDetail[activeTab];
const isDetailPanelExpanded: boolean =
(activeExpandedDetail?.panelView === 'eventDetail' &&
activeExpandedDetail?.params?.eventId === event._id) ||
(activeExpandedDetail?.panelView === 'hostDetail' &&
activeExpandedDetail?.params?.hostName === hostName) ||
(activeExpandedDetail?.panelView === 'networkDetail' &&
activeExpandedDetail?.params?.ip &&
hostIPAddresses?.includes(activeExpandedDetail?.params?.ip)) ||
false;
const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
const notesById = useDeepEqualSelector(getNotesByIds);
const noteIds: string[] = eventIdToNoteIds[event._id] || emptyNotes;
const isExpanded = useMemo(() => expandedEvent && expandedEvent.eventId === event._id, [
event._id,
expandedEvent,
]);
const notes: TimelineResultNote[] = useMemo(
() =>
@ -153,23 +177,28 @@ const StatefulEventComponent: React.FC<Props> = ({
[dispatch, timelineId]
);
const handleOnEventToggled = useCallback(() => {
const handleOnEventDetailPanelOpened = useCallback(() => {
const eventId = event._id;
const indexName = event._index!;
const updatedExpandedDetail: TimelineExpandedDetailType = {
panelView: 'eventDetail',
params: {
eventId,
indexName,
},
};
dispatch(
timelineActions.toggleExpandedEvent({
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
timelineId,
event: {
eventId,
indexName,
},
})
);
if (timelineId === TimelineId.active && tabType === TimelineTabs.query) {
activeTimeline.toggleExpandedEvent({ eventId, indexName });
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
}
}, [dispatch, event._id, event._index, tabType, timelineId]);
@ -209,63 +238,64 @@ const StatefulEventComponent: React.FC<Props> = ({
);
return (
<EventsTrGroup
$ariaRowindex={ariaRowindex}
className={STATEFUL_EVENT_CSS_CLASS_NAME}
data-test-subj="event"
eventType={getEventType(event.ecs)}
isBuildingBlockType={isEventBuildingBlockType(event.ecs)}
isExpanded={isExpanded}
ref={trGroupRef}
showLeftBorder={!isEventViewer}
>
<EventColumnView
id={event._id}
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={ariaRowindex}
columnHeaders={columnHeaders}
columnRenderers={columnRenderers}
data={event.data}
ecsData={event.ecs}
eventIdToNoteIds={eventIdToNoteIds}
expanded={isExpanded}
hasRowRenderers={hasRowRenderers}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
loadingEventIds={loadingEventIds}
notesCount={notes.length}
onEventToggled={handleOnEventToggled}
onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
onUnPinEvent={onUnPinEvent}
refetch={refetch}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
showNotes={!!showNotes[event._id]}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={onToggleShowNotes}
/>
<StatefulEventContext.Provider value={activeStatefulEventContext}>
<EventsTrGroup
$ariaRowindex={ariaRowindex}
className={STATEFUL_EVENT_CSS_CLASS_NAME}
data-test-subj="event"
eventType={getEventType(event.ecs)}
isBuildingBlockType={isEventBuildingBlockType(event.ecs)}
isExpanded={isDetailPanelExpanded}
ref={trGroupRef}
showLeftBorder={!isEventViewer}
>
<EventColumnView
id={event._id}
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={ariaRowindex}
columnHeaders={columnHeaders}
columnRenderers={columnRenderers}
data={event.data}
ecsData={event.ecs}
eventIdToNoteIds={eventIdToNoteIds}
hasRowRenderers={hasRowRenderers}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
loadingEventIds={loadingEventIds}
notesCount={notes.length}
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
onUnPinEvent={onUnPinEvent}
refetch={refetch}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
showNotes={!!showNotes[event._id]}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={onToggleShowNotes}
/>
<EventsTrSupplementContainerWrapper>
<EventsTrSupplement
className="siemEventsTable__trSupplement--notes"
data-test-subj="event-notes-flex-item"
>
<NoteCards
ariaRowindex={ariaRowindex}
associateNote={associateNote}
data-test-subj="note-cards"
notes={notes}
showAddNote={!!showNotes[event._id]}
toggleShowAddNote={onToggleShowNotes}
/>
</EventsTrSupplement>
<EventsTrSupplementContainerWrapper>
<EventsTrSupplement
className="siemEventsTable__trSupplement--notes"
data-test-subj="event-notes-flex-item"
>
<NoteCards
ariaRowindex={ariaRowindex}
associateNote={associateNote}
data-test-subj="note-cards"
notes={notes}
showAddNote={!!showNotes[event._id]}
toggleShowAddNote={onToggleShowNotes}
/>
</EventsTrSupplement>
{RowRendererContent}
</EventsTrSupplementContainerWrapper>
</EventsTrGroup>
{RowRendererContent}
</EventsTrSupplementContainerWrapper>
</EventsTrGroup>
</StatefulEventContext.Provider>
);
};

View file

@ -0,0 +1,17 @@
/*
* 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 { TimelineTabs } from '../../../../../../common/types/timeline';
interface StatefulEventContext {
tabType: TimelineTabs | undefined;
timelineID: string;
}
// This context is available to all children of the stateful_event component where the provider is currently set
export const StatefulEventContext = React.createContext<StatefulEventContext | null>(null);

View file

@ -240,14 +240,15 @@ describe('Body', () => {
expect(mockDispatch).toBeCalledTimes(1);
expect(mockDispatch.mock.calls[0][0]).toEqual({
payload: {
event: {
panelView: 'eventDetail',
params: {
eventId: '1',
indexName: undefined,
},
tabType: 'query',
timelineId: 'timeline-test',
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
});
});
@ -263,14 +264,15 @@ describe('Body', () => {
expect(mockDispatch).toBeCalledTimes(1);
expect(mockDispatch.mock.calls[0][0]).toEqual({
payload: {
event: {
panelView: 'eventDetail',
params: {
eventId: '1',
indexName: undefined,
},
tabType: 'pinned',
timelineId: 'timeline-test',
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
});
});
@ -286,14 +288,15 @@ describe('Body', () => {
expect(mockDispatch).toBeCalledTimes(1);
expect(mockDispatch.mock.calls[0][0]).toEqual({
payload: {
event: {
panelView: 'eventDetail',
params: {
eventId: '1',
indexName: undefined,
},
tabType: 'notes',
timelineId: 'timeline-test',
},
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
});
});
});

View file

@ -60,6 +60,10 @@ const EXTRA_WIDTH = 4; // px
export type StatefulBodyProps = OwnProps & PropsFromRedux;
/**
* The Body component is used everywhere timeline is used within the security application. It is the highest level component
* that is shared across all implementations of the timeline.
*/
export const BodyComponent = React.memo<StatefulBodyProps>(
({
activePage,

View file

@ -243,7 +243,7 @@ describe('Events', () => {
expect(wrapper.find('[data-test-subj="truncatable-message"]').exists()).toEqual(false);
});
test('it renders a hyperlink to the hosts details page when fieldName is host.name, and a hostname is provided', () => {
test('it renders a button to open the hosts details panel when fieldName is host.name, and a hostname is provided', () => {
const wrapper = mount(
<TestProviders>
<FormattedFieldValue
@ -255,10 +255,10 @@ describe('Events', () => {
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="host-details-link"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="host-details-button"]').exists()).toEqual(true);
});
test('it does NOT render a hyperlink to the hosts details page when fieldName is host.name, but a hostname is NOT provided', () => {
test('it does NOT render a button to open the hosts details panel when fieldName is host.name, but a hostname is NOT provided', () => {
const wrapper = mount(
<TestProviders>
<FormattedFieldValue
@ -270,7 +270,7 @@ describe('Events', () => {
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="host-details-link"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="host-details-button"]').exists()).toEqual(false);
});
test('it renders placeholder text when fieldName is host.name, but a hostname is NOT provided', () => {

View file

@ -5,13 +5,21 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback, useContext } from 'react';
import { useDispatch } from 'react-redux';
import { isString } from 'lodash/fp';
import { LinkAnchor } from '../../../../../common/components/links';
import {
TimelineId,
TimelineTabs,
TimelineExpandedDetailType,
} from '../../../../../../common/types/timeline';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { HostDetailsLink } from '../../../../../common/components/links';
import { TruncatableText } from '../../../../../common/components/truncatable_text';
import { StatefulEventContext } from '../events/stateful_event_context';
import { activeTimeline } from '../../../../containers/active_timeline_context';
import { timelineActions } from '../../../../store/timeline';
interface Props {
contextId: string;
@ -21,18 +29,48 @@ interface Props {
}
const HostNameComponent: React.FC<Props> = ({ fieldName, contextId, eventId, value }) => {
const hostname = `${value}`;
const dispatch = useDispatch();
const eventContext = useContext(StatefulEventContext);
const hostName = `${value}`;
return isString(value) && hostname.length > 0 ? (
const openHostDetailsSidePanel = useCallback(
(e) => {
e.preventDefault();
if (hostName && eventContext?.tabType && eventContext?.timelineID) {
const { timelineID, tabType } = eventContext;
const updatedExpandedDetail: TimelineExpandedDetailType = {
panelView: 'hostDetail',
params: {
hostName,
},
};
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
timelineId: timelineID,
tabType,
})
);
if (timelineID === TimelineId.active && tabType === TimelineTabs.query) {
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
}
}
},
[dispatch, eventContext, hostName]
);
return isString(value) && hostName.length > 0 ? (
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
tooltipContent={value}
value={value}
tooltipContent={hostName}
value={hostName}
>
<HostDetailsLink data-test-subj="host-details-link" hostName={hostname}>
<TruncatableText data-test-subj="draggable-truncatable-content">{value}</TruncatableText>
</HostDetailsLink>
<LinkAnchor href="#" data-test-subj="host-details-button" onClick={openHostDetailsSidePanel}>
<TruncatableText data-test-subj="draggable-truncatable-content">{hostName}</TruncatableText>
</LinkAnchor>
</DefaultDraggable>
) : (
getEmptyTagValue()

View file

@ -1,85 +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 { some } from 'lodash/fp';
import { EuiSpacer } from '@elastic/eui';
import React, { useMemo } from 'react';
import deepEqual from 'fast-deep-equal';
import { BrowserFields, DocValueFields } from '../../../common/containers/source';
import {
ExpandableEvent,
ExpandableEventTitle,
HandleOnEventClosed,
} from '../../../timelines/components/timeline/expandable_event';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { useTimelineEventsDetails } from '../../containers/details';
import { timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../store/timeline/defaults';
import { TimelineTabs } from '../../../../common/types/timeline';
interface EventDetailsProps {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
tabType: TimelineTabs;
timelineId: string;
handleOnEventClosed?: HandleOnEventClosed;
}
const EventDetailsComponent: React.FC<EventDetailsProps> = ({
browserFields,
docValueFields,
tabType,
timelineId,
handleOnEventClosed,
}) => {
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const expandedEvent = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent[tabType] ?? {}
);
const [loading, detailsData] = useTimelineEventsDetails({
docValueFields,
indexName: expandedEvent.indexName!,
eventId: expandedEvent.eventId!,
skip: !expandedEvent.eventId,
});
const isAlert = useMemo(
() => some({ category: 'signal', field: 'signal.rule.id' }, detailsData),
[detailsData]
);
return (
<>
<ExpandableEventTitle
isAlert={isAlert}
loading={loading}
handleOnEventClosed={handleOnEventClosed}
/>
<EuiSpacer size="m" />
<ExpandableEvent
browserFields={browserFields}
detailsData={detailsData}
event={expandedEvent}
isAlert={isAlert}
loading={loading}
timelineId={timelineId}
timelineTabType={tabType}
/>
</>
);
};
export const EventDetails = React.memo(
EventDetailsComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
prevProps.timelineId === nextProps.timelineId &&
prevProps.handleOnEventClosed === nextProps.handleOnEventClosed
);

View file

@ -18,7 +18,7 @@ import { isTab } from '../../../common/components/accessibility/helpers';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
import { TimelineType, TimelineTabs, TimelineId } from '../../../../common/types/timeline';
import { TimelineType, TimelineId } from '../../../../common/types/timeline';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { activeTimeline } from '../../containers/active_timeline_context';
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
@ -69,9 +69,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({ timelineId }) => {
id: timelineId,
columns: defaultHeaders,
indexNames: selectedPatterns,
expandedEvent: {
[TimelineTabs.query]: activeTimeline.getExpandedEvent(),
},
expandedDetail: activeTimeline.getExpandedDetail(),
show: false,
})
);

View file

@ -31,8 +31,8 @@ import { CREATED_BY, NOTES } from '../../notes/translations';
import { PARTICIPANTS } from '../../../../cases/translations';
import { NotePreviews } from '../../open_timeline/note_previews';
import { TimelineResultNote } from '../../open_timeline/types';
import { EventDetails } from '../event_details';
import { getTimelineNoteSelector } from './selectors';
import { DetailsPanel } from '../../side_panel';
const FullWidthFlexGroup = styled(EuiFlexGroup)`
width: 100%;
@ -125,7 +125,7 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []);
const {
createdBy,
expandedEvent,
expandedDetail,
eventIdToNoteIds,
noteIds,
status: timelineStatus,
@ -162,22 +162,22 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
[dispatch, timelineId]
);
const handleOnEventClosed = useCallback(() => {
dispatch(timelineActions.toggleExpandedEvent({ tabType: TimelineTabs.notes, timelineId }));
const handleOnPanelClosed = useCallback(() => {
dispatch(timelineActions.toggleDetailPanel({ tabType: TimelineTabs.notes, timelineId }));
}, [dispatch, timelineId]);
const EventDetailsContent = useMemo(
const DetailsPanelContent = useMemo(
() =>
expandedEvent != null && expandedEvent.eventId != null ? (
<EventDetails
expandedDetail[TimelineTabs.notes]?.panelView ? (
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
handleOnEventClosed={handleOnEventClosed}
handleOnPanelClosed={handleOnPanelClosed}
tabType={TimelineTabs.notes}
timelineId={timelineId}
/>
) : null,
[browserFields, docValueFields, expandedEvent, handleOnEventClosed, timelineId]
[browserFields, docValueFields, expandedDetail, handleOnPanelClosed, timelineId]
);
const SidebarContent = useMemo(
@ -216,7 +216,7 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = ({ timelineId }
</StyledPanel>
</ScrollableFlexItem>
<VerticalRule />
<ScrollableFlexItem grow={1}>{EventDetailsContent ?? SidebarContent}</ScrollableFlexItem>
<ScrollableFlexItem grow={1}>{DetailsPanelContent ?? SidebarContent}</ScrollableFlexItem>
</FullWidthFlexGroup>
);
};

View file

@ -13,7 +13,7 @@ export const getTimelineNoteSelector = () =>
createSelector(timelineSelectors.selectTimeline, (timeline) => {
return {
createdBy: timeline.createdBy,
expandedEvent: timeline.expandedEvent?.notes ?? {},
expandedDetail: timeline.expandedDetail ?? {},
eventIdToNoteIds: timeline?.eventIdToNoteIds ?? {},
noteIds: timeline.noteIds,
status: timeline.status,

View file

@ -135,7 +135,7 @@ In other use cases the message field can be used to concatenate different values
}
onEventClosed={[MockFunction]}
pinnedEventIds={Object {}}
showEventDetails={false}
showExpandedDetails={false}
sort={
Array [
Object {

View file

@ -96,7 +96,7 @@ describe('PinnedTabContent', () => {
itemsPerPageOptions: [5, 10, 20],
sort,
pinnedEventIds: {},
showEventDetails: false,
showExpandedDetails: false,
onEventClosed: jest.fn(),
};
});

View file

@ -25,11 +25,11 @@ import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../store/timeline/defaults';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { TimelineModel } from '../../../store/timeline/model';
import { EventDetails } from '../event_details';
import { ToggleExpandedEvent } from '../../../store/timeline/actions';
import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { State } from '../../../../common/store';
import { calculateTotalPages } from '../helpers';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { DetailsPanel } from '../../side_panel';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
@ -90,7 +90,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
itemsPerPageOptions,
pinnedEventIds,
onEventClosed,
showEventDetails,
showExpandedDetails,
sort,
}) => {
const { browserFields, docValueFields, loading: loadingSourcerer } = useSourcererScope(
@ -169,7 +169,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
timerangeKind: undefined,
});
const handleOnEventClosed = useCallback(() => {
const handleOnPanelClosed = useCallback(() => {
onEventClosed({ tabType: TimelineTabs.pinned, timelineId });
}, [timelineId, onEventClosed]);
@ -217,16 +217,16 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
</StyledEuiFlyoutFooter>
</EventDetailsWidthProvider>
</ScrollableFlexItem>
{showEventDetails && (
{showExpandedDetails && (
<>
<VerticalRule />
<ScrollableFlexItem grow={1}>
<EventDetails
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
handleOnPanelClosed={handleOnPanelClosed}
tabType={TimelineTabs.pinned}
timelineId={timelineId}
handleOnEventClosed={handleOnEventClosed}
/>
</ScrollableFlexItem>
</>
@ -242,7 +242,7 @@ const makeMapStateToProps = () => {
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
const {
columns,
expandedEvent,
expandedDetail,
itemsPerPage,
itemsPerPageOptions,
pinnedEventIds,
@ -255,7 +255,8 @@ const makeMapStateToProps = () => {
itemsPerPage,
itemsPerPageOptions,
pinnedEventIds,
showEventDetails: !!expandedEvent[TimelineTabs.pinned]?.eventId,
showExpandedDetails:
!!expandedDetail[TimelineTabs.pinned] && !!expandedDetail[TimelineTabs.pinned]?.panelView,
sort,
};
};
@ -263,8 +264,8 @@ const makeMapStateToProps = () => {
};
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
onEventClosed: (args: ToggleExpandedEvent) => {
dispatch(timelineActions.toggleExpandedEvent(args));
onEventClosed: (args: ToggleDetailPanel) => {
dispatch(timelineActions.toggleDetailPanel(args));
},
});
@ -278,7 +279,7 @@ const PinnedTabContent = connector(
(prevProps, nextProps) =>
prevProps.itemsPerPage === nextProps.itemsPerPage &&
prevProps.onEventClosed === nextProps.onEventClosed &&
prevProps.showEventDetails === nextProps.showEventDetails &&
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
prevProps.timelineId === nextProps.timelineId &&
deepEqual(prevProps.columns, nextProps.columns) &&
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&

View file

@ -262,7 +262,7 @@ In other use cases the message field can be used to concatenate different values
}
end="2018-03-24T03:33:52.253Z"
eventType="all"
expandedEvent={Object {}}
expandedDetail={Object {}}
filters={Array []}
isLive={false}
itemsPerPage={5}
@ -278,7 +278,7 @@ In other use cases the message field can be used to concatenate different values
onEventClosed={[MockFunction]}
show={true}
showCallOutUnauthorizedMsg={false}
showEventDetails={false}
showExpandedDetails={false}
sort={
Array [
Object {

View file

@ -96,9 +96,8 @@ describe('Timeline', () => {
columns: defaultHeaders,
dataProviders: mockDataProviders,
end: endDate,
expandedEvent: {},
eventType: 'all',
showEventDetails: false,
expandedDetail: {},
filters: [],
timelineId: TimelineId.test,
isLive: false,
@ -108,6 +107,7 @@ describe('Timeline', () => {
kqlQueryExpression: '',
onEventClosed: jest.fn(),
showCallOutUnauthorizedMsg: false,
showExpandedDetails: false,
sort,
start: startDate,
status: TimelineStatus.active,

View file

@ -46,12 +46,12 @@ import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timeline_events_count';
import { TimelineModel } from '../../../../timelines/store/timeline/model';
import { EventDetails } from '../event_details';
import { TimelineDatePickerLock } from '../date_picker_lock';
import { HideShowContainer } from '../styles';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { activeTimeline } from '../../../containers/active_timeline_context';
import { ToggleExpandedEvent } from '../../../store/timeline/actions';
import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { DetailsPanel } from '../../side_panel';
const TimelineHeaderContainer = styled.div`
margin-top: 6px;
@ -139,7 +139,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
dataProviders,
end,
eventType,
expandedEvent,
expandedDetail,
filters,
timelineId,
isLive,
@ -150,7 +150,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
onEventClosed,
show,
showCallOutUnauthorizedMsg,
showEventDetails,
showExpandedDetails,
start,
status,
sort,
@ -245,16 +245,17 @@ export const QueryTabContentComponent: React.FC<Props> = ({
timerangeKind,
});
const handleOnEventClosed = useCallback(() => {
const handleOnPanelClosed = useCallback(() => {
onEventClosed({ tabType: TimelineTabs.query, timelineId });
if (timelineId === TimelineId.active) {
activeTimeline.toggleExpandedEvent({
eventId: expandedEvent.eventId!,
indexName: expandedEvent.indexName!,
});
if (
expandedDetail[TimelineTabs.query]?.panelView &&
timelineId === TimelineId.active &&
showExpandedDetails
) {
activeTimeline.toggleExpandedDetail({});
}
}, [timelineId, onEventClosed, expandedEvent.eventId, expandedEvent.indexName]);
}, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
useEffect(() => {
setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
@ -350,16 +351,16 @@ export const QueryTabContentComponent: React.FC<Props> = ({
</StyledEuiFlyoutFooter>
</EventDetailsWidthProvider>
</ScrollableFlexItem>
{showEventDetails && (
{showExpandedDetails && (
<>
<VerticalRule />
<ScrollableFlexItem grow={1}>
<EventDetails
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
handleOnPanelClosed={handleOnPanelClosed}
tabType={TimelineTabs.query}
timelineId={timelineId}
handleOnEventClosed={handleOnEventClosed}
/>
</ScrollableFlexItem>
</>
@ -382,7 +383,7 @@ const makeMapStateToProps = () => {
columns,
dataProviders,
eventType,
expandedEvent,
expandedDetail,
filters,
itemsPerPage,
itemsPerPageOptions,
@ -406,7 +407,7 @@ const makeMapStateToProps = () => {
dataProviders,
eventType: eventType ?? 'raw',
end: input.timerange.to,
expandedEvent: expandedEvent[TimelineTabs.query] ?? {},
expandedDetail,
filters: timelineFilter,
timelineId,
isLive: input.policy.kind === 'interval',
@ -415,8 +416,9 @@ const makeMapStateToProps = () => {
kqlMode,
kqlQueryExpression,
showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state),
showEventDetails: !!expandedEvent[TimelineTabs.query]?.eventId,
show,
showExpandedDetails:
!!expandedDetail[TimelineTabs.query] && !!expandedDetail[TimelineTabs.query]?.panelView,
sort,
start: input.timerange.from,
status,
@ -437,8 +439,8 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
})
);
},
onEventClosed: (args: ToggleExpandedEvent) => {
dispatch(timelineActions.toggleExpandedEvent(args));
onEventClosed: (args: ToggleDetailPanel) => {
dispatch(timelineActions.toggleDetailPanel(args));
},
});
@ -460,7 +462,7 @@ const QueryTabContent = connector(
prevProps.onEventClosed === nextProps.onEventClosed &&
prevProps.show === nextProps.show &&
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
prevProps.showEventDetails === nextProps.showEventDetails &&
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
prevProps.status === nextProps.status &&
prevProps.timelineId === nextProps.timelineId &&
prevProps.updateEventTypeAndIndexesName === nextProps.updateEventTypeAndIndexesName &&

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { TimelineExpandedEventType } from '../../../common/types/timeline';
import {
TimelineExpandedDetail,
TimelineExpandedDetailType,
TimelineTabs,
} from '../../../common/types/timeline';
import { TimelineEventsAllRequestOptions } from '../../../common/search_strategy/timeline';
import { TimelineArgs } from '.';
@ -22,7 +26,7 @@ import { TimelineArgs } from '.';
class ActiveTimelineEvents {
private _activePage: number = 0;
private _expandedEvent: TimelineExpandedEventType = {};
private _expandedDetail: TimelineExpandedDetail = {};
private _pageName: string = '';
private _request: TimelineEventsAllRequestOptions | null = null;
private _response: TimelineArgs | null = null;
@ -35,20 +39,40 @@ class ActiveTimelineEvents {
this._activePage = activePage;
}
getExpandedEvent() {
return this._expandedEvent;
getExpandedDetail() {
return this._expandedDetail;
}
toggleExpandedEvent(expandedEvent: TimelineExpandedEventType) {
if (expandedEvent.eventId === this._expandedEvent.eventId) {
this._expandedEvent = {};
toggleExpandedDetail(expandedDetail: TimelineExpandedDetailType) {
const queryTab = TimelineTabs.query;
const currentExpandedDetail = this._expandedDetail[queryTab];
let isSameExpandedDetail;
// Check if the stored details matches the incoming detail
if (currentExpandedDetail?.panelView === 'eventDetail') {
isSameExpandedDetail =
expandedDetail?.panelView === 'eventDetail' &&
expandedDetail?.params?.eventId === currentExpandedDetail?.params?.eventId;
} else if (currentExpandedDetail?.panelView === 'hostDetail') {
isSameExpandedDetail =
expandedDetail?.panelView === 'hostDetail' &&
expandedDetail?.params?.hostName === currentExpandedDetail?.params?.hostName;
} else if (currentExpandedDetail?.panelView === 'networkDetail') {
isSameExpandedDetail =
expandedDetail?.panelView === 'networkDetail' &&
expandedDetail?.params?.ip === currentExpandedDetail?.params?.ip;
}
// if so, unset it, otherwise set it
if (isSameExpandedDetail) {
this._expandedDetail = {};
} else {
this._expandedEvent = expandedEvent;
this._expandedDetail = { [queryTab]: { ...expandedDetail } };
}
}
setExpandedEvent(expandedEvent: TimelineExpandedEventType) {
this._expandedEvent = expandedEvent;
setExpandedDetail(expandedDetail: TimelineExpandedDetail) {
this._expandedDetail = expandedDetail;
}
getPageName() {

View file

@ -113,7 +113,7 @@ export const useTimelineEvents = ({
clearSignalsState();
if (id === TimelineId.active) {
activeTimeline.setExpandedEvent({});
activeTimeline.setExpandedDetail({});
activeTimeline.setActivePage(newActivePage);
}
@ -178,7 +178,7 @@ export const useTimelineEvents = ({
updatedAt: Date.now(),
};
if (id === TimelineId.active) {
activeTimeline.setExpandedEvent({});
activeTimeline.setExpandedDetail({});
activeTimeline.setPageName(pageName);
activeTimeline.setRequest(request);
activeTimeline.setResponse(newTimelineResponse);

View file

@ -20,10 +20,10 @@ import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import {
TimelineEventsType,
TimelineExpandedEventType,
TimelineExpandedDetail,
TimelineExpandedDetailType,
TimelineTypeLiteral,
RowRendererId,
TimelineExpandedEvent,
TimelineTabs,
} from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
@ -38,12 +38,12 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
'ADD_NOTE_TO_EVENT'
);
export interface ToggleExpandedEvent {
event?: TimelineExpandedEventType;
export type ToggleDetailPanel = TimelineExpandedDetailType & {
tabType?: TimelineTabs;
timelineId: string;
}
export const toggleExpandedEvent = actionCreator<ToggleExpandedEvent>('TOGGLE_EXPANDED_EVENT');
};
export const toggleDetailPanel = actionCreator<ToggleDetailPanel>('TOGGLE_DETAIL_PANEL');
export const upsertColumn = actionCreator<{
column: ColumnHeaderOptions;
@ -67,7 +67,7 @@ export interface TimelineInput {
end: string;
};
excludedRowRendererIds?: RowRendererId[];
expandedEvent?: TimelineExpandedEvent;
expandedDetail?: TimelineExpandedDetail;
filters?: Filter[];
columns: ColumnHeaderOptions[];
itemsPerPage?: number;

View file

@ -25,7 +25,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
eventType: 'all',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
highlightedDropAndProviderId: '',
historyIds: [],
filters: [],

View file

@ -91,7 +91,7 @@ describe('Epic Timeline', () => {
description: '',
eventIdToNoteIds: {},
eventType: 'all',
expandedEvent: {},
expandedDetail: {},
excludedRowRendererIds: [],
highlightedDropAndProviderId: '',
historyIds: [],

View file

@ -82,7 +82,7 @@ describe('epicLocalStorage', () => {
dataProviders: mockDataProviders,
end: endDate,
eventType: 'all',
expandedEvent: {},
expandedDetail: {},
filters: [],
isLive: false,
itemsPerPage: 5,
@ -91,7 +91,7 @@ describe('epicLocalStorage', () => {
kqlQueryExpression: '',
onEventClosed: jest.fn(),
showCallOutUnauthorizedMsg: false,
showEventDetails: false,
showExpandedDetails: false,
start: startDate,
status: TimelineStatus.active,
sort,

View file

@ -8,6 +8,7 @@
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
import uuid from 'uuid';
import { ToggleDetailPanel } from './actions';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers';
@ -24,12 +25,13 @@ import { SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import {
TimelineEventsType,
TimelineExpandedEvent,
TimelineExpandedDetail,
TimelineTypeLiteral,
TimelineType,
RowRendererId,
TimelineStatus,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
@ -144,7 +146,7 @@ export const addTimelineToStore = ({
}: AddTimelineParams): TimelineById => {
if (shouldResetActiveTimelineContext(id, timelineById[id], timeline)) {
activeTimeline.setActivePage(0);
activeTimeline.setExpandedEvent({});
activeTimeline.setExpandedDetail({});
}
return {
...timelineById,
@ -171,7 +173,7 @@ interface AddNewTimelineParams {
end: string;
};
excludedRowRendererIds?: RowRendererId[];
expandedEvent?: TimelineExpandedEvent;
expandedDetail?: TimelineExpandedDetail;
filters?: Filter[];
id: string;
itemsPerPage?: number;
@ -192,7 +194,7 @@ export const addNewTimeline = ({
dataProviders = [],
dateRange: maybeDateRange,
excludedRowRendererIds = [],
expandedEvent = {},
expandedDetail = {},
filters = timelineDefaults.filters,
id,
itemsPerPage = timelineDefaults.itemsPerPage,
@ -221,7 +223,7 @@ export const addNewTimeline = ({
columns,
dataProviders,
dateRange,
expandedEvent,
expandedDetail,
excludedRowRendererIds,
filters,
itemsPerPage,
@ -1431,3 +1433,21 @@ export const updateExcludedRowRenderersIds = ({
},
};
};
export const updateTimelineDetailsPanel = (action: ToggleDetailPanel) => {
const { tabType } = action;
const panelViewOptions = new Set(['eventDetail', 'hostDetail', 'networkDetail']);
const expandedTabType = tabType ?? TimelineTabs.query;
return action.panelView && panelViewOptions.has(action.panelView)
? {
[expandedTabType]: {
params: action.params ? { ...action.params } : {},
panelView: action.panelView,
},
}
: {
[expandedTabType]: {},
};
};

View file

@ -14,7 +14,7 @@ import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'
import { SerializedFilterQuery } from '../../../common/store/types';
import type {
TimelineEventsType,
TimelineExpandedEvent,
TimelineExpandedDetail,
TimelineType,
TimelineStatus,
RowRendererId,
@ -63,7 +63,8 @@ export interface TimelineModel {
eventIdToNoteIds: Record<string, string[]>;
/** A list of Ids of excluded Row Renderers */
excludedRowRendererIds: RowRendererId[];
expandedEvent: TimelineExpandedEvent;
/** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */
expandedDetail: TimelineExpandedDetail;
filters?: Filter[];
/** When non-empty, display a graph view for this event */
graphEventId?: string;
@ -143,7 +144,7 @@ export type SubsetTimelineModel = Readonly<
| 'eventType'
| 'eventIdToNoteIds'
| 'excludedRowRendererIds'
| 'expandedEvent'
| 'expandedDetail'
| 'graphEventId'
| 'highlightedDropAndProviderId'
| 'historyIds'

View file

@ -79,7 +79,7 @@ const basicTimeline: TimelineModel = {
description: '',
eventIdToNoteIds: {},
excludedRowRendererIds: [],
expandedEvent: {},
expandedDetail: {},
highlightedDropAndProviderId: '',
historyIds: [],
id: 'foo',

View file

@ -35,7 +35,7 @@ import {
showCallOutUnauthorizedMsg,
showTimeline,
startTimelineSaving,
toggleExpandedEvent,
toggleDetailPanel,
unPinEvent,
updateAutoSaveMsg,
updateColumns,
@ -99,11 +99,12 @@ import {
updateSavedQuery,
updateGraphEventId,
updateFilters,
updateTimelineDetailsPanel,
updateTimelineEventType,
} from './helpers';
import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types';
import { TimelineType, TimelineTabs } from '../../../../common/types/timeline';
import { TimelineType } from '../../../../common/types/timeline';
export const initialTimelineState: TimelineState = {
timelineById: EMPTY_TIMELINE_BY_ID,
@ -130,6 +131,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
dataProviders,
dateRange,
excludedRowRendererIds,
expandedDetail = {},
show,
columns,
itemsPerPage,
@ -148,6 +150,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
dataProviders,
dateRange,
excludedRowRendererIds,
expandedDetail,
filters,
id,
itemsPerPage,
@ -178,22 +181,19 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }),
}))
.case(toggleExpandedEvent, (state, { tabType, timelineId, event = {} }) => {
const expandedTabType = tabType ?? TimelineTabs.query;
return {
...state,
timelineById: {
...state.timelineById,
[timelineId]: {
...state.timelineById[timelineId],
expandedEvent: {
...state.timelineById[timelineId].expandedEvent,
[expandedTabType]: event,
},
.case(toggleDetailPanel, (state, action) => ({
...state,
timelineById: {
...state.timelineById,
[action.timelineId]: {
...state.timelineById[action.timelineId],
expandedDetail: {
...state.timelineById[action.timelineId].expandedDetail,
...updateTimelineDetailsPanel(action),
},
},
};
})
},
}))
.case(addProvider, (state, { id, provider }) => ({
...state,
timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }),

View file

@ -19523,9 +19523,7 @@
"xpack.securitySolution.timeline.eventsTableAriaLabel": "イベント; {activePage}/{totalPages} ページ",
"xpack.securitySolution.timeline.expandableEvent.alertTitleLabel": "アラートの詳細",
"xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel": "閉じる",
"xpack.securitySolution.timeline.expandableEvent.copyToClipboardToolTip": "クリップボードにコピー",
"xpack.securitySolution.timeline.expandableEvent.eventTitleLabel": "イベントの詳細",
"xpack.securitySolution.timeline.expandableEvent.eventToolTipTitle": "イベント",
"xpack.securitySolution.timeline.expandableEvent.messageTitle": "メッセージ",
"xpack.securitySolution.timeline.expandableEvent.placeholder": "イベント詳細を表示するには、イベントを選択します",
"xpack.securitySolution.timeline.fieldTooltip": "フィールド",

View file

@ -19569,9 +19569,7 @@
"xpack.securitySolution.timeline.eventsTableAriaLabel": "事件;第 {activePage} 页,共 {totalPages} 页",
"xpack.securitySolution.timeline.expandableEvent.alertTitleLabel": "告警详情",
"xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel": "关闭",
"xpack.securitySolution.timeline.expandableEvent.copyToClipboardToolTip": "复制到剪贴板",
"xpack.securitySolution.timeline.expandableEvent.eventTitleLabel": "事件详情",
"xpack.securitySolution.timeline.expandableEvent.eventToolTipTitle": "事件",
"xpack.securitySolution.timeline.expandableEvent.messageTitle": "消息",
"xpack.securitySolution.timeline.expandableEvent.placeholder": "选择事件以显示事件详情",
"xpack.securitySolution.timeline.fieldTooltip": "字段",