mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Host details fly out modal is not working in alerts table (#109942)
* fix expanded host and ip panel * reuse existing links components * rename * add unit tests * add unit tests * update comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
695280b756
commit
602392e88d
11 changed files with 485 additions and 95 deletions
|
@ -16,7 +16,7 @@ import {
|
|||
PropsForAnchor,
|
||||
PropsForButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useCallback, SyntheticEvent } from 'react';
|
||||
import { isNil } from 'lodash/fp';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -105,7 +105,8 @@ const HostDetailsLinkComponent: React.FC<{
|
|||
children?: React.ReactNode;
|
||||
hostName: string;
|
||||
isButton?: boolean;
|
||||
}> = ({ children, hostName, isButton }) => {
|
||||
onClick?: (e: SyntheticEvent) => void;
|
||||
}> = ({ children, hostName, isButton, onClick }) => {
|
||||
const { formatUrl, search } = useFormatUrl(SecurityPageName.hosts);
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const goToHostDetails = useCallback(
|
||||
|
@ -121,15 +122,17 @@ const HostDetailsLinkComponent: React.FC<{
|
|||
|
||||
return isButton ? (
|
||||
<LinkButton
|
||||
onClick={goToHostDetails}
|
||||
onClick={onClick ?? goToHostDetails}
|
||||
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
|
||||
data-test-subj="host-details-button"
|
||||
>
|
||||
{children ? children : hostName}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<LinkAnchor
|
||||
onClick={goToHostDetails}
|
||||
onClick={onClick ?? goToHostDetails}
|
||||
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
|
||||
data-test-subj="host-details-button"
|
||||
>
|
||||
{children ? children : hostName}
|
||||
</LinkAnchor>
|
||||
|
@ -177,7 +180,8 @@ const NetworkDetailsLinkComponent: React.FC<{
|
|||
ip: string;
|
||||
flowTarget?: FlowTarget | FlowTargetSourceDest;
|
||||
isButton?: boolean;
|
||||
}> = ({ children, ip, flowTarget = FlowTarget.source, isButton }) => {
|
||||
onClick?: (e: SyntheticEvent) => void | undefined;
|
||||
}> = ({ children, ip, flowTarget = FlowTarget.source, isButton, onClick }) => {
|
||||
const { formatUrl, search } = useFormatUrl(SecurityPageName.network);
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const goToNetworkDetails = useCallback(
|
||||
|
@ -194,14 +198,16 @@ const NetworkDetailsLinkComponent: React.FC<{
|
|||
return isButton ? (
|
||||
<LinkButton
|
||||
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}
|
||||
onClick={goToNetworkDetails}
|
||||
onClick={onClick ?? goToNetworkDetails}
|
||||
data-test-subj="network-details"
|
||||
>
|
||||
{children ? children : ip}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<LinkAnchor
|
||||
onClick={goToNetworkDetails}
|
||||
onClick={onClick ?? goToNetworkDetails}
|
||||
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}
|
||||
data-test-subj="network-details"
|
||||
>
|
||||
{children ? children : ip}
|
||||
</LinkAnchor>
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { FormattedIp } from './index';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { TimelineId, TimelineTabs } from '../../../../common';
|
||||
import { StatefulEventContext } from '../../../../../timelines/public';
|
||||
import { timelineActions } from '../../store/timeline';
|
||||
import { activeTimeline } from '../../containers/active_timeline_context';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useDispatch: jest.fn().mockReturnValue(jest.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/lib/kibana/kibana_react', () => {
|
||||
return {
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: jest.fn(),
|
||||
navigateToApp: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/components/drag_and_drop/draggable_wrapper', () => {
|
||||
const original = jest.requireActual('../../../common/components/drag_and_drop/draggable_wrapper');
|
||||
return {
|
||||
...original,
|
||||
// eslint-disable-next-line react/display-name
|
||||
DraggableWrapper: () => <div data-test-subj="DraggableWrapper" />,
|
||||
};
|
||||
});
|
||||
|
||||
describe('FormattedIp', () => {
|
||||
const props = {
|
||||
value: '192.168.1.1',
|
||||
contextId: 'test-context-id',
|
||||
eventId: 'test-event-id',
|
||||
isDraggable: false,
|
||||
fieldName: 'host.ip',
|
||||
};
|
||||
|
||||
let toggleDetailPanel: jest.SpyInstance;
|
||||
let toggleExpandedDetail: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
toggleDetailPanel = jest.spyOn(timelineActions, 'toggleDetailPanel');
|
||||
toggleExpandedDetail = jest.spyOn(activeTimeline, 'toggleExpandedDetail');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
toggleDetailPanel.mockClear();
|
||||
toggleExpandedDetail.mockClear();
|
||||
});
|
||||
test('should render ip address', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormattedIp {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toEqual(props.value);
|
||||
});
|
||||
|
||||
test('should render DraggableWrapper if isDraggable is true', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isDraggable: true,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormattedIp {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="DraggableWrapper"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('if not enableIpDetailsFlyout, should go to network details page', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormattedIp {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="network-details"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).not.toHaveBeenCalled();
|
||||
expect(toggleExpandedDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableIpDetailsFlyout, should open NetworkDetailsSidePanel', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: TimelineId.active,
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<FormattedIp {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="network-details"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).toHaveBeenCalledWith({
|
||||
panelView: 'networkDetail',
|
||||
params: {
|
||||
flowTarget: 'source',
|
||||
ip: props.value,
|
||||
},
|
||||
tabType: context.tabType,
|
||||
timelineId: context.timelineID,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableIpDetailsFlyout and timelineId equals to `timeline-1`, should call toggleExpandedDetail', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: TimelineId.active,
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<FormattedIp {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="network-details"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleExpandedDetail).toHaveBeenCalledWith({
|
||||
panelView: 'networkDetail',
|
||||
params: {
|
||||
flowTarget: 'source',
|
||||
ip: props.value,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableIpDetailsFlyout but timelineId not equals to `TimelineId.active`, should not call toggleExpandedDetail', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: 'detection',
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<FormattedIp {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="network-details"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).toHaveBeenCalledWith({
|
||||
panelView: 'networkDetail',
|
||||
params: {
|
||||
flowTarget: 'source',
|
||||
ip: props.value,
|
||||
},
|
||||
tabType: context.tabType,
|
||||
timelineId: context.timelineID,
|
||||
});
|
||||
expect(toggleExpandedDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -31,11 +31,8 @@ import {
|
|||
} 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';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { useFormatUrl, getNetworkDetailsUrl } from '../../../common/components/link_to';
|
||||
import { encodeIpv6 } from '../../../common/lib/helpers';
|
||||
import { NetworkDetailsLink } from '../../../common/components/links';
|
||||
import { StatefulEventContext } from '../../../../../timelines/public';
|
||||
|
||||
const getUniqueId = ({
|
||||
contextId,
|
||||
|
@ -168,8 +165,8 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
|||
|
||||
const dispatch = useDispatch();
|
||||
const eventContext = useContext(StatefulEventContext);
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.network);
|
||||
const isInTimelineContext = address && eventContext?.tabType && eventContext?.timelineID;
|
||||
const isInTimelineContext =
|
||||
address && eventContext?.enableIpDetailsFlyout && eventContext?.timelineID;
|
||||
|
||||
const openNetworkDetailsSidePanel = useCallback(
|
||||
(e) => {
|
||||
|
@ -202,21 +199,19 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
|||
[eventContext, isInTimelineContext, address, fieldName, dispatch]
|
||||
);
|
||||
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
// When this component is used outside of timeline/alerts table (i.e. in the flyout) we would still like it to link to the IP Overview page
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<Content field={fieldName} tooltipContent={fieldName}>
|
||||
<LinkAnchor
|
||||
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(address))))}
|
||||
data-test-subj="network-details"
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
// When this component is used outside of timeline (i.e. in the flyout) we would still like it to link to the IP Overview page
|
||||
<NetworkDetailsLink
|
||||
ip={address}
|
||||
isButton={false}
|
||||
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
|
||||
>
|
||||
{address}
|
||||
</LinkAnchor>
|
||||
/>
|
||||
</Content>
|
||||
),
|
||||
[address, fieldName, formatUrl, isInTimelineContext, openNetworkDetailsSidePanel]
|
||||
[address, fieldName, isInTimelineContext, openNetworkDetailsSidePanel]
|
||||
);
|
||||
|
||||
const render = useCallback(
|
||||
|
|
|
@ -40,7 +40,7 @@ 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';
|
||||
import { StatefulEventContext } from '../../../../../../../timelines/public';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
|
@ -103,7 +103,13 @@ 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 [activeStatefulEventContext] = useState({
|
||||
timelineID: timelineId,
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
tabType,
|
||||
});
|
||||
|
||||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedDetail = useDeepEqualSelector(
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { 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);
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { HostName } from './host_name';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common';
|
||||
import { StatefulEventContext } from '../../../../../../../timelines/public';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { activeTimeline } from '../../../../containers/active_timeline_context';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const origin = jest.requireActual('react-redux');
|
||||
return {
|
||||
...origin,
|
||||
useDispatch: jest.fn().mockReturnValue(jest.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana/kibana_react', () => {
|
||||
return {
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: jest.fn(),
|
||||
navigateToApp: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../common/components/draggables', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
DefaultDraggable: () => <div data-test-subj="DefaultDraggable" />,
|
||||
}));
|
||||
|
||||
describe('HostName', () => {
|
||||
const props = {
|
||||
fieldName: 'host.name',
|
||||
contextId: 'test-context-id',
|
||||
eventId: 'test-event-id',
|
||||
isDraggable: false,
|
||||
value: 'Mock Host',
|
||||
};
|
||||
|
||||
let toggleDetailPanel: jest.SpyInstance;
|
||||
let toggleExpandedDetail: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
toggleDetailPanel = jest.spyOn(timelineActions, 'toggleDetailPanel');
|
||||
toggleExpandedDetail = jest.spyOn(activeTimeline, 'toggleExpandedDetail');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
toggleDetailPanel.mockClear();
|
||||
toggleExpandedDetail.mockClear();
|
||||
});
|
||||
test('should render host name', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HostName {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="host-details-button"]').last().text()).toEqual(
|
||||
props.value
|
||||
);
|
||||
});
|
||||
|
||||
test('should render DefaultDraggable if isDraggable is true', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isDraggable: true,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HostName {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="DefaultDraggable"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('if not enableHostDetailsFlyout, should go to hostdetails page', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HostName {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).not.toHaveBeenCalled();
|
||||
expect(toggleExpandedDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableHostDetailsFlyout, should open HostDetailsSidePanel', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: TimelineId.active,
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<HostName {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).toHaveBeenCalledWith({
|
||||
panelView: 'hostDetail',
|
||||
params: {
|
||||
hostName: props.value,
|
||||
},
|
||||
tabType: context.tabType,
|
||||
timelineId: context.timelineID,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableHostDetailsFlyout and timelineId equals to `timeline-1`, should call toggleExpandedDetail', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: TimelineId.active,
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<HostName {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleExpandedDetail).toHaveBeenCalledWith({
|
||||
panelView: 'hostDetail',
|
||||
params: {
|
||||
hostName: props.value,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if enableHostDetailsFlyout but timelineId not equals to `TimelineId.active`, should not call toggleExpandedDetail', async () => {
|
||||
const context = {
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
timelineID: 'detection',
|
||||
tabType: TimelineTabs.query,
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatefulEventContext.Provider value={context}>
|
||||
<HostName {...props} />
|
||||
</StatefulEventContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(toggleDetailPanel).toHaveBeenCalledWith({
|
||||
panelView: 'hostDetail',
|
||||
params: {
|
||||
hostName: props.value,
|
||||
},
|
||||
tabType: context.tabType,
|
||||
timelineId: context.timelineID,
|
||||
});
|
||||
expect(toggleExpandedDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isString } from 'lodash/fp';
|
||||
import { LinkAnchor } from '../../../../../common/components/links';
|
||||
import { HostDetailsLink } from '../../../../../common/components/links';
|
||||
import {
|
||||
TimelineId,
|
||||
TimelineTabs,
|
||||
|
@ -17,11 +17,9 @@ import {
|
|||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
|
||||
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';
|
||||
import { SecurityPageName } from '../../../../../../common/constants';
|
||||
import { useFormatUrl, getHostDetailsUrl } from '../../../../../common/components/link_to';
|
||||
import { StatefulEventContext } from '../../../../../../../timelines/public';
|
||||
|
||||
interface Props {
|
||||
contextId: string;
|
||||
|
@ -41,10 +39,8 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
const dispatch = useDispatch();
|
||||
const eventContext = useContext(StatefulEventContext);
|
||||
const hostName = `${value}`;
|
||||
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.hosts);
|
||||
const isInTimelineContext = hostName && eventContext?.tabType && eventContext?.timelineID;
|
||||
|
||||
const isInTimelineContext =
|
||||
hostName && eventContext?.enableHostDetailsFlyout && eventContext?.timelineID;
|
||||
const openHostDetailsSidePanel = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -73,19 +69,19 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
[dispatch, eventContext, isInTimelineContext, hostName]
|
||||
);
|
||||
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
// When this component is used outside of timeline/alerts table (i.e. in the flyout) we would still like it to link to the Host Details page
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<LinkAnchor
|
||||
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
|
||||
data-test-subj="host-details-button"
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
// When this component is used outside of timeline (i.e. in the flyout) we would still like it to link to the Host Details page
|
||||
<HostDetailsLink
|
||||
hostName={hostName}
|
||||
isButton={false}
|
||||
onClick={isInTimelineContext ? openHostDetailsSidePanel : undefined}
|
||||
>
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">{hostName}</TruncatableText>
|
||||
</LinkAnchor>
|
||||
</HostDetailsLink>
|
||||
),
|
||||
[formatUrl, hostName, isInTimelineContext, openHostDetailsSidePanel]
|
||||
[hostName, isInTimelineContext, openHostDetailsSidePanel]
|
||||
);
|
||||
|
||||
return isString(value) && hostName.length > 0 ? (
|
||||
|
|
|
@ -128,9 +128,13 @@ describe('Body', () => {
|
|||
expect(wrapper.find('div.euiDataGridRowCell').first().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test.skip('it renders a tooltip for timestamp', () => {
|
||||
test('it renders cell value', () => {
|
||||
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
|
||||
const testProps = { ...props, columnHeaders: headersJustTimestamp };
|
||||
const testProps = {
|
||||
...props,
|
||||
columnHeaders: headersJustTimestamp,
|
||||
data: mockTimelineData.slice(0, 1),
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
|
@ -139,10 +143,10 @@ describe('Body', () => {
|
|||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="data-driven-columns"]')
|
||||
.first()
|
||||
.find('[data-test-subj="statefulCell"]')
|
||||
.last()
|
||||
.find('[data-test-subj="dataGridRowCell"]')
|
||||
.at(0)
|
||||
.find('.euiDataGridRowCell__truncate')
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toEqual(mockTimelineData[0].ecs.timestamp);
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
|||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import type { OnRowSelected, OnSelectAll } from '../types';
|
||||
import type { Refetch } from '../../../store/t_grid/inputs';
|
||||
import { StatefulFieldsBrowser } from '../../../';
|
||||
import { StatefulEventContext, StatefulFieldsBrowser } from '../../../';
|
||||
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { RowAction } from './row_action';
|
||||
|
@ -659,39 +659,48 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
return Cell;
|
||||
}, [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers]);
|
||||
|
||||
// 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: id,
|
||||
tabType,
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{tableView === 'gridView' && (
|
||||
<EuiDataGrid
|
||||
data-test-subj="body-data-grid"
|
||||
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={gridStyle}
|
||||
leadingControlColumns={leadingTGridControlColumns}
|
||||
trailingControlColumns={trailingTGridControlColumns}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
rowCount={data.length}
|
||||
renderCellValue={renderTGridCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
/>
|
||||
)}
|
||||
{tableView === 'eventRenderedView' && (
|
||||
<EventRenderedView
|
||||
alertToolbar={alertToolbar}
|
||||
browserFields={browserFields}
|
||||
events={data}
|
||||
leadingControlColumns={leadingTGridControlColumns ?? []}
|
||||
onChangePage={loadPage}
|
||||
pageIndex={activePage}
|
||||
pageSize={querySize}
|
||||
pageSizeOptions={itemsPerPageOptions}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={id}
|
||||
totalItemCount={totalItems}
|
||||
/>
|
||||
)}
|
||||
<StatefulEventContext.Provider value={activeStatefulEventContext}>
|
||||
{tableView === 'gridView' && (
|
||||
<EuiDataGrid
|
||||
data-test-subj="body-data-grid"
|
||||
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={gridStyle}
|
||||
leadingControlColumns={leadingTGridControlColumns}
|
||||
trailingControlColumns={trailingTGridControlColumns}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
rowCount={data.length}
|
||||
renderCellValue={renderTGridCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
/>
|
||||
)}
|
||||
{tableView === 'eventRenderedView' && (
|
||||
<EventRenderedView
|
||||
alertToolbar={alertToolbar}
|
||||
browserFields={browserFields}
|
||||
events={data}
|
||||
leadingControlColumns={leadingTGridControlColumns ?? []}
|
||||
onChangePage={loadPage}
|
||||
pageIndex={activePage}
|
||||
pageSize={querySize}
|
||||
pageSizeOptions={itemsPerPageOptions}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={id}
|
||||
totalItemCount={totalItems}
|
||||
/>
|
||||
)}
|
||||
</StatefulEventContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { PluginInitializerContext } from '../../../../src/core/public';
|
||||
|
||||
import { TimelinesPlugin } from './plugin';
|
||||
import type { StatefulEventContextType } from './types';
|
||||
export * as tGridActions from './store/t_grid/actions';
|
||||
export * as tGridSelectors from './store/t_grid/selectors';
|
||||
export type {
|
||||
|
@ -59,3 +61,5 @@ export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items';
|
|||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new TimelinesPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export const StatefulEventContext = createContext<StatefulEventContextType | null>(null);
|
||||
|
|
|
@ -24,6 +24,7 @@ import type { TGridStandaloneProps } from './components/t_grid/standalone';
|
|||
import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline';
|
||||
import { HoverActionsConfig } from './components/hover_actions/index';
|
||||
import type { AddToCaseActionProps } from './components/actions/timeline/cases/add_to_case_action';
|
||||
import { TimelineTabs } from '../common';
|
||||
export * from './store/t_grid';
|
||||
export interface TimelinesUIStart {
|
||||
getHoverActions: () => HoverActionsConfig;
|
||||
|
@ -66,3 +67,10 @@ export type GetTGridProps<T extends TGridType> = T extends 'standalone'
|
|||
? TGridIntegratedCompProps
|
||||
: TGridIntegratedCompProps;
|
||||
export type TGridProps = TGridStandaloneCompProps | TGridIntegratedCompProps;
|
||||
|
||||
export interface StatefulEventContextType {
|
||||
tabType: TimelineTabs | undefined;
|
||||
timelineID: string;
|
||||
enableHostDetailsFlyout: boolean;
|
||||
enableIpDetailsFlyout: boolean;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue