mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Conflicts: # x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx
This commit is contained in:
parent
852ef0b643
commit
b121769bb6
68 changed files with 3002 additions and 618 deletions
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))))}
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -214,7 +214,7 @@ export const mockGlobalState: State = {
|
|||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
excludedRowRendererIds: [],
|
||||
expandedEvent: {},
|
||||
expandedDetail: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
isFavorite: false,
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -156,7 +156,7 @@ describe('alert actions', () => {
|
|||
eventIdToNoteIds: {},
|
||||
eventType: 'all',
|
||||
excludedRowRendererIds: [],
|
||||
expandedEvent: {},
|
||||
expandedDetail: {},
|
||||
filters: [
|
||||
{
|
||||
$state: {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -147,6 +147,7 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
id={id}
|
||||
inspect={inspect}
|
||||
ip={ip}
|
||||
isInDetailsSidePanel={false}
|
||||
data={networkDetails}
|
||||
anomaliesData={anomaliesData}
|
||||
loading={loading}
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]}
|
||||
>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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(','),
|
||||
},
|
||||
|
|
1029
x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
generated
Normal file
1029
x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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;
|
|
@ -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
|
||||
);
|
|
@ -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',
|
||||
{
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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 />
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -96,7 +96,7 @@ describe('PinnedTabContent', () => {
|
|||
itemsPerPageOptions: [5, 10, 20],
|
||||
sort,
|
||||
pinnedEventIds: {},
|
||||
showEventDetails: false,
|
||||
showExpandedDetails: false,
|
||||
onEventClosed: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -25,7 +25,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick<TimelineModel, 'filter
|
|||
eventType: 'all',
|
||||
eventIdToNoteIds: {},
|
||||
excludedRowRendererIds: [],
|
||||
expandedEvent: {},
|
||||
expandedDetail: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
filters: [],
|
||||
|
|
|
@ -91,7 +91,7 @@ describe('Epic Timeline', () => {
|
|||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
eventType: 'all',
|
||||
expandedEvent: {},
|
||||
expandedDetail: {},
|
||||
excludedRowRendererIds: [],
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]: {},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -79,7 +79,7 @@ const basicTimeline: TimelineModel = {
|
|||
description: '',
|
||||
eventIdToNoteIds: {},
|
||||
excludedRowRendererIds: [],
|
||||
expandedEvent: {},
|
||||
expandedDetail: {},
|
||||
highlightedDropAndProviderId: '',
|
||||
historyIds: [],
|
||||
id: 'foo',
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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": "フィールド",
|
||||
|
|
|
@ -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": "字段",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue