[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:
Angela Chuang 2021-08-26 16:40:30 +01:00 committed by GitHub
parent 695280b756
commit 602392e88d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 485 additions and 95 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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