[RAC] [SECURIT SOLUTIUONS] Remove drag drop security solutions (#106721)

* wip to remove drag & drop

* fix timeline action visibility and filter present on scroll

* remove unused class

* clean up hover actions

* add isDraggable on row render to allow the control of drag and drop

* fix add to timeline to work the old way

* fix types + unit test

* fix cypress test, I went to  fast  with the renaming

* review I

Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
This commit is contained in:
Xavier Mouligneau 2021-07-28 20:12:10 -04:00 committed by GitHub
parent 5e2c22cef9
commit 3f14abb372
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
148 changed files with 2170 additions and 2292 deletions

View file

@ -44,7 +44,7 @@ describe('timeline data providers', () => {
closeTimeline();
});
it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => {
it.skip('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => {
dragAndDropFirstHostToTimeline();
openTimelineUsingToggle();
cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`)
@ -78,7 +78,7 @@ describe('timeline data providers', () => {
});
});
it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => {
it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => {
dragFirstHostToTimeline();
cy.get(IS_DRAGGING_DATA_PROVIDERS)
@ -87,7 +87,7 @@ describe('timeline data providers', () => {
.should('have.class', 'drop-target-data-providers');
});
it('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => {
it.skip('render an extra highlighted area in dataProvider when the user starts dragging a host AND is hovering over the data providers', () => {
dragFirstHostToEmptyTimelineDataProviders();
cy.get(IS_DRAGGING_DATA_PROVIDERS)

View file

@ -79,7 +79,7 @@ describe('timeline flyout button', () => {
closeTimelineUsingCloseButton();
});
it('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => {
it.skip('sets correct classes when the user starts dragging a host, but is not hovering over the data providers', () => {
dragFirstHostToTimeline();
cy.get(IS_DRAGGING_DATA_PROVIDERS)

View file

@ -7,6 +7,6 @@
export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]';
export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"] a.euiLink';
export const HOSTS_NAMES = '[data-test-subj="render-content-host.name"] a.euiLink';
export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="draggable-content-host.name"]';
export const HOSTS_NAMES_DRAGGABLE = '[data-test-subj="render-content-host.name"]';

View file

@ -5,6 +5,6 @@
* 2.0.
*/
export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]';
export const PROCESS_NAME_FIELD = '[data-test-subj="render-content-process.name"]';
export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]';

View file

@ -340,13 +340,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => {
const dataProviderId = `draggableId.content.draggable-legend-item-uuid_v4()-${escapeDataProviderId(
stackByField
)}-${escapeDataProviderId(datum.key)}`;
expect(
wrapper
.find(`[draggableId="${dataProviderId}"] [data-test-subj="providerContainer"]`)
.first()
.text()
).toEqual(datum.key);
expect(wrapper.find(`div[data-provider-id="${dataProviderId}"]`).first().text()).toEqual(
datum.key
);
});
});
});

View file

@ -46,6 +46,7 @@ const DraggableLegendItemComponent: React.FC<{
data-test-subj={`legend-item-${dataProviderId}`}
field={field}
id={dataProviderId}
isDraggable={false}
timelineId={timelineId}
value={value}
/>

View file

@ -17,6 +17,7 @@ exports[`DraggableWrapper rendering it renders against the snapshot 1`] = `
},
}
}
isDraggable={true}
render={[Function]}
/>
`;

View file

@ -42,7 +42,11 @@ describe('DraggableWrapper', () => {
const wrapper = shallow(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
/>
</DragDropContextWrapper>
</TestProviders>
);
@ -54,7 +58,11 @@ describe('DraggableWrapper', () => {
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
/>
</DragDropContextWrapper>
</TestProviders>
);
@ -66,19 +74,27 @@ describe('DraggableWrapper', () => {
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
/>
</DragDropContextWrapper>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(false);
expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(false);
});
test('it renders hover actions when the mouse is over the text of draggable wrapper', async () => {
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
/>
</DragDropContextWrapper>
</TestProviders>
);
@ -88,7 +104,7 @@ describe('DraggableWrapper', () => {
wrapper.update();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true);
});
});
});
@ -98,7 +114,12 @@ describe('DraggableWrapper', () => {
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} truncate />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
truncate
/>
</DragDropContextWrapper>
</TestProviders>
);
@ -112,7 +133,11 @@ describe('DraggableWrapper', () => {
const wrapper = mount(
<TestProviders>
<DragDropContextWrapper browserFields={mockBrowserFields}>
<DraggableWrapper dataProvider={dataProvider} render={() => message} />
<DraggableWrapper
dataProvider={dataProvider}
isDraggable={true}
render={() => message}
/>
</DragDropContextWrapper>
</TestProviders>
);

View file

@ -7,7 +7,7 @@
import { EuiScreenReaderOnly } from '@elastic/eui';
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Draggable,
DraggableProvided,
@ -25,12 +25,13 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
import * as i18n from './translations';
import { useKibana } from '../../lib/kibana';
import { useHoverActions } from '../hover_actions/use_hover_actions';
// As right now, we do not know what we want there, we will keep it as a placeholder
export const DragEffects = styled.div``;
@ -80,7 +81,7 @@ const Wrapper = styled.div<WrapperProps>`
Wrapper.displayName = 'Wrapper';
const ProviderContentWrapper = styled.span`
export const ProviderContentWrapper = styled.span`
> span.euiToolTipAnchor {
display: block; /* allow EuiTooltip content to be truncatable */
}
@ -95,6 +96,7 @@ type RenderFunctionProp = (
interface Props {
dataProvider: DataProvider;
disabled?: boolean;
isDraggable?: boolean;
inline?: boolean;
render: RenderFunctionProp;
timelineId?: string;
@ -121,55 +123,35 @@ export const getStyle = (
};
};
const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => {
const links = draggableElement?.querySelectorAll('.euiLink') ?? [];
return links.length > 0;
};
const DraggableWrapperComponent: React.FC<Props> = ({
const DraggableOnWrapperComponent: React.FC<Props> = ({
dataProvider,
onFilterAdded,
render,
timelineId,
truncate,
}) => {
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
const draggableRef = useRef<HTMLDivElement | null>(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [showTopN, setShowTopN] = useState<boolean>(false);
const [goGetTimelineId, setGoGetTimelineId] = useState(false);
const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId);
const [providerRegistered, setProviderRegistered] = useState(false);
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
const dispatch = useDispatch();
const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => {
if (prevHoverActionsOwnFocus) {
setTimeout(() => {
keyboardHandlerRef.current?.focus();
}, 0);
}
return false; // always give up ownership
});
setTimeout(() => {
setHoverActionsOwnFocus(false);
}, 0); // invoked on the next tick, because we want to restore focus first
}, [keyboardHandlerRef]);
const toggleTopN = useCallback(() => {
setShowTopN((prevShowTopN) => {
const newShowTopN = !prevShowTopN;
if (newShowTopN === false) {
handleClosePopOverTrigger();
}
return newShowTopN;
});
}, [handleClosePopOverTrigger]);
const {
closePopOverTrigger,
handleClosePopOverTrigger,
hoverActionsOwnFocus,
hoverContent,
keyboardHandlerRef,
onCloseRequested,
openPopover,
onFocus,
setContainerRef,
showTopN,
} = useHoverActions({
dataProvider,
onFilterAdded,
render,
timelineId,
truncate,
});
const registerProvider = useCallback(() => {
if (!isDisabled) {
@ -192,49 +174,6 @@ const DraggableWrapperComponent: React.FC<Props> = ({
[unRegisterProvider]
);
const hoverContent = useMemo(() => {
// display links as additional content in the hover menu to enable keyboard
// navigation of links (when the draggable contains them):
const additionalContent =
hoverActionsOwnFocus && !showTopN && draggableContainsLinks(draggableRef.current) ? (
<ProviderContentWrapper
data-test-subj={`draggable-link-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, null, { isDragging: false, isDropAnimating: false })}
</ProviderContentWrapper>
) : null;
return (
<DraggableWrapperHoverContent
additionalContent={additionalContent}
closePopOver={handleClosePopOverTrigger}
draggableId={getDraggableId(dataProvider.id)}
field={dataProvider.queryMatch.field}
goGetTimelineId={setGoGetTimelineId}
onFilterAdded={onFilterAdded}
ownFocus={hoverActionsOwnFocus}
showTopN={showTopN}
timelineId={timelineId ?? timelineIdFind}
toggleTopN={toggleTopN}
value={
typeof dataProvider.queryMatch.value !== 'number'
? dataProvider.queryMatch.value
: `${dataProvider.queryMatch.value}`
}
/>
);
}, [
dataProvider,
handleClosePopOverTrigger,
hoverActionsOwnFocus,
onFilterAdded,
render,
showTopN,
timelineId,
timelineIdFind,
toggleTopN,
]);
const RenderClone = useCallback(
(provided, snapshot) => (
<ConditionalPortal registerProvider={registerProvider}>
@ -264,7 +203,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
{...provided.dragHandleProps}
ref={(e: HTMLDivElement) => {
provided.innerRef(e);
draggableRef.current = e;
setContainerRef(e);
}}
data-test-subj="providerContainer"
isDragging={snapshot.isDragging}
@ -292,13 +231,9 @@ const DraggableWrapperComponent: React.FC<Props> = ({
)}
</ProviderContainer>
),
[dataProvider, registerProvider, render, truncate]
[dataProvider, registerProvider, render, setContainerRef, truncate]
);
const openPopover = useCallback(() => {
setHoverActionsOwnFocus(true);
}, []);
const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableId(dataProvider.id),
@ -307,24 +242,6 @@ const DraggableWrapperComponent: React.FC<Props> = ({
openPopover,
});
const onFocus = useCallback(() => {
if (!hoverActionsOwnFocus) {
keyboardHandlerRef.current?.focus();
}
}, [hoverActionsOwnFocus, keyboardHandlerRef]);
const onCloseRequested = useCallback(() => {
setShowTopN(false);
if (hoverActionsOwnFocus) {
setHoverActionsOwnFocus(false);
setTimeout(() => {
onFocus(); // return focus to this draggable on the next tick, because we owned focus
}, 0);
}
}, [onFocus, hoverActionsOwnFocus]);
const DroppableContent = useCallback(
(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
@ -350,7 +267,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
{droppableProvided.placeholder}
</div>
),
[DraggableContent, dataProvider.id, isDisabled, onBlur, onFocus, onKeyDown]
[DraggableContent, dataProvider.id, isDisabled, keyboardHandlerRef, onBlur, onFocus, onKeyDown]
);
const content = useMemo(
@ -385,6 +302,75 @@ const DraggableWrapperComponent: React.FC<Props> = ({
);
};
const DraggableWrapperComponent: React.FC<Props> = ({
dataProvider,
isDraggable = false,
onFilterAdded,
render,
timelineId,
truncate,
}) => {
const {
closePopOverTrigger,
hoverActionsOwnFocus,
hoverContent,
onCloseRequested,
setContainerRef,
showTopN,
} = useHoverActions({
dataProvider,
isDraggable,
onFilterAdded,
render,
timelineId,
truncate,
});
const renderContent = useCallback(
() => (
<div
ref={(e: HTMLDivElement) => {
setContainerRef(e);
}}
tabIndex={-1}
data-provider-id={getDraggableId(dataProvider.id)}
>
{truncate ? (
<TruncatableText data-test-subj="render-truncatable-content">
{render(dataProvider, null, { isDragging: false, isDropAnimating: false })}
</TruncatableText>
) : (
<ProviderContentWrapper
data-test-subj={`render-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, null, { isDragging: false, isDropAnimating: false })}
</ProviderContentWrapper>
)}
</div>
),
[dataProvider, render, setContainerRef, truncate]
);
if (!isDraggable) {
return (
<WithHoverActions
alwaysShow={showTopN || hoverActionsOwnFocus}
closePopOverTrigger={closePopOverTrigger}
hoverContent={hoverContent}
onCloseRequested={onCloseRequested}
render={renderContent}
/>
);
}
return (
<DraggableOnWrapperComponent
dataProvider={dataProvider}
onFilterAdded={onFilterAdded}
render={render}
timelineId={timelineId}
truncate={truncate}
/>
);
};
export const DraggableWrapper = React.memo(DraggableWrapperComponent);
DraggableWrapper.displayName = 'DraggableWrapper';

View file

@ -1,564 +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 { waitFor } from '@testing-library/react';
import { mount, ReactWrapper } from 'enzyme';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { mockBrowserFields } from '../../containers/source/mock';
import '../../mock/match_media';
import { useKibana } from '../../lib/kibana';
import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
import { TimelineId } from '../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
jest.mock('../link_to');
jest.mock('../../lib/kibana');
jest.mock('../../containers/sourcerer', () => {
const original = jest.requireActual('../../containers/sourcerer');
return {
...original,
useSourcererScope: jest.fn(),
};
});
jest.mock('uuid', () => {
return {
v1: jest.fn(() => 'uuid.v1()'),
v4: jest.fn(() => 'uuid.v4()'),
};
});
const mockStartDragToTimeline = jest.fn();
jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => {
const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline');
return {
...original,
useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }),
};
});
const mockAddFilters = jest.fn();
jest.mock('../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
useDeepEqualSelector: jest.fn(),
}));
jest.mock('../../../common/hooks/use_invalid_filter_query.tsx');
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const timelineId = TimelineId.active;
const field = 'process.name';
const value = 'nice';
const toggleTopN = jest.fn();
const goGetTimelineId = jest.fn();
const defaultProps = {
field,
goGetTimelineId,
ownFocus: false,
showTopN: false,
timelineId,
toggleTopN,
value,
};
describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
mockStartDragToTimeline.mockReset();
(useDeepEqualSelector as jest.Mock).mockReturnValue({
filterManager: { addFilters: mockAddFilters },
});
(useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
selectedPatterns: [],
indexPattern: {},
});
});
/**
* The tests for "Filter for value" and "Filter out value" are similar enough
* to combine them into "table tests" using this array
*/
const forOrOut = ['for', 'out'];
forOrOut.forEach((hoverAction) => {
describe(`Filter ${hoverAction} value`, () => {
beforeEach(() => {
jest.clearAllMocks();
});
test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...defaultProps} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists()
).toBe(true);
});
test(`it does NOT render the 'Filter ${hoverAction} value' button when showTopN is true`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...{ ...defaultProps, showTopN: true }} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists()
).toBe(false);
});
test(`it should call goGetTimelineId when user is over the 'Filter ${hoverAction} value' button`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...{ ...defaultProps, timelineId: undefined }} />
</TestProviders>
);
const button = wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first();
button.simulate('mouseenter');
expect(goGetTimelineId).toHaveBeenCalledWith(true);
});
describe('when run in the context of a timeline', () => {
let wrapper: ReactWrapper;
let onFilterAdded: () => void;
beforeEach(() => {
onFilterAdded = jest.fn();
wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded }} />
</TestProviders>
);
});
test('when clicked, it adds a filter to the timeline when running in the context of a timeline', () => {
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(mockAddFilters).toBeCalledWith({
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: hoverAction === 'out' ? true : false,
params: { query: 'nice' },
type: 'phrase',
value: 'nice',
},
query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } },
});
});
test('when clicked, invokes onFilterAdded when running in the context of a timeline', () => {
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(onFilterAdded).toBeCalled();
});
});
describe('when NOT run in the context of a timeline', () => {
let wrapper: ReactWrapper;
let onFilterAdded: () => void;
const kibana = useKibana();
beforeEach(() => {
kibana.services.data.query.filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{ ...defaultProps, onFilterAdded, timelineId: TimelineId.test }}
/>
</TestProviders>
);
});
test('when clicked, it adds a filter to the global filters when NOT running in the context of a timeline', () => {
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith({
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: hoverAction === 'out' ? true : false,
params: { query: 'nice' },
type: 'phrase',
value: 'nice',
},
query: { match: { 'process.name': { query: 'nice', type: 'phrase' } } },
});
});
test('when clicked, invokes onFilterAdded when NOT running in the context of a timeline', () => {
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(onFilterAdded).toBeCalled();
});
});
describe('an empty string value when run in the context of a timeline', () => {
let filterManager: FilterManager;
let wrapper: ReactWrapper;
let onFilterAdded: () => void;
beforeEach(() => {
filterManager = new FilterManager(mockUiSettingsForFilterManager);
filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded, value: '' }} />
</TestProviders>
);
});
const expectedFilterTypeDescription =
hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"';
test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the timeline when run in the context of a timeline`, () => {
const expected =
hoverAction === 'for'
? {
exists: { field: 'process.name' },
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: true,
type: 'exists',
value: 'exists',
},
}
: {
exists: { field: 'process.name' },
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: false,
type: 'exists',
value: 'exists',
},
};
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(mockAddFilters).toBeCalledWith(expected);
});
});
describe('an empty string value when NOT run in the context of a timeline', () => {
let wrapper: ReactWrapper;
let onFilterAdded: () => void;
const kibana = useKibana();
beforeEach(() => {
kibana.services.data.query.filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
onFilterAdded,
timelineId: TimelineId.test,
value: '',
}}
/>
</TestProviders>
);
});
const expectedFilterTypeDescription =
hoverAction === 'for' ? 'a "NOT exists"' : 'an "exists"';
test(`when clicked, it adds ${expectedFilterTypeDescription} filter to the global filters when NOT running in the context of a timeline`, () => {
const expected =
hoverAction === 'for'
? {
exists: { field: 'process.name' },
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: true,
type: 'exists',
value: 'exists',
},
}
: {
exists: { field: 'process.name' },
meta: {
alias: null,
disabled: false,
key: 'process.name',
negate: false,
type: 'exists',
value: 'exists',
},
};
wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click');
wrapper.update();
expect(kibana.services.data.query.filterManager.addFilters).toBeCalledWith(expected);
});
});
});
});
describe('Add to timeline', () => {
const aggregatableStringField = 'cloud.account.id';
const draggableId = 'draggable.id';
[false, true].forEach((showTopN) => {
[value, null].forEach((maybeValue) => {
[draggableId, undefined].forEach((maybeDraggableId) => {
const shouldRender = !showTopN && maybeValue != null && maybeDraggableId != null;
const assertion = shouldRender ? 'should render' : 'should NOT render';
test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
draggableId: maybeDraggableId,
field: aggregatableStringField,
showTopN,
value: maybeValue,
}}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="add-to-timeline"]').first().exists()).toBe(
shouldRender
);
});
});
});
});
test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', async () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
draggableId,
field: aggregatableStringField,
}}
/>
</TestProviders>
);
wrapper.find('[data-test-subj="add-to-timeline"]').first().simulate('click');
await waitFor(() => {
wrapper.update();
expect(mockStartDragToTimeline).toHaveBeenCalled();
});
});
});
describe('Top N', () => {
test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, () => {
const aggregatableStringField = 'cloud.account.id';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: aggregatableStringField,
}}
/>
</TestProviders>
);
wrapper.update();
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true);
});
test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
}}
/>
</TestProviders>
);
wrapper.update();
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true);
});
test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, () => {
const notKnownToBrowserFields = 'unknown.field';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: notKnownToBrowserFields,
}}
/>
</TestProviders>
);
wrapper.update();
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false);
});
test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, async () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
timelineId: undefined,
}}
/>
</TestProviders>
);
const button = wrapper.find(`[data-test-subj="show-top-field"]`).first();
button.simulate('mouseenter');
await waitFor(() => {
expect(goGetTimelineId).toHaveBeenCalledWith(true);
});
});
test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
}}
/>
</TestProviders>
);
wrapper.update();
wrapper.find('[data-test-subj="show-top-field"]').first().simulate('click');
wrapper.update();
expect(toggleTopN).toBeCalled();
});
test(`it does NOT render the Top N histogram when when showTopN is false`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
}}
/>
</TestProviders>
);
wrapper.update();
expect(wrapper.find('[data-test-subj="eventsByDatasetOverviewPanel"]').first().exists()).toBe(
false
);
});
test(`it does NOT render the 'Show top field' button when showTopN is true`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
showTopN: true,
}}
/>
</TestProviders>
);
wrapper.update();
expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false);
});
test(`it renders the Top N histogram when when showTopN is true`, () => {
const allowlistedField = 'signal.rule.name';
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
field: allowlistedField,
showTopN: true,
}}
/>
</TestProviders>
);
wrapper.update();
expect(
wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').first().exists()
).toBe(true);
});
});
describe('Copy to Clipboard', () => {
test(`it renders the 'Copy to Clipboard' button when showTopN is false`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(true);
});
test(`it does NOT render the 'Copy to Clipboard' button when showTopN is true`, () => {
const wrapper = mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{
...defaultProps,
showTopN: true,
}}
/>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false);
});
});
});

View file

@ -1,425 +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 {
EuiButtonIcon,
EuiFocusTrap,
EuiPanel,
EuiScreenReaderOnly,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { getAllFieldsByName } from '../../containers/source';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { useKibana } from '../../lib/kibana';
import { createFilter } from '../add_filter_to_global_search_bar';
import { StatefulTopN } from '../top_n';
import { allowTopN } from './helpers';
import * as i18n from './translations';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { TimelineId } from '../../../../common/types/timeline';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { stopPropagationAndPreventDefault } from '../../../../../timelines/public';
import { TooltipWithKeyboardShortcut } from '../accessibility';
export const AdditionalContent = styled.div`
padding: 2px;
`;
AdditionalContent.displayName = 'AdditionalContent';
const getAdditionalScreenReaderOnlyContext = ({
field,
value,
}: {
field: string;
value?: string[] | string | null;
}): string => {
if (value == null) {
return field;
}
return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`;
};
const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f';
const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o';
const ADD_TO_TIMELINE_KEYBOARD_SHORTCUT = 'a';
const SHOW_TOP_N_KEYBOARD_SHORTCUT = 't';
const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c';
interface Props {
additionalContent?: React.ReactNode;
closePopOver?: () => void;
draggableId?: DraggableId;
field: string;
goGetTimelineId?: (args: boolean) => void;
onFilterAdded?: () => void;
ownFocus: boolean;
showTopN: boolean;
timelineId?: string | null;
toggleTopN: () => void;
value?: string[] | string | null;
}
/** Returns a value for the `disabled` prop of `EuiFocusTrap` */
const isFocusTrapDisabled = ({
ownFocus,
showTopN,
}: {
ownFocus: boolean;
showTopN: boolean;
}): boolean => {
if (showTopN) {
return false; // we *always* want to trap focus when showing Top N
}
return !ownFocus;
};
const DraggableWrapperHoverContentComponent: React.FC<Props> = ({
additionalContent = null,
closePopOver,
draggableId,
field,
goGetTimelineId,
onFilterAdded,
ownFocus,
showTopN,
timelineId,
toggleTopN,
value,
}) => {
const kibana = useKibana();
const { timelines } = kibana.services;
const { startDragToTimeline } = timelines.getUseAddToTimeline()({
draggableId,
fieldName: field,
});
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) =>
getManageTimeline(state, timelineId ?? '')
);
const defaultFocusedButtonRef = useRef<HTMLButtonElement | null>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
const filterManager = useMemo(
() => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup),
[timelineId, activeFilterMananager, filterManagerBackup]
);
// Regarding data from useManageTimeline:
// * `indexToAdd`, which enables the alerts index to be appended to
// the `indexPattern` returned by `useWithSource`, may only be populated when
// this component is rendered in the context of the active timeline. This
// behavior enables the 'All events' view by appending the alerts index
// to the index pattern.
const activeScope: SourcererScopeName =
timelineId === TimelineId.active
? SourcererScopeName.timeline
: timelineId != null &&
[TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes(
timelineId as TimelineId
)
? SourcererScopeName.detections
: SourcererScopeName.default;
const { browserFields, indexPattern } = useSourcererScope(activeScope);
const handleStartDragToTimeline = useCallback(() => {
startDragToTimeline();
if (closePopOver != null) {
closePopOver();
}
}, [closePopOver, startDragToTimeline]);
const filterForValue = useCallback(() => {
const filter =
value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value);
const activeFilterManager = filterManager;
if (activeFilterManager != null) {
activeFilterManager.addFilters(filter);
if (closePopOver != null) {
closePopOver();
}
if (onFilterAdded != null) {
onFilterAdded();
}
}
}, [closePopOver, field, value, filterManager, onFilterAdded]);
const filterOutValue = useCallback(() => {
const filter =
value?.length === 0 ? createFilter(field, null, false) : createFilter(field, value, true);
const activeFilterManager = filterManager;
if (activeFilterManager != null) {
activeFilterManager.addFilters(filter);
if (closePopOver != null) {
closePopOver();
}
if (onFilterAdded != null) {
onFilterAdded();
}
}
}, [closePopOver, field, value, filterManager, onFilterAdded]);
const isInit = useRef(true);
useEffect(() => {
if (isInit.current && goGetTimelineId != null && timelineId == null) {
isInit.current = false;
goGetTimelineId(true);
}
}, [goGetTimelineId, timelineId]);
useEffect(() => {
if (ownFocus) {
setTimeout(() => {
defaultFocusedButtonRef.current?.focus();
}, 0);
}
}, [ownFocus]);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (!ownFocus) {
return;
}
switch (keyboardEvent.key) {
case FILTER_FOR_VALUE_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
filterForValue();
break;
case FILTER_OUT_VALUE_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
filterOutValue();
break;
case ADD_TO_TIMELINE_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
handleStartDragToTimeline();
break;
case SHOW_TOP_N_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
toggleTopN();
break;
case COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
const copyToClipboardButton = panelRef.current?.querySelector<HTMLButtonElement>(
`.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}`
);
if (copyToClipboardButton != null) {
copyToClipboardButton.click();
if (closePopOver != null) {
closePopOver();
}
}
break;
case 'Enter':
break;
case 'Escape':
stopPropagationAndPreventDefault(keyboardEvent);
if (closePopOver != null) {
closePopOver();
}
break;
default:
break;
}
},
[closePopOver, filterForValue, filterOutValue, handleStartDragToTimeline, ownFocus, toggleTopN]
);
return (
<EuiPanel onKeyDown={onKeyDown} paddingSize={showTopN ? 'none' : 's'} panelRef={panelRef}>
<EuiFocusTrap
disabled={isFocusTrapDisabled({
ownFocus,
showTopN,
})}
>
<EuiScreenReaderOnly>
<p>{i18n.YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}</p>
</EuiScreenReaderOnly>
{additionalContent != null && <AdditionalContent>{additionalContent}</AdditionalContent>}
{!showTopN && value != null && (
<EuiToolTip
content={
<TooltipWithKeyboardShortcut
additionalScreenReaderOnlyContext={getAdditionalScreenReaderOnlyContext({
field,
value,
})}
content={i18n.FILTER_FOR_VALUE}
shortcut={FILTER_FOR_VALUE_KEYBOARD_SHORTCUT}
showShortcut={ownFocus}
/>
}
>
<EuiButtonIcon
aria-label={i18n.FILTER_FOR_VALUE}
buttonRef={defaultFocusedButtonRef}
color="text"
data-test-subj="filter-for-value"
iconType="magnifyWithPlus"
onClick={filterForValue}
/>
</EuiToolTip>
)}
{!showTopN && value != null && (
<EuiToolTip
content={
<TooltipWithKeyboardShortcut
additionalScreenReaderOnlyContext={getAdditionalScreenReaderOnlyContext({
field,
value,
})}
content={i18n.FILTER_OUT_VALUE}
shortcut={FILTER_OUT_VALUE_KEYBOARD_SHORTCUT}
showShortcut={ownFocus}
/>
}
>
<EuiButtonIcon
aria-label={i18n.FILTER_OUT_VALUE}
color="text"
data-test-subj="filter-out-value"
iconType="magnifyWithMinus"
onClick={filterOutValue}
/>
</EuiToolTip>
)}
{!showTopN && value != null && draggableId != null && (
<EuiToolTip
content={
<TooltipWithKeyboardShortcut
additionalScreenReaderOnlyContext={getAdditionalScreenReaderOnlyContext({
field,
value,
})}
content={i18n.ADD_TO_TIMELINE}
shortcut={ADD_TO_TIMELINE_KEYBOARD_SHORTCUT}
showShortcut={ownFocus}
/>
}
>
<EuiButtonIcon
aria-label={i18n.ADD_TO_TIMELINE}
color="text"
data-test-subj="add-to-timeline"
iconType="timeline"
onClick={handleStartDragToTimeline}
/>
</EuiToolTip>
)}
<>
{allowTopN({
browserField: getAllFieldsByName(browserFields)[field],
fieldName: field,
}) && (
<>
{!showTopN && (
<EuiToolTip
content={
<TooltipWithKeyboardShortcut
additionalScreenReaderOnlyContext={getAdditionalScreenReaderOnlyContext({
field,
value,
})}
content={i18n.SHOW_TOP(field)}
shortcut={SHOW_TOP_N_KEYBOARD_SHORTCUT}
showShortcut={ownFocus}
/>
}
>
<EuiButtonIcon
aria-label={i18n.SHOW_TOP(field)}
color="text"
data-test-subj="show-top-field"
iconType="visBarVertical"
onClick={toggleTopN}
/>
</EuiToolTip>
)}
{showTopN && (
<StatefulTopN
browserFields={browserFields}
field={field}
indexPattern={indexPattern}
onFilterAdded={onFilterAdded}
timelineId={timelineId ?? undefined}
toggleTopN={toggleTopN}
value={value}
/>
)}
</>
)}
</>
{!showTopN && (
<WithCopyToClipboard
data-test-subj="copy-to-clipboard"
keyboardShortcut={ownFocus ? COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT : ''}
text={`${field}${value != null ? `: "${value}"` : ''}`}
titleSummary={i18n.FIELD}
/>
)}
</EuiFocusTrap>
</EuiPanel>
);
};
DraggableWrapperHoverContentComponent.displayName = 'DraggableWrapperHoverContentComponent';
export const DraggableWrapperHoverContent = React.memo(DraggableWrapperHoverContentComponent);
export const useGetTimelineId = function (
elem: React.MutableRefObject<Element | null>,
getTimelineId: boolean = false
) {
const [timelineId, setTimelineId] = useState<string | null>(null);
useEffect(() => {
let startElem: Element | (Node & ParentNode) | null = elem.current;
if (startElem != null && getTimelineId) {
for (; startElem && startElem !== document; startElem = startElem.parentNode) {
const myElem: Element = startElem as Element;
if (
myElem != null &&
myElem.classList != null &&
myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) &&
myElem.hasAttribute('data-timeline-id')
) {
setTimelineId(myElem.getAttribute('data-timeline-id'));
break;
}
}
}
}, [elem, getTimelineId]);
return timelineId;
};

View file

@ -0,0 +1,37 @@
/*
* 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, { useEffect, useState } from 'react';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
export const useGetTimelineId = function (
elem: React.MutableRefObject<Element | null>,
getTimelineId: boolean = false
) {
const [timelineId, setTimelineId] = useState<string | null>(null);
useEffect(() => {
let startElem: Element | (Node & ParentNode) | null = elem.current;
if (startElem != null && getTimelineId) {
for (; startElem && startElem !== document; startElem = startElem.parentNode) {
const myElem: Element = startElem as Element;
if (
myElem != null &&
myElem.classList != null &&
myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) &&
myElem.hasAttribute('data-timeline-id')
) {
setTimelineId(myElem.getAttribute('data-timeline-id'));
break;
}
}
}
}, [elem, getTimelineId]);
return timelineId;
};

View file

@ -36,6 +36,7 @@ exports[`draggables rendering it renders the default DefaultDraggable 1`] = `
},
}
}
isDraggable={true}
render={[Function]}
/>
`;

View file

@ -20,6 +20,7 @@ import { Provider } from '../../../timelines/components/timeline/data_providers/
export interface DefaultDraggableType {
id: string;
isDraggable?: boolean;
field: string;
value?: string | null;
name?: string | null;
@ -79,6 +80,7 @@ Content.displayName = 'Content';
* that's only displayed when the specified value is non-`null`.
*
* @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}`
* @param isDraggable - optional prop to disable drag & drop and it will defaulted to true
* @param field - the name of the field, e.g. `network.transport`
* @param value - value of the field e.g. `tcp`
* @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data
@ -88,7 +90,17 @@ Content.displayName = 'Content';
* @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data
*/
export const DefaultDraggable = React.memo<DefaultDraggableType>(
({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => {
({
id,
isDraggable = true,
field,
value,
name,
children,
timelineId,
tooltipContent,
queryValue,
}) => {
const dataProviderProp: DataProvider = useMemo(
() => ({
and: [],
@ -125,6 +137,7 @@ export const DefaultDraggable = React.memo<DefaultDraggableType>(
return (
<DraggableWrapper
dataProvider={dataProviderProp}
isDraggable={isDraggable}
render={renderCallback}
timelineId={timelineId}
/>
@ -155,6 +168,7 @@ export type BadgeDraggableType = Omit<DefaultDraggableType, 'id'> & {
* @param field - the name of the field, e.g. `network.transport`
* @param value - value of the field e.g. `tcp`
* @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge
* @param isDraggable
* @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data
* @param color - defaults to `hollow`, optionally overwrite the color of the badge icon
* @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior
@ -168,6 +182,7 @@ const DraggableBadgeComponent: React.FC<BadgeDraggableType> = ({
field,
value,
iconType,
isDraggable,
name,
color = 'hollow',
children,
@ -177,6 +192,7 @@ const DraggableBadgeComponent: React.FC<BadgeDraggableType> = ({
value != null ? (
<DefaultDraggable
id={`draggable-badge-default-draggable-${contextId}-${eventId}-${field}-${value}`}
isDraggable={isDraggable}
field={field}
name={name}
value={value}

View file

@ -63,6 +63,7 @@ const EnrichmentDescription: React.FC<ThreatSummaryItem['description']> = ({
key={key}
contextId={key}
eventId={eventId}
isDraggable={false}
fieldName={fieldName || 'unknown'}
value={value}
/>

View file

@ -245,7 +245,7 @@ describe('EventFieldsBrowser', () => {
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="draggable-content-@timestamp"]').at(0).text()).toEqual(
expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').at(0).text()).toEqual(
'Feb 28, 2019 @ 16:50:54.621'
);
});

View file

@ -6,11 +6,10 @@
*/
import React, { useCallback, useState, useRef } from 'react';
import { getDraggableId } from '@kbn/securitysolution-t-grid';
import { HoverActions } from '../../hover_actions';
import { useActionCellDataProvider } from './use_action_cell_data_provider';
import { EventFieldsData } from '../types';
import { useGetTimelineId } from '../../drag_and_drop/draggable_wrapper_hover_content';
import { useGetTimelineId } from '../../drag_and_drop/use_get_timeline_id_from_dom';
import { ColumnHeaderOptions } from '../../../../../common/types/timeline';
import { BrowserField } from '../../../containers/source';
@ -66,11 +65,10 @@ export const ActionCell: React.FC<Props> = React.memo(
});
}, []);
const draggableIds = actionCellConfig?.idList.map((id) => getDraggableId(id));
return (
<HoverActions
dataType={data.type}
draggableIds={draggableIds?.length ? draggableIds : undefined}
dataProvider={actionCellConfig?.dataProvider}
field={data.field}
goGetTimelineId={setGoGetTimelineId}
isObjectArray={data.isObjectArray}

View file

@ -55,6 +55,7 @@ export const FieldValueCell = React.memo(
fieldFormat={data.format}
fieldName={data.field}
fieldType={data.type}
isDraggable={false}
isObjectArray={data.isObjectArray}
value={value}
linkValue={(getLinkValue && getLinkValue(data.field)) ?? linkValue}

View file

@ -9,6 +9,7 @@
import { escapeDataProviderId } from '@kbn/securitysolution-t-grid';
import { isArray, isEmpty, isString } from 'lodash/fp';
import { useMemo } from 'react';
import {
AGENT_STATUS_FIELD_NAME,
EVENT_MODULE_FIELD_NAME,
@ -27,6 +28,7 @@ import { EVENT_DURATION_FIELD_NAME } from '../../../../timelines/components/dura
import { PORT_NAMES } from '../../../../network/components/port';
import { INDICATOR_REFERENCE } from '../../../../../common/cti/constants';
import { BrowserField } from '../../../containers/source';
import { DataProvider, IS_OPERATOR } from '../../../../../common/types';
export interface UseActionCellDataProvider {
contextId?: string;
@ -40,6 +42,20 @@ export interface UseActionCellDataProvider {
values: string[] | null | undefined;
}
const getDataProvider = (field: string, id: string, value: string): DataProvider => ({
and: [],
enabled: true,
id: escapeDataProviderId(id),
name: field,
excluded: false,
kqlQuery: '',
queryMatch: {
field,
value,
operator: IS_OPERATOR,
},
});
export const useActionCellDataProvider = ({
contextId,
eventId,
@ -50,72 +66,90 @@ export const useActionCellDataProvider = ({
isObjectArray,
linkValue,
values,
}: UseActionCellDataProvider): { idList: string[]; stringValues: string[] } | null => {
if (values === null || values === undefined) return null;
const stringifiedValues: string[] = [];
const arrayValues = Array.isArray(values) ? values : [values];
const idList: string[] = arrayValues.reduce((memo, value, index) => {
let id = null;
let valueAsString: string = isString(value) ? value : `${values}`;
if (fieldFromBrowserField == null) {
stringifiedValues.push(valueAsString);
return memo;
}
const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}-${eventId}-${field}-${value}`;
if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) {
stringifiedValues.push(valueAsString);
return memo;
} else if (fieldType === IP_FIELD_TYPE) {
id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`;
if (isString(value) && !isEmpty(value)) {
try {
const addresses = JSON.parse(value);
if (isArray(addresses)) {
valueAsString = addresses.join(',');
}
} catch (_) {
// Default to keeping the existing string value
}: UseActionCellDataProvider): {
stringValues: string[];
dataProvider: DataProvider[];
} | null => {
const cellData = useMemo(() => {
if (values === null || values === undefined) return null;
const arrayValues = Array.isArray(values) ? values : [values];
return arrayValues.reduce<{
stringValues: string[];
dataProvider: DataProvider[];
}>(
(memo, value, index) => {
let id: string = '';
let valueAsString: string = isString(value) ? value : `${values}`;
const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}`;
if (fieldFromBrowserField == null) {
memo.stringValues.push(valueAsString);
return memo;
}
}
} else if (PORT_NAMES.some((portName) => field === portName)) {
id = `port-default-draggable-${appendedUniqueId}`;
} else if (field === EVENT_DURATION_FIELD_NAME) {
id = `duration-default-draggable-${appendedUniqueId}`;
} else if (field === HOST_NAME_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}`;
} else if (fieldFormat === BYTES_FORMAT) {
id = `bytes-default-draggable-${appendedUniqueId}`;
} else if (field === SIGNAL_RULE_NAME_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`;
} else if (field === EVENT_MODULE_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
} else if (field === SIGNAL_STATUS_FIELD_NAME) {
id = `alert-details-value-default-draggable-${appendedUniqueId}`;
} else if (field === AGENT_STATUS_FIELD_NAME) {
const valueToUse = typeof value === 'string' ? value : '';
id = `event-details-value-default-draggable-${appendedUniqueId}`;
valueAsString = valueToUse;
} else if (
[
RULE_REFERENCE_FIELD_NAME,
REFERENCE_URL_FIELD_NAME,
EVENT_URL_FIELD_NAME,
INDICATOR_REFERENCE,
].includes(field)
) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
} else {
id = `event-details-value-default-draggable-${appendedUniqueId}`;
}
stringifiedValues.push(valueAsString);
memo.push(escapeDataProviderId(id));
return memo;
}, [] as string[]);
return {
idList,
stringValues: stringifiedValues,
};
if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) {
memo.stringValues.push(valueAsString);
return memo;
} else if (fieldType === IP_FIELD_TYPE) {
id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`;
if (isString(value) && !isEmpty(value)) {
try {
const addresses = JSON.parse(value);
if (isArray(addresses)) {
valueAsString = addresses.join(',');
addresses.forEach((ip) => memo.dataProvider.push(getDataProvider(field, id, ip)));
}
} catch (_) {
// Default to keeping the existing string value
}
memo.stringValues.push(valueAsString);
return memo;
}
} else if (PORT_NAMES.some((portName) => field === portName)) {
id = `port-default-draggable-${appendedUniqueId}`;
} else if (field === EVENT_DURATION_FIELD_NAME) {
id = `duration-default-draggable-${appendedUniqueId}`;
} else if (field === HOST_NAME_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}`;
} else if (fieldFormat === BYTES_FORMAT) {
id = `bytes-default-draggable-${appendedUniqueId}`;
} else if (field === SIGNAL_RULE_NAME_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`;
} else if (field === EVENT_MODULE_FIELD_NAME) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
} else if (field === SIGNAL_STATUS_FIELD_NAME) {
id = `alert-details-value-default-draggable-${appendedUniqueId}`;
} else if (field === AGENT_STATUS_FIELD_NAME) {
const valueToUse = typeof value === 'string' ? value : '';
id = `event-details-value-default-draggable-${appendedUniqueId}`;
valueAsString = valueToUse;
} else if (
[
RULE_REFERENCE_FIELD_NAME,
REFERENCE_URL_FIELD_NAME,
EVENT_URL_FIELD_NAME,
INDICATOR_REFERENCE,
].includes(field)
) {
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
} else {
id = `event-details-value-default-draggable-${appendedUniqueId}`;
}
memo.stringValues.push(valueAsString);
memo.dataProvider.push(getDataProvider(field, id, value));
return memo;
},
{ stringValues: [], dataProvider: [] }
);
}, [
contextId,
eventId,
field,
fieldFormat,
fieldFromBrowserField,
fieldType,
isObjectArray,
linkValue,
values,
]);
return cellData;
};

View file

@ -6,16 +6,17 @@
*/
import { EuiFocusTrap, EuiScreenReaderOnly } from '@elastic/eui';
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { getAllFieldsByName } from '../../containers/source';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
import { isEmpty } from 'lodash';
import { useKibana } from '../../lib/kibana';
import { getAllFieldsByName } from '../../containers/source';
import { allowTopN } from './utils';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { ColumnHeaderOptions, TimelineId } from '../../../../common/types/timeline';
import { ColumnHeaderOptions, DataProvider, TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { timelineSelectors } from '../../../timelines/store/timeline';
@ -38,43 +39,51 @@ export const AdditionalContent = styled.div`
AdditionalContent.displayName = 'AdditionalContent';
const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>`
const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean; $showOwnFocus: boolean }>`
padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`};
display: flex;
&:focus-within {
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
opacity: 1;
${(props) =>
props.$showOwnFocus
? `
&:focus-within {
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
opacity: 1;
}
}
}
&:hover {
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
opacity: 1;
&:hover {
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
opacity: 1;
}
}
}
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
opacity: ${(props) => (props.$showTopN ? 1 : 0)};
opacity: ${props.$showTopN ? 1 : 0};
&:focus {
opacity: 1;
&:focus {
opacity: 1;
}
}
}
`
: ''}
`;
interface Props {
additionalContent?: React.ReactNode;
closePopOver?: () => void;
dataProvider?: DataProvider | DataProvider[];
dataType?: string;
draggableIds?: DraggableId[];
draggableId?: DraggableId;
field: string;
goGetTimelineId?: (args: boolean) => void;
isObjectArray: boolean;
onFilterAdded?: () => void;
ownFocus: boolean;
showOwnFocus?: boolean;
showTopN: boolean;
timelineId?: string | null;
toggleColumn?: (column: ColumnHeaderOptions) => void;
@ -100,13 +109,15 @@ const isFocusTrapDisabled = ({
export const HoverActions: React.FC<Props> = React.memo(
({
additionalContent = null,
dataProvider,
dataType,
draggableIds,
draggableId,
field,
goGetTimelineId,
isObjectArray,
onFilterAdded,
ownFocus,
showOwnFocus = true,
showTopN,
timelineId,
toggleColumn,
@ -117,29 +128,13 @@ export const HoverActions: React.FC<Props> = React.memo(
const { timelines } = kibana.services;
// Common actions used by the alert table and alert flyout
const {
addToTimeline: {
AddToTimelineButton,
keyboardShortcut: addToTimelineKeyboardShortcut,
useGetHandleStartDragToTimeline,
},
columnToggle: {
ColumnToggleButton,
columnToggleFn,
keyboardShortcut: columnToggleKeyboardShortcut,
},
copy: { CopyButton, keyboardShortcut: copyKeyboardShortcut },
filterForValue: {
FilterForValueButton,
filterForValueFn,
keyboardShortcut: filterForValueKeyboardShortcut,
},
filterOutValue: {
FilterOutValueButton,
filterOutValueFn,
keyboardShortcut: filterOutValueKeyboardShortcut,
},
getAddToTimelineButton,
getColumnToggleButton,
getCopyButton,
getFilterForValueButton,
getFilterOutValueButton,
} = timelines.getHoverActions();
const [stKeyboardEvent, setStKeyboardEvent] = useState<React.KeyboardEvent>();
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
@ -169,30 +164,8 @@ export const HoverActions: React.FC<Props> = React.memo(
: SourcererScopeName.default;
const { browserFields } = useSourcererScope(activeScope);
const handleStartDragToTimeline = (() => {
const handleStartDragToTimelineFns = draggableIds?.map((draggableId) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useGetHandleStartDragToTimeline({ draggableId, field });
});
return () => handleStartDragToTimelineFns?.forEach((dragFn) => dragFn());
})();
const handleFilterForValue = useCallback(() => {
filterForValueFn({ field, value: values, filterManager, onFilterAdded });
}, [filterForValueFn, field, values, filterManager, onFilterAdded]);
const handleFilterOutValue = useCallback(() => {
filterOutValueFn({ field, value: values, filterManager, onFilterAdded });
}, [filterOutValueFn, field, values, filterManager, onFilterAdded]);
const handleToggleColumn = useCallback(
() => (toggleColumn ? columnToggleFn({ toggleColumn, field }) : null),
[columnToggleFn, field, toggleColumn]
);
const isInit = useRef(true);
const defaultFocusedButtonRef = useRef<HTMLButtonElement | null>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (isInit.current && goGetTimelineId != null && timelineId == null) {
@ -215,31 +188,6 @@ export const HoverActions: React.FC<Props> = React.memo(
return;
}
switch (keyboardEvent.key) {
case addToTimelineKeyboardShortcut:
stopPropagationAndPreventDefault(keyboardEvent);
handleStartDragToTimeline();
break;
case columnToggleKeyboardShortcut:
stopPropagationAndPreventDefault(keyboardEvent);
handleToggleColumn();
break;
case copyKeyboardShortcut:
stopPropagationAndPreventDefault(keyboardEvent);
const copyToClipboardButton = panelRef.current?.querySelector<HTMLButtonElement>(
`.${COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME}`
);
if (copyToClipboardButton != null) {
copyToClipboardButton.click();
}
break;
case filterForValueKeyboardShortcut:
stopPropagationAndPreventDefault(keyboardEvent);
handleFilterForValue();
break;
case filterOutValueKeyboardShortcut:
stopPropagationAndPreventDefault(keyboardEvent);
handleFilterOutValue();
break;
case SHOW_TOP_N_KEYBOARD_SHORTCUT:
stopPropagationAndPreventDefault(keyboardEvent);
toggleTopN();
@ -250,33 +198,26 @@ export const HoverActions: React.FC<Props> = React.memo(
stopPropagationAndPreventDefault(keyboardEvent);
break;
default:
setStKeyboardEvent(keyboardEvent);
break;
}
},
[
addToTimelineKeyboardShortcut,
columnToggleKeyboardShortcut,
copyKeyboardShortcut,
filterForValueKeyboardShortcut,
filterOutValueKeyboardShortcut,
handleFilterForValue,
handleFilterOutValue,
handleStartDragToTimeline,
handleToggleColumn,
ownFocus,
toggleTopN,
]
[ownFocus, toggleTopN]
);
const showFilters = values != null;
return (
<StyledHoverActionsContainer onKeyDown={onKeyDown} ref={panelRef} $showTopN={showTopN}>
<EuiFocusTrap
disabled={isFocusTrapDisabled({
ownFocus,
showTopN,
})}
<EuiFocusTrap
disabled={isFocusTrapDisabled({
ownFocus,
showTopN,
})}
>
<StyledHoverActionsContainer
onKeyDown={onKeyDown}
$showTopN={showTopN}
$showOwnFocus={showOwnFocus}
>
<EuiScreenReaderOnly>
<p>{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(field)}</p>
@ -286,46 +227,58 @@ export const HoverActions: React.FC<Props> = React.memo(
{showFilters && (
<>
<FilterForValueButton
data-test-subj="hover-actions-filter-for"
defaultFocusedButtonRef={defaultFocusedButtonRef}
field={field}
onClick={handleFilterForValue}
ownFocus={ownFocus}
showTooltip
value={values}
/>
<FilterOutValueButton
data-test-subj="hover-actions-filter-out"
field={field}
onClick={handleFilterOutValue}
ownFocus={ownFocus}
showTooltip
value={values}
/>
<div data-test-subj="hover-actions-filter-for">
{getFilterForValueButton({
defaultFocusedButtonRef,
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onFilterAdded,
ownFocus,
showTooltip: true,
value: values,
})}
</div>
<div data-test-subj="hover-actions-filter-out">
{getFilterOutValueButton({
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onFilterAdded,
ownFocus,
showTooltip: true,
value: values,
})}
</div>
</>
)}
{toggleColumn && (
<ColumnToggleButton
data-test-subj="hover-actions-toggle-column"
field={field}
isDisabled={isObjectArray && dataType !== 'geo_point'}
isObjectArray={isObjectArray}
onClick={handleToggleColumn}
ownFocus={ownFocus}
value={values}
/>
<div data-test-subj="hover-actions-toggle-column">
{getColumnToggleButton({
field,
isDisabled: isObjectArray && dataType !== 'geo_point',
isObjectArray,
keyboardEvent: stKeyboardEvent,
ownFocus,
showTooltip: true,
toggleColumn,
value: values,
})}
</div>
)}
{showFilters && draggableIds != null && (
<AddToTimelineButton
data-test-subj="hover-actions-add-timeline"
field={field}
onClick={handleStartDragToTimeline}
ownFocus={ownFocus}
showTooltip
value={values}
/>
{showFilters && (draggableId != null || !isEmpty(dataProvider)) && (
<div data-test-subj="hover-actions-add-timeline">
{getAddToTimelineButton({
dataProvider,
draggableId,
field,
keyboardEvent: stKeyboardEvent,
ownFocus,
showTooltip: true,
value: values,
})}
</div>
)}
{allowTopN({
browserField: getAllFieldsByName(browserFields)[field],
@ -342,18 +295,20 @@ export const HoverActions: React.FC<Props> = React.memo(
value={values}
/>
)}
{showFilters && (
<CopyButton
data-test-subj="hover-actions-copy-button"
field={field}
isHoverAction
ownFocus={ownFocus}
showTooltip
value={values}
/>
{field != null && (
<div data-test-subj="hover-actions-copy-button">
{getCopyButton({
field,
isHoverAction: true,
keyboardEvent: stKeyboardEvent,
ownFocus,
showTooltip: true,
value: values,
})}
</div>
)}
</EuiFocusTrap>
</StyledHoverActionsContainer>
</StyledHoverActionsContainer>
</EuiFocusTrap>
);
}
);

View file

@ -0,0 +1,178 @@
/*
* 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, useState, useRef } from 'react';
import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd';
import { HoverActions } from '.';
import { DataProvider } from '../../../../common/types';
import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper';
import { getDraggableId } from '../drag_and_drop/helpers';
import { useGetTimelineId } from '../drag_and_drop/use_get_timeline_id_from_dom';
const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => {
const links = draggableElement?.querySelectorAll('.euiLink') ?? [];
return links.length > 0;
};
type RenderFunctionProp = (
props: DataProvider,
provided: DraggableProvided | null,
state: DraggableStateSnapshot
) => React.ReactNode;
interface Props {
dataProvider: DataProvider;
disabled?: boolean;
isDraggable?: boolean;
inline?: boolean;
render: RenderFunctionProp;
timelineId?: string;
truncate?: boolean;
onFilterAdded?: () => void;
}
export const useHoverActions = ({
dataProvider,
isDraggable,
onFilterAdded,
render,
timelineId,
}: Props) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [showTopN, setShowTopN] = useState<boolean>(false);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
const [goGetTimelineId, setGoGetTimelineId] = useState(false);
const timelineIdFind = useGetTimelineId(containerRef, goGetTimelineId);
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => {
if (prevHoverActionsOwnFocus) {
setTimeout(() => {
keyboardHandlerRef.current?.focus();
}, 0);
}
return false; // always give up ownership
});
setTimeout(() => {
setHoverActionsOwnFocus(false);
}, 0); // invoked on the next tick, because we want to restore focus first
}, [keyboardHandlerRef]);
const toggleTopN = useCallback(() => {
setShowTopN((prevShowTopN) => {
const newShowTopN = !prevShowTopN;
if (newShowTopN === false) {
handleClosePopOverTrigger();
}
return newShowTopN;
});
}, [handleClosePopOverTrigger]);
const hoverContent = useMemo(() => {
// display links as additional content in the hover menu to enable keyboard
// navigation of links (when the draggable contains them):
const additionalContent =
hoverActionsOwnFocus && !showTopN && draggableContainsLinks(containerRef.current) ? (
<ProviderContentWrapper
data-test-subj={`draggable-link-content-${dataProvider.queryMatch.field}`}
>
{render(dataProvider, null, { isDragging: false, isDropAnimating: false })}
</ProviderContentWrapper>
) : null;
return (
<HoverActions
additionalContent={additionalContent}
closePopOver={handleClosePopOverTrigger}
dataProvider={dataProvider}
draggableId={isDraggable ? getDraggableId(dataProvider.id) : undefined}
field={dataProvider.queryMatch.field}
isObjectArray={false}
goGetTimelineId={setGoGetTimelineId}
onFilterAdded={onFilterAdded}
ownFocus={hoverActionsOwnFocus}
showOwnFocus={false}
showTopN={showTopN}
timelineId={timelineId ?? timelineIdFind}
toggleTopN={toggleTopN}
values={
typeof dataProvider.queryMatch.value !== 'number'
? dataProvider.queryMatch.value
: `${dataProvider.queryMatch.value}`
}
/>
);
}, [
dataProvider,
handleClosePopOverTrigger,
hoverActionsOwnFocus,
isDraggable,
onFilterAdded,
render,
showTopN,
timelineId,
timelineIdFind,
toggleTopN,
]);
const setContainerRef = useCallback((e: HTMLDivElement) => {
containerRef.current = e;
}, []);
const onFocus = useCallback(() => {
if (!hoverActionsOwnFocus) {
keyboardHandlerRef.current?.focus();
}
}, [hoverActionsOwnFocus, keyboardHandlerRef]);
const onCloseRequested = useCallback(() => {
setShowTopN(false);
if (hoverActionsOwnFocus) {
setHoverActionsOwnFocus(false);
setTimeout(() => {
onFocus(); // return focus to this draggable on the next tick, because we owned focus
}, 0);
}
}, [onFocus, hoverActionsOwnFocus]);
const openPopover = useCallback(() => {
setHoverActionsOwnFocus(true);
}, []);
return useMemo(
() => ({
closePopOverTrigger,
handleClosePopOverTrigger,
hoverActionsOwnFocus,
hoverContent,
keyboardHandlerRef,
onCloseRequested,
onFocus,
openPopover,
setContainerRef,
showTopN,
}),
[
closePopOverTrigger,
handleClosePopOverTrigger,
hoverActionsOwnFocus,
hoverContent,
onCloseRequested,
onFocus,
openPopover,
setContainerRef,
showTopN,
]
);
};

View file

@ -70,67 +70,6 @@ describe('get_anomalies_host_table_columns', () => {
expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(false);
});
test('on host page, we should escape the draggable id', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.page,
startDate,
endDate,
interval,
narrowDateRange
);
const column = columns.find((col) => col.name === i18n.SCORE) as Columns<
string,
AnomaliesByHost
>;
const anomaly: AnomaliesByHost = {
hostName: 'host.name',
anomaly: {
detectorIndex: 0,
entityName: 'entity-name-1',
entityValue: 'entity-value-1',
influencers: [],
jobId: 'job-1',
rowId: 'row-1',
severity: 100,
time: new Date('01/01/2000').valueOf(),
source: {
job_id: 'job-1',
result_type: 'result-1',
probability: 50,
multi_bucket_impact: 0,
record_score: 0,
initial_record_score: 0,
bucket_span: 0,
detector_index: 0,
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
function_description: 'description-1',
typical: [5, 3],
actual: [7, 4],
influencers: [],
},
},
};
if (column != null && column.render != null) {
const wrapper = mount(<TestProviders>{column.render('', anomaly)}</TestProviders>);
expect(
wrapper
.find(
'[draggableId="draggableId.content.anomalies-host-table-severity-host_name-entity-name-1-entity-value-1-100-job-1"]'
)
.first()
.exists()
).toBe(true);
} else {
expect(column).not.toBe(null);
}
});
test('on host page, undefined influencers should turn into an empty column string', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.page,

View file

@ -43,62 +43,6 @@ describe('get_anomalies_network_table_columns', () => {
expect(columns.some((col) => col.name === i18n.NETWORK_NAME)).toEqual(false);
});
test('on network page, we should escape the draggable id', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate);
const column = columns.find((col) => col.name === i18n.SCORE) as Columns<
string,
AnomaliesByNetwork
>;
const anomaly: AnomaliesByNetwork = {
type: 'source.ip',
ip: '127.0.0.1',
anomaly: {
detectorIndex: 0,
entityName: 'entity-name-1',
entityValue: 'entity-value-1',
influencers: [],
jobId: 'job-1',
rowId: 'row-1',
severity: 100,
time: new Date('01/01/2000').valueOf(),
source: {
job_id: 'job-1',
result_type: 'result-1',
probability: 50,
multi_bucket_impact: 0,
record_score: 0,
initial_record_score: 0,
bucket_span: 0,
detector_index: 0,
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
function_description: 'description-1',
typical: [5, 3],
actual: [7, 4],
influencers: [],
},
},
};
if (column != null && column.render != null) {
const wrapper = mount(<TestProviders>{column.render('', anomaly)}</TestProviders>);
expect(
wrapper
.find(
'[draggableId="draggableId.content.anomalies-network-table-severity-127_0_0_1-entity-name-1-entity-value-1-100-job-1"]'
)
.first()
.exists()
).toBe(true);
} else {
expect(column).not.toBe(null);
}
});
test('on network page, undefined influencers should turn into an empty column string', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate);
const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns<

View file

@ -55,7 +55,7 @@ describe('Table Helpers', () => {
displayCount: 0,
});
const wrapper = mount(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe(
expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe(
'(Empty String)'
);
});
@ -81,7 +81,7 @@ describe('Table Helpers', () => {
render: renderer,
});
const wrapper = mount(<TestProviders>{rowItem}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe(
expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe(
'Hi item1 renderer'
);
});
@ -116,7 +116,7 @@ describe('Table Helpers', () => {
idPrefix: 'idPrefix',
});
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe(
expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe(
'(Empty String)'
);
});
@ -163,7 +163,7 @@ describe('Table Helpers', () => {
displayCount: 2,
});
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggableWrapperDiv"]').hostNodes().length).toBe(2);
expect(wrapper.find('[data-test-subj="withHoverActionsButton"]').hostNodes().length).toBe(2);
});
test('it uses custom renderer', () => {
@ -175,7 +175,7 @@ describe('Table Helpers', () => {
render: renderer,
});
const wrapper = mount(<TestProviders>{rowItems}</TestProviders>);
expect(wrapper.find('[data-test-subj="draggable-content-attrName"]').first().text()).toBe(
expect(wrapper.find('[data-test-subj="render-content-attrName"]').first().text()).toBe(
'Hi item1 renderer'
);
});

View file

@ -60,13 +60,15 @@ export const DirectionBadge = React.memo<{
contextId: string;
direction?: string | null;
eventId: string;
}>(({ contextId, eventId, direction }) => (
isDraggable?: boolean;
}>(({ contextId, eventId, direction, isDraggable }) => (
<DraggableBadge
contextId={contextId}
data-test-subj="network-direction"
eventId={eventId}
field={NETWORK_DIRECTION_FIELD_NAME}
iconType={getDirectionIcon(direction)}
isDraggable={isDraggable}
value={direction}
/>
));

View file

@ -22,13 +22,15 @@ export const Ip = React.memo<{
contextId: string;
eventId: string;
fieldName: string;
isDraggable?: boolean;
value?: string | null;
}>(({ contextId, eventId, fieldName, value }) => (
}>(({ contextId, eventId, fieldName, isDraggable, value }) => (
<FormattedFieldValue
contextId={contextId}
data-test-subj="formatted-ip"
eventId={eventId}
fieldName={fieldName}
isDraggable={isDraggable}
fieldType={IP_FIELD_TYPE}
value={value}
truncate

View file

@ -5,6 +5,7 @@ exports[`Port renders correctly against snapshot 1`] = `
data-test-subj="port"
field="destination.port"
id="port-default-draggable-test-abcd-destination.port-443"
isDraggable={true}
tooltipContent="destination.port"
value="443"
>

View file

@ -29,7 +29,7 @@ export const Port = React.memo<{
contextId: string;
eventId: string;
fieldName: string;
isDraggable: boolean;
isDraggable?: boolean;
value: string | undefined | null;
}>(({ contextId, eventId, fieldName, isDraggable, value }) =>
isDraggable ? (
@ -37,6 +37,7 @@ export const Port = React.memo<{
data-test-subj="port"
field={fieldName}
id={`port-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={fieldName}
value={value}
>

View file

@ -73,8 +73,9 @@ const GeoFieldValues = React.memo<{
contextId: string;
eventId: string;
fieldName: string;
isDraggable?: boolean;
values?: string[] | null;
}>(({ contextId, eventId, fieldName, values }) =>
}>(({ contextId, eventId, fieldName, isDraggable, values }) =>
values != null ? (
<>
{uniq(values).map((value) => (
@ -92,6 +93,7 @@ const GeoFieldValues = React.memo<{
data-test-subj={fieldName}
field={fieldName}
id={`geo-field-values-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={fieldName}
value={value}
/>
@ -114,7 +116,7 @@ GeoFieldValues.displayName = 'GeoFieldValues';
* - `source|destination.geo.city_name`
*/
export const GeoFields = React.memo<GeoFieldsProps>((props) => {
const { contextId, eventId, type } = props;
const { contextId, eventId, isDraggable, type } = props;
const propNameToFieldName = getGeoFieldPropNameToFieldNameMap(type);
return (
@ -124,6 +126,7 @@ export const GeoFields = React.memo<GeoFieldsProps>((props) => {
contextId={contextId}
eventId={eventId}
fieldName={geo.fieldName}
isDraggable={isDraggable}
key={geo.fieldName}
values={get(geo.prop, props)}
/>

View file

@ -36,6 +36,7 @@ export const SourceDestination = React.memo<SourceDestinationProps>(
destinationPackets,
destinationPort,
eventId,
isDraggable,
networkBytes,
networkCommunityId,
networkDirection,
@ -59,8 +60,9 @@ export const SourceDestination = React.memo<SourceDestinationProps>(
packets={networkPackets}
communityId={networkCommunityId}
contextId={contextId}
eventId={eventId}
direction={networkDirection}
eventId={eventId}
isDraggable={isDraggable}
protocol={networkProtocol}
transport={transport}
/>
@ -79,6 +81,7 @@ export const SourceDestination = React.memo<SourceDestinationProps>(
destinationPackets={destinationPackets}
destinationPort={destinationPort}
eventId={eventId}
isDraggable={isDraggable}
sourceBytes={sourceBytes}
sourceGeoContinentName={sourceGeoContinentName}
sourceGeoCountryName={sourceGeoCountryName}

View file

@ -25,9 +25,10 @@ IpPortSeparator.displayName = 'IpPortSeparator';
const PortWithSeparator = React.memo<{
contextId: string;
eventId: string;
isDraggable?: boolean;
port?: string | null;
portFieldName: string;
}>(({ contextId, eventId, port, portFieldName }) => {
}>(({ contextId, eventId, isDraggable, port, portFieldName }) => {
return port != null ? (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>
@ -39,7 +40,7 @@ const PortWithSeparator = React.memo<{
data-test-subj="port"
eventId={eventId}
fieldName={portFieldName}
isDraggable={true}
isDraggable={isDraggable}
value={port}
/>
</EuiFlexItem>
@ -58,9 +59,10 @@ export const IpWithPort = React.memo<{
eventId: string;
ip?: string | null;
ipFieldName: string;
isDraggable?: boolean;
port?: string | null;
portFieldName: string;
}>(({ contextId, eventId, ip, ipFieldName, port, portFieldName }) => (
}>(({ contextId, eventId, ip, ipFieldName, isDraggable, port, portFieldName }) => (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={false}>
<Ip
@ -68,6 +70,7 @@ export const IpWithPort = React.memo<{
data-test-subj="ip"
eventId={eventId}
fieldName={ipFieldName}
isDraggable={isDraggable}
value={ip}
/>
</EuiFlexItem>
@ -75,6 +78,7 @@ export const IpWithPort = React.memo<{
<PortWithSeparator
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
port={port}
portFieldName={portFieldName}
/>

View file

@ -45,97 +45,120 @@ export const Network = React.memo<{
contextId: string;
direction?: string[] | null;
eventId: string;
isDraggable?: boolean;
packets?: string[] | null;
protocol?: string[] | null;
transport?: string[] | null;
}>(({ bytes, communityId, contextId, direction, eventId, packets, protocol, transport }) => (
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="none">
{direction != null
? uniq(direction).map((dir) => (
<EuiFlexItemMarginRight grow={false} key={dir}>
<DirectionBadge contextId={contextId} eventId={eventId} direction={dir} />
</EuiFlexItemMarginRight>
))
: null}
}>(
({
bytes,
communityId,
contextId,
direction,
eventId,
isDraggable,
packets,
protocol,
transport,
}) => (
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="none">
{direction != null
? uniq(direction).map((dir) => (
<EuiFlexItemMarginRight grow={false} key={dir}>
<DirectionBadge
contextId={contextId}
direction={dir}
eventId={eventId}
isDraggable={isDraggable}
/>
</EuiFlexItemMarginRight>
))
: null}
{protocol != null
? uniq(protocol).map((proto) => (
<EuiFlexItemMarginRight grow={false} key={proto}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-protocol"
eventId={eventId}
field={NETWORK_PROTOCOL_FIELD_NAME}
value={proto}
/>
</EuiFlexItemMarginRight>
))
: null}
{protocol != null
? uniq(protocol).map((proto) => (
<EuiFlexItemMarginRight grow={false} key={proto}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-protocol"
eventId={eventId}
field={NETWORK_PROTOCOL_FIELD_NAME}
isDraggable={isDraggable}
value={proto}
/>
</EuiFlexItemMarginRight>
))
: null}
{bytes != null
? uniq(bytes).map((b) =>
!isNaN(Number(b)) ? (
<EuiFlexItemMarginRight grow={false} key={b}>
{bytes != null
? uniq(bytes).map((b) =>
!isNaN(Number(b)) ? (
<EuiFlexItemMarginRight grow={false} key={b}>
<DefaultDraggable
field={NETWORK_BYTES_FIELD_NAME}
id={`network-default-draggable-${contextId}-${eventId}-${NETWORK_BYTES_FIELD_NAME}-${b}`}
isDraggable={isDraggable}
value={b}
>
<Stats size="xs">
<span data-test-subj="network-bytes">
<PreferenceFormattedBytes value={b} />
</span>
</Stats>
</DefaultDraggable>
</EuiFlexItemMarginRight>
) : null
)
: null}
{packets != null
? uniq(packets).map((p) => (
<EuiFlexItemMarginRight grow={false} key={p}>
<DefaultDraggable
field={NETWORK_BYTES_FIELD_NAME}
id={`network-default-draggable-${contextId}-${eventId}-${NETWORK_BYTES_FIELD_NAME}-${b}`}
value={b}
field={NETWORK_PACKETS_FIELD_NAME}
id={`network-default-draggable-${contextId}-${eventId}-${NETWORK_PACKETS_FIELD_NAME}-${p}`}
isDraggable={isDraggable}
value={p}
>
<Stats size="xs">
<span data-test-subj="network-bytes">
<PreferenceFormattedBytes value={b} />
</span>
<span data-test-subj="network-packets">{`${p} ${i18n.PACKETS}`}</span>
</Stats>
</DefaultDraggable>
</EuiFlexItemMarginRight>
) : null
)
: null}
))
: null}
{packets != null
? uniq(packets).map((p) => (
<EuiFlexItemMarginRight grow={false} key={p}>
<DefaultDraggable
field={NETWORK_PACKETS_FIELD_NAME}
id={`network-default-draggable-${contextId}-${eventId}-${NETWORK_PACKETS_FIELD_NAME}-${p}`}
value={p}
>
<Stats size="xs">
<span data-test-subj="network-packets">{`${p} ${i18n.PACKETS}`}</span>
</Stats>
</DefaultDraggable>
</EuiFlexItemMarginRight>
))
: null}
{transport != null
? uniq(transport).map((trans) => (
<EuiFlexItemMarginRight grow={false} key={trans}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-transport"
eventId={eventId}
field={NETWORK_TRANSPORT_FIELD_NAME}
isDraggable={isDraggable}
value={trans}
/>
</EuiFlexItemMarginRight>
))
: null}
{transport != null
? uniq(transport).map((trans) => (
<EuiFlexItemMarginRight grow={false} key={trans}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-transport"
eventId={eventId}
field={NETWORK_TRANSPORT_FIELD_NAME}
value={trans}
/>
</EuiFlexItemMarginRight>
))
: null}
{communityId != null
? uniq(communityId).map((trans) => (
<EuiFlexItem grow={false} key={trans}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-community-id"
eventId={eventId}
field={NETWORK_COMMUNITY_ID_FIELD_NAME}
value={trans}
/>
</EuiFlexItem>
))
: null}
</EuiFlexGroup>
));
{communityId != null
? uniq(communityId).map((trans) => (
<EuiFlexItem grow={false} key={trans}>
<DraggableBadge
contextId={contextId}
data-test-subj="network-community-id"
eventId={eventId}
field={NETWORK_COMMUNITY_ID_FIELD_NAME}
isDraggable={isDraggable}
value={trans}
/>
</EuiFlexItem>
))
: null}
</EuiFlexGroup>
)
);
Network.displayName = 'Network';

View file

@ -56,10 +56,11 @@ Data.displayName = 'Data';
const SourceArrow = React.memo<{
contextId: string;
eventId: string;
isDraggable?: boolean;
sourceBytes: string | undefined;
sourceBytesPercent: number | undefined;
sourcePackets: string | undefined;
}>(({ contextId, eventId, sourceBytes, sourceBytesPercent, sourcePackets }) => {
}>(({ contextId, eventId, isDraggable, sourceBytes, sourceBytesPercent, sourcePackets }) => {
const sourceArrowHeight =
sourceBytesPercent != null
? getArrowHeightFromPercent(sourceBytesPercent)
@ -76,6 +77,7 @@ const SourceArrow = React.memo<{
<DefaultDraggable
field={SOURCE_BYTES_FIELD_NAME}
id={`source-arrow-default-draggable-${contextId}-${eventId}-${SOURCE_BYTES_FIELD_NAME}-${sourceBytes}`}
isDraggable={isDraggable}
value={sourceBytes}
>
<Data size="xs">
@ -101,6 +103,7 @@ const SourceArrow = React.memo<{
<DefaultDraggable
field={SOURCE_PACKETS_FIELD_NAME}
id={`source-arrow-default-draggable-${contextId}-${eventId}-${SOURCE_PACKETS_FIELD_NAME}-${sourcePackets}`}
isDraggable={isDraggable}
value={sourcePackets}
>
<Data size="xs">
@ -129,73 +132,85 @@ SourceArrow.displayName = 'SourceArrow';
*/
const DestinationArrow = React.memo<{
contextId: string;
eventId: string;
destinationBytes: string | undefined;
destinationBytesPercent: number | undefined;
destinationPackets: string | undefined;
}>(({ contextId, eventId, destinationBytes, destinationBytesPercent, destinationPackets }) => {
const destinationArrowHeight =
destinationBytesPercent != null
? getArrowHeightFromPercent(destinationBytesPercent)
: DEFAULT_ARROW_HEIGHT;
eventId: string;
isDraggable?: boolean;
}>(
({
contextId,
destinationBytes,
destinationBytesPercent,
destinationPackets,
eventId,
isDraggable,
}) => {
const destinationArrowHeight =
destinationBytesPercent != null
? getArrowHeightFromPercent(destinationBytesPercent)
: DEFAULT_ARROW_HEIGHT;
return (
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center">
<EuiFlexItem grow={false}>
<ArrowHead direction="arrowLeft" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
{destinationBytes != null && !isNaN(Number(destinationBytes)) ? (
return (
<EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center">
<EuiFlexItem grow={false}>
<DefaultDraggable
field={DESTINATION_BYTES_FIELD_NAME}
id={`destination-arrow-default-draggable-${contextId}-${eventId}-${DESTINATION_BYTES_FIELD_NAME}-${destinationBytes}`}
value={destinationBytes}
>
<Data size="xs">
{destinationBytesPercent != null ? (
<Percent data-test-subj="destination-bytes-percent">
{`(${numeral(destinationBytesPercent).format('0.00')}%)`}
</Percent>
) : null}
<span data-test-subj="destination-bytes">
<PreferenceFormattedBytes value={destinationBytes} />
</span>
</Data>
</DefaultDraggable>
<ArrowHead direction="arrowLeft" />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
{destinationPackets != null && !isNaN(Number(destinationPackets)) ? (
<EuiFlexItem grow={false}>
<DefaultDraggable
field={DESTINATION_PACKETS_FIELD_NAME}
id={`destination-arrow-default-draggable-${contextId}-${eventId}-${DESTINATION_PACKETS_FIELD_NAME}-${destinationPackets}`}
value={destinationPackets}
>
<Data size="xs">
<span data-test-subj="destination-packets">{`${numeral(destinationPackets).format(
'0,0'
)} ${i18n.PACKETS}`}</span>
</Data>
</DefaultDraggable>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
</EuiFlexGroup>
);
});
{destinationBytes != null && !isNaN(Number(destinationBytes)) ? (
<EuiFlexItem grow={false}>
<DefaultDraggable
field={DESTINATION_BYTES_FIELD_NAME}
id={`destination-arrow-default-draggable-${contextId}-${eventId}-${DESTINATION_BYTES_FIELD_NAME}-${destinationBytes}`}
isDraggable={isDraggable}
value={destinationBytes}
>
<Data size="xs">
{destinationBytesPercent != null ? (
<Percent data-test-subj="destination-bytes-percent">
{`(${numeral(destinationBytesPercent).format('0.00')}%)`}
</Percent>
) : null}
<span data-test-subj="destination-bytes">
<PreferenceFormattedBytes value={destinationBytes} />
</span>
</Data>
</DefaultDraggable>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
{destinationPackets != null && !isNaN(Number(destinationPackets)) ? (
<EuiFlexItem grow={false}>
<DefaultDraggable
field={DESTINATION_PACKETS_FIELD_NAME}
id={`destination-arrow-default-draggable-${contextId}-${eventId}-${DESTINATION_PACKETS_FIELD_NAME}-${destinationPackets}`}
isDraggable={isDraggable}
value={destinationPackets}
>
<Data size="xs">
<span data-test-subj="destination-packets">{`${numeral(destinationPackets).format(
'0,0'
)} ${i18n.PACKETS}`}</span>
</Data>
</DefaultDraggable>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<ArrowBody height={destinationArrowHeight} />
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
DestinationArrow.displayName = 'DestinationArrow';
@ -208,67 +223,79 @@ export const SourceDestinationArrows = React.memo<{
destinationBytes?: string[] | null;
destinationPackets?: string[] | null;
eventId: string;
isDraggable?: boolean;
sourceBytes?: string[] | null;
sourcePackets?: string[] | null;
}>(({ contextId, destinationBytes, destinationPackets, eventId, sourceBytes, sourcePackets }) => {
const maybeSourceBytes =
sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined;
}>(
({
contextId,
destinationBytes,
destinationPackets,
eventId,
isDraggable,
sourceBytes,
sourcePackets,
}) => {
const maybeSourceBytes =
sourceBytes != null && hasOneValue(sourceBytes) ? sourceBytes[0] : undefined;
const maybeSourcePackets =
sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined;
const maybeSourcePackets =
sourcePackets != null && hasOneValue(sourcePackets) ? sourcePackets[0] : undefined;
const maybeDestinationBytes =
destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined;
const maybeDestinationBytes =
destinationBytes != null && hasOneValue(destinationBytes) ? destinationBytes[0] : undefined;
const maybeDestinationPackets =
destinationPackets != null && hasOneValue(destinationPackets)
? destinationPackets[0]
: undefined;
const maybeDestinationPackets =
destinationPackets != null && hasOneValue(destinationPackets)
? destinationPackets[0]
: undefined;
const maybeSourceBytesPercent =
maybeSourceBytes != null && maybeDestinationBytes != null
? getPercent({
numerator: Number(maybeSourceBytes),
denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes),
})
: undefined;
const maybeSourceBytesPercent =
maybeSourceBytes != null && maybeDestinationBytes != null
? getPercent({
numerator: Number(maybeSourceBytes),
denominator: Number(maybeSourceBytes) + Number(maybeDestinationBytes),
})
: undefined;
const maybeDestinationBytesPercent =
maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined;
const maybeDestinationBytesPercent =
maybeSourceBytesPercent != null ? 100 - maybeSourceBytesPercent : undefined;
return (
<SourceDestinationArrowsContainer
alignItems="center"
data-test-subj="source-destination-arrows-container"
justifyContent="center"
direction="column"
gutterSize="none"
>
{maybeSourceBytes != null ? (
<EuiFlexItem grow={false}>
<SourceArrow
contextId={contextId}
sourceBytes={maybeSourceBytes}
sourcePackets={maybeSourcePackets}
sourceBytesPercent={maybeSourceBytesPercent}
eventId={eventId}
/>
</EuiFlexItem>
) : null}
{maybeDestinationBytes != null ? (
<EuiFlexItem grow={false}>
<DestinationArrow
contextId={contextId}
destinationBytes={maybeDestinationBytes}
destinationPackets={maybeDestinationPackets}
destinationBytesPercent={maybeDestinationBytesPercent}
eventId={eventId}
/>
</EuiFlexItem>
) : null}
</SourceDestinationArrowsContainer>
);
});
return (
<SourceDestinationArrowsContainer
alignItems="center"
data-test-subj="source-destination-arrows-container"
justifyContent="center"
direction="column"
gutterSize="none"
>
{maybeSourceBytes != null ? (
<EuiFlexItem grow={false}>
<SourceArrow
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
sourceBytes={maybeSourceBytes}
sourcePackets={maybeSourcePackets}
sourceBytesPercent={maybeSourceBytesPercent}
/>
</EuiFlexItem>
) : null}
{maybeDestinationBytes != null ? (
<EuiFlexItem grow={false}>
<DestinationArrow
contextId={contextId}
destinationBytes={maybeDestinationBytes}
destinationPackets={maybeDestinationPackets}
destinationBytesPercent={maybeDestinationBytesPercent}
eventId={eventId}
isDraggable={isDraggable}
/>
</EuiFlexItem>
) : null}
</SourceDestinationArrowsContainer>
);
}
);
SourceDestinationArrows.displayName = 'SourceDestinationArrows';

View file

@ -958,6 +958,7 @@ describe('SourceDestinationIp', () => {
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))}
eventId={get(ID_FIELD_NAME, getMockNetflowData())}
isDraggable={true}
sourceGeoContinentName={asArrayIfExists(
get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData())
)}
@ -979,7 +980,6 @@ describe('SourceDestinationIp', () => {
/>
</TestProviders>
);
expect(
removeExternalLinkText(
wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text()
@ -1011,6 +1011,7 @@ describe('SourceDestinationIp', () => {
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))}
eventId={get(ID_FIELD_NAME, getMockNetflowData())}
isDraggable={true}
sourceGeoContinentName={asArrayIfExists(
get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData())
)}
@ -1064,6 +1065,7 @@ describe('SourceDestinationIp', () => {
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))}
eventId={get(ID_FIELD_NAME, getMockNetflowData())}
isDraggable={true}
sourceGeoContinentName={asArrayIfExists(
get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData())
)}
@ -1118,6 +1120,7 @@ describe('SourceDestinationIp', () => {
destinationIp={undefined}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))}
eventId={get(ID_FIELD_NAME, getMockNetflowData())}
isDraggable={true}
sourceGeoContinentName={asArrayIfExists(
get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData())
)}
@ -1271,6 +1274,7 @@ describe('SourceDestinationIp', () => {
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, getMockNetflowData()))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, getMockNetflowData()))}
eventId={get(ID_FIELD_NAME, getMockNetflowData())}
isDraggable={true}
sourceGeoContinentName={asArrayIfExists(
get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, getMockNetflowData())
)}

View file

@ -88,54 +88,67 @@ const IpAdressesWithPorts = React.memo<{
destinationIp?: string[] | null;
destinationPort?: Array<number | string | null> | null;
eventId: string;
isDraggable?: boolean;
sourceIp?: string[] | null;
sourcePort?: Array<number | string | null> | null;
type: SourceDestinationType;
}>(({ contextId, destinationIp, destinationPort, eventId, sourceIp, sourcePort, type }) => {
const ip = type === 'source' ? sourceIp : destinationIp;
const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME;
const port = type === 'source' ? sourcePort : destinationPort;
const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME;
}>(
({
contextId,
destinationIp,
destinationPort,
eventId,
isDraggable,
sourceIp,
sourcePort,
type,
}) => {
const ip = type === 'source' ? sourceIp : destinationIp;
const ipFieldName = type === 'source' ? SOURCE_IP_FIELD_NAME : DESTINATION_IP_FIELD_NAME;
const port = type === 'source' ? sourcePort : destinationPort;
const portFieldName = type === 'source' ? SOURCE_PORT_FIELD_NAME : DESTINATION_PORT_FIELD_NAME;
if (ip == null) {
return null; // if ip is not populated as an array, ports will be ignored
if (ip == null) {
return null; // if ip is not populated as an array, ports will be ignored
}
// IMPORTANT: The ip and port arrays are parallel arrays; the port at
// index `i` corresponds with the ip address at index `i`. We must
// preserve the relationships between the parallel arrays:
const ipPortPairs: IpPortPair[] =
port != null && ip.length === port.length
? ip.map((address, i) => ({
ip: address,
port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array
}))
: ip.map((address) => ({
ip: address,
port: null, // drop the port, because the length of the parallel ip and port arrays is different
}));
return (
<EuiFlexGroup gutterSize="none">
{uniqWith(deepEqual, ipPortPairs).map(
(ipPortPair) =>
ipPortPair.ip != null && (
<EuiFlexItem grow={false} key={ipPortPair.ip}>
<IpWithPort
contextId={contextId}
data-test-subj={`${type}-ip-and-port`}
eventId={eventId}
ip={ipPortPair.ip}
ipFieldName={ipFieldName}
isDraggable={isDraggable}
port={ipPortPair.port}
portFieldName={portFieldName}
/>
</EuiFlexItem>
)
)}
</EuiFlexGroup>
);
}
// IMPORTANT: The ip and port arrays are parallel arrays; the port at
// index `i` corresponds with the ip address at index `i`. We must
// preserve the relationships between the parallel arrays:
const ipPortPairs: IpPortPair[] =
port != null && ip.length === port.length
? ip.map((address, i) => ({
ip: address,
port: port[i] != null ? `${port[i]}` : null, // use the corresponding port in the parallel array
}))
: ip.map((address) => ({
ip: address,
port: null, // drop the port, because the length of the parallel ip and port arrays is different
}));
return (
<EuiFlexGroup gutterSize="none">
{uniqWith(deepEqual, ipPortPairs).map(
(ipPortPair) =>
ipPortPair.ip != null && (
<EuiFlexItem grow={false} key={ipPortPair.ip}>
<IpWithPort
contextId={contextId}
data-test-subj={`${type}-ip-and-port`}
eventId={eventId}
ip={ipPortPair.ip}
ipFieldName={ipFieldName}
port={ipPortPair.port}
portFieldName={portFieldName}
/>
</EuiFlexItem>
)
)}
</EuiFlexGroup>
);
});
);
IpAdressesWithPorts.displayName = 'IpAdressesWithPorts';
@ -159,6 +172,7 @@ export const SourceDestinationIp = React.memo<SourceDestinationIpProps>(
destinationIp,
destinationPort,
eventId,
isDraggable,
sourceGeoContinentName,
sourceGeoCountryName,
sourceGeoCountryIsoCode,
@ -189,6 +203,7 @@ export const SourceDestinationIp = React.memo<SourceDestinationIpProps>(
destinationIp={destinationIp}
destinationPort={destinationPort}
eventId={eventId}
isDraggable={isDraggable}
sourceIp={sourceIp}
sourcePort={sourcePort}
type={type}
@ -202,7 +217,7 @@ export const SourceDestinationIp = React.memo<SourceDestinationIpProps>(
data-test-subj="port"
eventId={eventId}
fieldName={`${type}.port`}
isDraggable={true}
isDraggable={isDraggable}
value={port}
/>
</EuiFlexItem>
@ -219,6 +234,7 @@ export const SourceDestinationIp = React.memo<SourceDestinationIpProps>(
destinationGeoRegionName={destinationGeoRegionName}
destinationGeoCityName={destinationGeoCityName}
eventId={eventId}
isDraggable={isDraggable}
sourceGeoContinentName={sourceGeoContinentName}
sourceGeoCountryName={sourceGeoCountryName}
sourceGeoCountryIsoCode={sourceGeoCountryIsoCode}

View file

@ -32,6 +32,7 @@ export const SourceDestinationWithArrows = React.memo<SourceDestinationWithArrow
destinationPackets,
destinationPort,
eventId,
isDraggable,
sourceBytes,
sourceGeoContinentName,
sourceGeoCountryName,
@ -54,6 +55,7 @@ export const SourceDestinationWithArrows = React.memo<SourceDestinationWithArrow
destinationIp={destinationIp}
destinationPort={destinationPort}
eventId={eventId}
isDraggable={isDraggable}
sourceGeoContinentName={sourceGeoContinentName}
sourceGeoCountryName={sourceGeoCountryName}
sourceGeoCountryIsoCode={sourceGeoCountryIsoCode}
@ -70,6 +72,7 @@ export const SourceDestinationWithArrows = React.memo<SourceDestinationWithArrow
destinationBytes={destinationBytes}
destinationPackets={destinationPackets}
eventId={eventId}
isDraggable={isDraggable}
sourceBytes={sourceBytes}
sourcePackets={sourcePackets}
/>
@ -85,6 +88,7 @@ export const SourceDestinationWithArrows = React.memo<SourceDestinationWithArrow
destinationIp={destinationIp}
destinationPort={destinationPort}
eventId={eventId}
isDraggable={isDraggable}
sourceGeoContinentName={sourceGeoContinentName}
sourceGeoCountryName={sourceGeoCountryName}
sourceGeoCountryIsoCode={sourceGeoCountryIsoCode}

View file

@ -15,6 +15,7 @@ export interface GeoFieldsProps {
destinationGeoRegionName?: string[] | null;
destinationGeoCityName?: string[] | null;
eventId: string;
isDraggable?: boolean;
sourceGeoContinentName?: string[] | null;
sourceGeoCountryName?: string[] | null;
sourceGeoCountryIsoCode?: string[] | null;
@ -37,6 +38,7 @@ export interface SourceDestinationProps {
destinationPort?: string[] | null;
direction?: string[] | null;
eventId: string;
isDraggable?: boolean;
networkBytes?: string[] | null;
networkCommunityId?: string[] | null;
networkDirection?: string[] | null;
@ -63,6 +65,7 @@ export interface SourceDestinationIpProps {
destinationIp?: string[] | null;
destinationPort?: Array<number | string | null> | null;
eventId: string;
isDraggable?: boolean;
sourceGeoContinentName?: string[] | null;
sourceGeoCountryName?: string[] | null;
sourceGeoCountryIsoCode?: string[] | null;
@ -85,6 +88,7 @@ export interface SourceDestinationWithArrowsProps {
destinationPackets?: string[] | null;
destinationPort?: string[] | null;
eventId: string;
isDraggable?: boolean;
sourceBytes?: string[] | null;
sourceGeoContinentName?: string[] | null;
sourceGeoCountryName?: string[] | null;

View file

@ -40,8 +40,9 @@ export const CertificateFingerprint = React.memo<{
certificateType: CertificateType;
contextId: string;
fieldName: string;
isDraggable?: boolean;
value?: string | null;
}>(({ eventId, certificateType, contextId, fieldName, value }) => {
}>(({ eventId, certificateType, contextId, fieldName, isDraggable, value }) => {
return (
<DraggableBadge
contextId={contextId}
@ -49,6 +50,7 @@ export const CertificateFingerprint = React.memo<{
eventId={eventId}
field={fieldName}
iconType="snowflake"
isDraggable={isDraggable}
tooltipContent={
<EuiText size="xs">
<span>{fieldName}</span>

View file

@ -26,6 +26,7 @@ export const Duration = React.memo<{
isDraggable ? (
<DefaultDraggable
id={`duration-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
// @ts-expect-error
name={name}
field={fieldName}

View file

@ -10,6 +10,7 @@ exports[`Field Renderers #autonomousSystemRenderer it renders correctly against
<DefaultDraggable
field="source.as.organization.name"
id="autonomous-system-renderer-default-draggable-ip-overview-source.as.organization.name"
isDraggable={false}
value="Test Org"
/>
</EuiFlexItem>
@ -24,6 +25,7 @@ exports[`Field Renderers #autonomousSystemRenderer it renders correctly against
<DefaultDraggable
field="source.as.number"
id="autonomous-system-renderer-default-draggable-ip-overview-source.as.number"
isDraggable={false}
value="12345"
/>
</EuiFlexItem>
@ -58,6 +60,7 @@ exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1
},
}
}
isDraggable={false}
render={[Function]}
/>
`;
@ -79,6 +82,7 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot
},
}
}
isDraggable={false}
render={[Function]}
/>
`;
@ -94,6 +98,7 @@ exports[`Field Renderers #locationRenderer it renders correctly against snapshot
<DefaultDraggable
field="source.geo.city_name"
id="location-renderer-default-draggable-ip-overview-source.geo.city_name"
isDraggable={false}
value={
Array [
"New York",
@ -108,6 +113,7 @@ exports[`Field Renderers #locationRenderer it renders correctly against snapshot
<DefaultDraggable
field="source.geo.region_name"
id="location-renderer-default-draggable-ip-overview-source.geo.region_name"
isDraggable={false}
value={
Array [
"New York",

View file

@ -56,6 +56,7 @@ export const locationRenderer = (
id={`location-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${fieldName}`}
isDraggable={false}
field={fieldName}
value={locationValue}
/>
@ -84,6 +85,7 @@ export const autonomousSystemRenderer = (
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${flowTarget}.as.organization.name`}
isDraggable={false}
field={`${flowTarget}.as.organization.name`}
value={as.organization.name}
/>
@ -94,6 +96,7 @@ export const autonomousSystemRenderer = (
id={`autonomous-system-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}${flowTarget}.as.number`}
isDraggable={false}
field={`${flowTarget}.as.number`}
value={`${as.number}`}
/>
@ -123,6 +126,7 @@ export const hostIdRenderer = ({
id={`host-id-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}host-id`}
isDraggable={false}
field="host.id"
value={host.id[0]}
>
@ -154,6 +158,7 @@ export const hostNameRenderer = (
id={`host-name-renderer-default-draggable-${IpOverviewId}-${
contextID ? `${contextID}-` : ''
}host-name`}
isDraggable={false}
field={'host.name'}
value={host.name[0]}
>
@ -204,7 +209,7 @@ export const DefaultFieldRendererComponent: React.FC<DefaultFieldRendererProps>
</>
)}
{typeof rowItem === 'string' && (
<DefaultDraggable id={id} field={attrName} value={rowItem}>
<DefaultDraggable id={id} isDraggable={false} field={attrName} value={rowItem}>
{render ? render(rowItem) : rowItem}
</DefaultDraggable>
)}

View file

@ -59,11 +59,11 @@ describe('FieldName', () => {
</TestProviders>
);
await waitFor(() => {
wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter');
wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter');
wrapper.update();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true);
});
});

View file

@ -11,11 +11,9 @@ import styled from 'styled-components';
import { OnUpdateColumns } from '../timeline/events';
import { WithHoverActions } from '../../../common/components/with_hover_actions';
import {
DraggableWrapperHoverContent,
useGetTimelineId,
} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content';
import { useGetTimelineId } from '../../../common/components/drag_and_drop/use_get_timeline_id_from_dom';
import { ColumnHeaderOptions } from '../../../../common';
import { HoverActions } from '../../../common/components/hover_actions';
/**
* The name of a (draggable) field
@ -112,9 +110,10 @@ export const FieldName = React.memo<{
const hoverContent = useMemo(
() => (
<DraggableWrapperHoverContent
<HoverActions
closePopOver={handleClosePopOverTrigger}
field={fieldId}
isObjectArray={false}
ownFocus={hoverActionsOwnFocus}
showTopN={showTopN}
toggleTopN={toggleTopN}

View file

@ -132,6 +132,7 @@ const NonDecoratedIpComponent: React.FC<{
return (
<DraggableWrapper
dataProvider={dataProviderProp}
isDraggable={isDraggable}
key={key}
render={render}
truncate={truncate}
@ -237,6 +238,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
return (
<DraggableWrapper
dataProvider={dataProviderProp}
isDraggable={isDraggable}
key={key}
render={render}
truncate={truncate}

View file

@ -30,14 +30,16 @@ export const Ja3Fingerprint = React.memo<{
eventId: string;
contextId: string;
fieldName: string;
isDraggable?: boolean;
value?: string | null;
}>(({ contextId, eventId, fieldName, value }) => (
}>(({ contextId, eventId, fieldName, isDraggable, value }) => (
<DraggableBadge
contextId={contextId}
data-test-subj="ja3-hash"
eventId={eventId}
field={fieldName}
iconType="snowflake"
isDraggable={isDraggable}
value={value}
>
<Ja3FingerprintLabel data-test-subj="ja3-fingerprint-label">

View file

@ -23,6 +23,7 @@ import { JA3_HASH_FIELD_NAME, Ja3Fingerprint } from '../../ja3_fingerprint';
export const Fingerprints = React.memo<{
contextId: string;
eventId: string;
isDraggable?: boolean;
tlsClientCertificateFingerprintSha1?: string[] | null;
tlsFingerprintsJa3Hash?: string[] | null;
tlsServerCertificateFingerprintSha1?: string[] | null;
@ -30,6 +31,7 @@ export const Fingerprints = React.memo<{
({
contextId,
eventId,
isDraggable,
tlsClientCertificateFingerprintSha1,
tlsFingerprintsJa3Hash,
tlsServerCertificateFingerprintSha1,
@ -48,6 +50,7 @@ export const Fingerprints = React.memo<{
eventId={eventId}
fieldName={JA3_HASH_FIELD_NAME}
contextId={contextId}
isDraggable={isDraggable}
value={ja3}
/>
</EuiFlexItem>
@ -61,6 +64,7 @@ export const Fingerprints = React.memo<{
certificateType="client"
contextId={contextId}
fieldName={TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME}
isDraggable={isDraggable}
value={clientCert}
/>
</EuiFlexItem>
@ -74,6 +78,7 @@ export const Fingerprints = React.memo<{
certificateType="server"
contextId={contextId}
fieldName={TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME}
isDraggable={isDraggable}
value={serverCert}
/>
</EuiFlexItem>

View file

@ -37,6 +37,7 @@ export const Netflow = React.memo<NetflowProps>(
eventId,
eventEnd,
eventStart,
isDraggable,
networkBytes,
networkCommunityId,
networkDirection,
@ -82,6 +83,7 @@ export const Netflow = React.memo<NetflowProps>(
eventId={eventId}
eventEnd={eventEnd}
eventStart={eventStart}
isDraggable={isDraggable}
networkBytes={networkBytes}
networkCommunityId={networkCommunityId}
networkDirection={networkDirection}
@ -105,6 +107,7 @@ export const Netflow = React.memo<NetflowProps>(
<Fingerprints
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
tlsClientCertificateFingerprintSha1={tlsClientCertificateFingerprintSha1}
tlsFingerprintsJa3Hash={tlsFingerprintsJa3Hash}
tlsServerCertificateFingerprintSha1={tlsServerCertificateFingerprintSha1}

View file

@ -38,7 +38,8 @@ export const DurationEventStartEnd = React.memo<{
eventId: string;
eventEnd?: string[] | null;
eventStart?: string[] | null;
}>(({ contextId, eventDuration, eventId, eventEnd, eventStart }) => (
isDraggable?: boolean;
}>(({ contextId, eventDuration, eventId, eventEnd, eventStart, isDraggable }) => (
<EuiFlexGroup
alignItems="flexStart"
data-test-subj="duration-and-start-group"
@ -53,6 +54,7 @@ export const DurationEventStartEnd = React.memo<{
data-test-subj="event-duration"
field={EVENT_DURATION_FIELD_NAME}
id={`duration-event-start-end-default-draggable-${contextId}-${eventId}-${EVENT_DURATION_FIELD_NAME}-${duration}`}
isDraggable={isDraggable}
// @ts-expect-error
name={name}
tooltipContent={null}
@ -76,6 +78,7 @@ export const DurationEventStartEnd = React.memo<{
data-test-subj="event-start"
field={EVENT_START_FIELD_NAME}
id={`duration-event-start-end-default-draggable-${contextId}-${eventId}-${EVENT_START_FIELD_NAME}-${start}`}
isDraggable={isDraggable}
tooltipContent={null}
value={start}
>
@ -94,6 +97,7 @@ export const DurationEventStartEnd = React.memo<{
data-test-subj="event-end"
field={EVENT_END_FIELD_NAME}
id={`duration-event-start-end-default-draggable-${contextId}-${eventId}-${EVENT_END_FIELD_NAME}-${end}`}
isDraggable={isDraggable}
tooltipContent={null}
value={end}
>

View file

@ -48,6 +48,7 @@ export const NetflowColumns = React.memo<NetflowColumnsProps>(
eventId,
eventEnd,
eventStart,
isDraggable,
networkBytes,
networkCommunityId,
networkDirection,
@ -76,6 +77,7 @@ export const NetflowColumns = React.memo<NetflowColumnsProps>(
<UserProcess
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
processName={processName}
userName={userName}
/>
@ -88,6 +90,7 @@ export const NetflowColumns = React.memo<NetflowColumnsProps>(
eventId={eventId}
eventEnd={eventEnd}
eventStart={eventStart}
isDraggable={isDraggable}
/>
</EuiFlexItemMarginRight>
@ -104,6 +107,7 @@ export const NetflowColumns = React.memo<NetflowColumnsProps>(
destinationPackets={destinationPackets}
destinationPort={destinationPort}
eventId={eventId}
isDraggable={isDraggable}
networkBytes={networkBytes}
networkCommunityId={networkCommunityId}
networkDirection={networkDirection}

View file

@ -21,6 +21,7 @@ export interface NetflowColumnsProps {
eventId: string;
eventEnd?: string[] | null;
eventStart?: string[] | null;
isDraggable?: boolean;
networkBytes?: string[] | null;
networkCommunityId?: string[] | null;
networkDirection?: string[] | null;

View file

@ -22,9 +22,10 @@ export const USER_NAME_FIELD_NAME = 'user.name';
export const UserProcess = React.memo<{
contextId: string;
eventId: string;
isDraggable?: boolean;
processName?: string[] | null;
userName?: string[] | null;
}>(({ contextId, eventId, processName, userName }) => (
}>(({ contextId, eventId, isDraggable, processName, userName }) => (
<EuiFlexGroup
alignItems="flexStart"
data-test-subj="user-process"
@ -40,6 +41,7 @@ export const UserProcess = React.memo<{
data-test-subj="user-name"
eventId={eventId}
field={USER_NAME_FIELD_NAME}
isDraggable={isDraggable}
value={user}
iconType="user"
/>
@ -55,6 +57,7 @@ export const UserProcess = React.memo<{
data-test-subj="process-name"
eventId={eventId}
field={PROCESS_NAME_FIELD_NAME}
isDraggable={isDraggable}
value={process}
iconType="console"
/>

View file

@ -20,6 +20,7 @@ export interface NetflowProps {
eventId: string;
eventEnd?: string[] | null;
eventStart?: string[] | null;
isDraggable?: boolean;
networkBytes?: string[] | null;
networkCommunityId?: string[] | null;
networkDirection?: string[] | null;

View file

@ -26,6 +26,7 @@ const AlertsExampleComponent: React.FC = () => {
{alertsRowRenderer.renderRow({
browserFields: {},
data: mockEndpointProcessExecutionMalwarePreventionAlert,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const AuditdExampleComponent: React.FC = () => {
{auditdRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[26].ecs,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const AuditdFileExampleComponent: React.FC = () => {
{auditdFileRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[27].ecs,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const LibraryExampleComponent: React.FC = () => {
{libraryRowRenderer.renderRow({
browserFields: {},
data: mockEndpointLibraryLoadEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -16,6 +16,7 @@ const NetflowExampleComponent: React.FC = () => (
{netflowRowRenderer.renderRow({
browserFields: {},
data: getMockNetflowData(),
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const RegistryExampleComponent: React.FC = () => {
{registryRowRenderer.renderRow({
browserFields: {},
data: mockEndpointRegistryModificationEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -16,6 +16,7 @@ const SuricataExampleComponent: React.FC = () => (
{suricataRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[2].ecs,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const SystemExampleComponent: React.FC = () => {
{systemRowRenderer.renderRow({
browserFields: {},
data: mockEndgameTerminationEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -19,6 +19,7 @@ const SystemDnsExampleComponent: React.FC = () => {
{systemDnsRowRenderer.renderRow({
browserFields: {},
data: mockEndgameDnsRequest,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const SystemEndgameProcessExampleComponent: React.FC = () => {
{systemEndgameProcessRowRenderer.renderRow({
browserFields: {},
data: mockEndgameCreationEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const SystemFileExampleComponent: React.FC = () => {
{systemFileRowRenderer.renderRow({
browserFields: {},
data: mockEndgameFileDeleteEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -23,6 +23,7 @@ const SystemFimExampleComponent: React.FC = () => {
{systemFimRowRenderer.renderRow({
browserFields: {},
data: mockEndgameFileCreateEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -21,6 +21,7 @@ const SystemSecurityEventExampleComponent: React.FC = () => {
{systemSecurityEventRowRenderer.renderRow({
browserFields: {},
data: mockEndgameUserLogon,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -22,6 +22,7 @@ const SystemSocketExampleComponent: React.FC = () => {
{systemSocketRowRenderer.renderRow({
browserFields: {},
data: mockEndgameIpv4ConnectionAcceptEvent,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -16,6 +16,7 @@ const ThreatMatchExampleComponent: React.FC = () => (
{threatMatchRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[31].ecs,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -16,6 +16,7 @@ const ZeekExampleComponent: React.FC = () => (
{zeekRowRenderer.renderRow({
browserFields: {},
data: mockTimelineData[13].ecs,
isDraggable: false,
timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID,
})}
</>

View file

@ -80,6 +80,7 @@ export const StatefulRowRenderer = ({
{rowRenderer.renderRow({
browserFields,
data: event.ecs,
isDraggable: true,
timelineId,
})}
</div>

View file

@ -19,6 +19,7 @@ exports[`empty_column_renderer renders correctly against snapshot 1`] = `
},
}
}
isDraggable={true}
key="empty-column-renderer-draggable-wrapper-test-source.ip-1-source.ip"
render={[Function]}
/>

View file

@ -42,6 +42,7 @@ export const AgentStatuses = React.memo(
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={fieldName}
value={`${agentStatus}`}
>
@ -60,6 +61,7 @@ export const AgentStatuses = React.memo(
<DefaultDraggable
field={isolationFieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${isolationFieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={isolationFieldName}
value={`${isIsolated}`}
>

View file

@ -15,9 +15,10 @@ interface Props {
contextId: string;
eventId: string;
processTitle: string | null | undefined;
isDraggable?: boolean;
}
export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) => {
export const ArgsComponent = ({ args, contextId, eventId, processTitle, isDraggable }: Props) => {
if (isNillEmptyOrNotFinite(args) && isNillEmptyOrNotFinite(processTitle)) {
return null;
}
@ -31,6 +32,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props)
contextId={`${contextId}-args-${i}-${arg}`}
eventId={eventId}
field="process.args"
isDraggable={isDraggable}
value={arg}
/>
</TokensFlexItem>
@ -42,6 +44,7 @@ export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props)
contextId={contextId}
eventId={eventId}
field="process.title"
isDraggable={isDraggable}
value={processTitle}
/>
</TokensFlexItem>

View file

@ -98,6 +98,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
},
}
}
isDraggable={true}
text="connected using"
timelineId="test"
/>
@ -222,6 +223,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
}
}
fileIcon="document"
isDraggable={true}
text="opened file using"
timelineId="test"
/>

View file

@ -36,6 +36,7 @@ interface Props {
workingDirectory: string | null | undefined;
args: string[] | null | undefined;
session: string | null | undefined;
isDraggable?: boolean;
}
export const AuditdGenericLine = React.memo<Props>(
@ -55,6 +56,7 @@ export const AuditdGenericLine = React.memo<Props>(
result,
session,
text,
isDraggable,
}) => (
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="none" wrap={true}>
<SessionUserHostWorkingDir
@ -66,6 +68,7 @@ export const AuditdGenericLine = React.memo<Props>(
secondary={secondary}
workingDirectory={workingDirectory}
session={session}
isDraggable={isDraggable}
/>
{processExecutable != null && (
<TokensFlexItem grow={false} component="span">
@ -81,9 +84,16 @@ export const AuditdGenericLine = React.memo<Props>(
processPid={processPid}
processName={processName}
processExecutable={processExecutable}
isDraggable={isDraggable}
/>
</TokensFlexItem>
<Args eventId={id} args={args} contextId={contextId} processTitle={processTitle} />
<Args
eventId={id}
args={args}
contextId={contextId}
isDraggable={isDraggable}
processTitle={processTitle}
/>
{result != null && (
<TokensFlexItem grow={false} component="span">
{i18n.WITH_RESULT}
@ -94,6 +104,7 @@ export const AuditdGenericLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="auditd.result"
isDraggable={isDraggable}
queryValue={result}
value={result}
/>
@ -107,13 +118,14 @@ AuditdGenericLine.displayName = 'AuditdGenericLine';
interface GenericDetailsProps {
browserFields: BrowserFields;
data: Ecs;
isDraggable?: boolean;
contextId: string;
text: string;
timelineId: string;
}
export const AuditdGenericDetails = React.memo<GenericDetailsProps>(
({ data, contextId, text, timelineId }) => {
({ data, contextId, isDraggable, text, timelineId }) => {
const id = data._id;
const session: string | null | undefined = get('auditd.session[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
@ -146,9 +158,10 @@ export const AuditdGenericDetails = React.memo<GenericDetailsProps>(
primary={primary}
result={result}
secondary={secondary}
isDraggable={isDraggable}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} timelineId={timelineId} />
<NetflowRenderer data={data} isDraggable={isDraggable} timelineId={timelineId} />
</Details>
);
} else {

View file

@ -38,6 +38,7 @@ interface Props {
workingDirectory: string | null | undefined;
args: string[] | null | undefined;
session: string | null | undefined;
isDraggable?: boolean;
}
export const AuditdGenericFileLine = React.memo<Props>(
@ -59,6 +60,7 @@ export const AuditdGenericFileLine = React.memo<Props>(
session,
text,
fileIcon,
isDraggable,
}) => (
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="none" wrap={true}>
<SessionUserHostWorkingDir
@ -70,6 +72,7 @@ export const AuditdGenericFileLine = React.memo<Props>(
secondary={secondary}
workingDirectory={workingDirectory}
session={session}
isDraggable={isDraggable}
/>
{(filePath != null || processExecutable != null) && (
<TokensFlexItem grow={false} component="span">
@ -81,6 +84,7 @@ export const AuditdGenericFileLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="file.path"
isDraggable={isDraggable}
value={filePath}
iconType={fileIcon}
/>
@ -96,12 +100,19 @@ export const AuditdGenericFileLine = React.memo<Props>(
endgamePid={undefined}
endgameProcessName={undefined}
eventId={id}
isDraggable={isDraggable}
processPid={processPid}
processName={processName}
processExecutable={processExecutable}
/>
</TokensFlexItem>
<Args eventId={id} args={args} contextId={contextId} processTitle={processTitle} />
<Args
eventId={id}
args={args}
contextId={contextId}
isDraggable={isDraggable}
processTitle={processTitle}
/>
{result != null && (
<TokensFlexItem grow={false} component="span">
{i18n.WITH_RESULT}
@ -112,6 +123,7 @@ export const AuditdGenericFileLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="auditd.result"
isDraggable={isDraggable}
queryValue={result}
value={result}
/>
@ -124,15 +136,16 @@ AuditdGenericFileLine.displayName = 'AuditdGenericFileLine';
interface GenericDetailsProps {
browserFields: BrowserFields;
data: Ecs;
contextId: string;
data: Ecs;
text: string;
fileIcon: IconType;
timelineId: string;
isDraggable?: boolean;
}
export const AuditdGenericFileDetails = React.memo<GenericDetailsProps>(
({ data, contextId, text, fileIcon = 'document', timelineId }) => {
({ data, contextId, text, fileIcon = 'document', timelineId, isDraggable }) => {
const id = data._id;
const session: string | null | undefined = get('auditd.session[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
@ -169,9 +182,10 @@ export const AuditdGenericFileDetails = React.memo<GenericDetailsProps>(
secondary={secondary}
fileIcon={fileIcon}
result={result}
isDraggable={isDraggable}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} timelineId={timelineId} />
<NetflowRenderer data={data} isDraggable={isDraggable} timelineId={timelineId} />
</Details>
);
} else {

View file

@ -55,6 +55,7 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields,
data: auditd,
isDraggable: true,
timelineId: 'test',
});
@ -84,6 +85,7 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields: mockBrowserFields,
data: auditd,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -117,6 +119,7 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields,
data: auditdFile,
isDraggable: true,
timelineId: 'test',
});
@ -146,6 +149,7 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields: mockBrowserFields,
data: auditdFile,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(

View file

@ -36,11 +36,12 @@ export const createGenericAuditRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, timelineId }) => (
renderRow: ({ browserFields, data, isDraggable, timelineId }) => (
<RowRendererContainer>
<AuditdGenericDetails
browserFields={browserFields}
data={data}
isDraggable={isDraggable}
contextId={`${actionName}-${timelineId}`}
text={text}
timelineId={timelineId}
@ -69,14 +70,15 @@ export const createGenericFileRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, timelineId }) => (
renderRow: ({ browserFields, data, isDraggable, timelineId }) => (
<RowRendererContainer>
<AuditdGenericFileDetails
browserFields={browserFields}
data={data}
contextId={`${actionName}-${timelineId}`}
text={text}
data={data}
fileIcon={fileIcon}
isDraggable={isDraggable}
text={text}
timelineId={timelineId}
/>
</RowRendererContainer>

View file

@ -21,69 +21,77 @@ interface Props {
eventId: string;
primary: string | null | undefined;
secondary: string | null | undefined;
isDraggable?: boolean;
}
export const PrimarySecondary = React.memo<Props>(({ contextId, eventId, primary, secondary }) => {
if (nilOrUnSet(primary) && nilOrUnSet(secondary)) {
return null;
} else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.primary"
value={primary}
iconType="user"
/>
);
} else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
value={secondary}
iconType="user"
/>
);
} else if (primary === secondary) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
value={secondary}
iconType="user"
/>
);
} else {
return (
<EuiFlexGroup gutterSize="none">
<TokensFlexItem grow={false} component="span">
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.primary"
value={primary}
iconType="user"
/>
</TokensFlexItem>
<TokensFlexItem grow={false} component="span">
{i18n.AS}
</TokensFlexItem>
<TokensFlexItem grow={false} component="span">
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
value={secondary}
iconType="user"
/>
</TokensFlexItem>
</EuiFlexGroup>
);
export const PrimarySecondary = React.memo<Props>(
({ contextId, eventId, primary, secondary, isDraggable }) => {
if (nilOrUnSet(primary) && nilOrUnSet(secondary)) {
return null;
} else if (!nilOrUnSet(primary) && nilOrUnSet(secondary)) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.primary"
isDraggable={isDraggable}
value={primary}
iconType="user"
/>
);
} else if (nilOrUnSet(primary) && !nilOrUnSet(secondary)) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
isDraggable={isDraggable}
value={secondary}
iconType="user"
/>
);
} else if (primary === secondary) {
return (
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
isDraggable={isDraggable}
value={secondary}
iconType="user"
/>
);
} else {
return (
<EuiFlexGroup gutterSize="none">
<TokensFlexItem grow={false} component="span">
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.primary"
isDraggable={isDraggable}
value={primary}
iconType="user"
/>
</TokensFlexItem>
<TokensFlexItem grow={false} component="span">
{i18n.AS}
</TokensFlexItem>
<TokensFlexItem grow={false} component="span">
<DraggableBadge
contextId={contextId}
eventId={eventId}
field="auditd.summary.actor.secondary"
isDraggable={isDraggable}
value={secondary}
iconType="user"
/>
</TokensFlexItem>
</EuiFlexGroup>
);
}
}
});
);
PrimarySecondary.displayName = 'PrimarySecondary';
@ -93,10 +101,11 @@ interface PrimarySecondaryUserInfoProps {
userName: string | null | undefined;
primary: string | null | undefined;
secondary: string | null | undefined;
isDraggable?: boolean;
}
export const PrimarySecondaryUserInfo = React.memo<PrimarySecondaryUserInfoProps>(
({ contextId, eventId, userName, primary, secondary }) => {
({ contextId, eventId, userName, primary, secondary, isDraggable }) => {
if (nilOrUnSet(userName) && nilOrUnSet(primary) && nilOrUnSet(secondary)) {
return null;
} else if (
@ -111,6 +120,7 @@ export const PrimarySecondaryUserInfo = React.memo<PrimarySecondaryUserInfoProps
contextId={contextId}
eventId={eventId}
field="user.name"
isDraggable={isDraggable}
value={userName}
iconType="user"
/>
@ -121,6 +131,7 @@ export const PrimarySecondaryUserInfo = React.memo<PrimarySecondaryUserInfoProps
contextId={contextId}
eventId={eventId}
field="user.name"
isDraggable={isDraggable}
value={userName}
iconType="user"
/>
@ -130,6 +141,7 @@ export const PrimarySecondaryUserInfo = React.memo<PrimarySecondaryUserInfoProps
<PrimarySecondary
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
primary={primary}
secondary={secondary}
/>

View file

@ -23,10 +23,21 @@ interface Props {
secondary: string | null | undefined;
workingDirectory: string | null | undefined;
session: string | null | undefined;
isDraggable?: boolean;
}
export const SessionUserHostWorkingDir = React.memo<Props>(
({ eventId, contextId, hostName, userName, primary, secondary, workingDirectory, session }) => (
({
eventId,
contextId,
hostName,
userName,
primary,
secondary,
workingDirectory,
session,
isDraggable,
}) => (
<>
<TokensFlexItem grow={false} component="span">
{i18n.SESSION}
@ -38,6 +49,7 @@ export const SessionUserHostWorkingDir = React.memo<Props>(
field="auditd.session"
value={session}
iconType="number"
isDraggable={isDraggable}
/>
</TokensFlexItem>
<TokensFlexItem grow={false} component="span">
@ -47,6 +59,7 @@ export const SessionUserHostWorkingDir = React.memo<Props>(
userName={userName}
primary={primary}
secondary={secondary}
isDraggable={isDraggable}
/>
</TokensFlexItem>
{hostName != null && (
@ -59,6 +72,7 @@ export const SessionUserHostWorkingDir = React.memo<Props>(
eventId={eventId}
workingDirectory={workingDirectory}
hostName={hostName}
isDraggable={isDraggable}
/>
</>
)

View file

@ -26,6 +26,7 @@ export const Bytes = React.memo<{
isDraggable ? (
<DefaultDraggable
id={`bytes-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
// @ts-expect-error
name={name}
field={fieldName}

View file

@ -24,6 +24,7 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1`
}
}
eventId="1"
isDraggable={true}
/>
</styled.div>
</RowRendererContainer>

View file

@ -26,6 +26,7 @@ interface IndicatorDetailsProps {
indicatorProvider: string | undefined;
indicatorReference: string | undefined;
indicatorType: string | undefined;
isDraggable?: boolean;
}
export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
@ -35,6 +36,7 @@ export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
indicatorProvider,
indicatorReference,
indicatorType,
isDraggable,
}) => (
<EuiFlexGroup
alignItems="flexStart"
@ -51,6 +53,7 @@ export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
data-test-subj="threat-match-indicator-details-indicator-type"
eventId={eventId}
field={INDICATOR_MATCHED_TYPE}
isDraggable={isDraggable}
value={indicatorType}
/>
</EuiFlexItem>
@ -71,6 +74,7 @@ export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
data-test-subj="threat-match-indicator-details-indicator-dataset"
eventId={eventId}
field={INDICATOR_DATASET}
isDraggable={isDraggable}
value={indicatorDataset}
/>
</EuiFlexItem>
@ -92,6 +96,7 @@ export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
data-test-subj="threat-match-indicator-details-indicator-provider"
eventId={eventId}
field={INDICATOR_PROVIDER}
isDraggable={isDraggable}
value={indicatorProvider}
/>
</EuiFlexItem>
@ -108,6 +113,7 @@ export const IndicatorDetails: React.FC<IndicatorDetailsProps> = ({
data-test-subj="threat-match-indicator-details-indicator-reference"
eventId={eventId}
fieldName={INDICATOR_REFERENCE}
isDraggable={isDraggable}
value={indicatorReference}
/>
</EuiFlexItem>

View file

@ -16,6 +16,7 @@ import { HorizontalSpacer } from './helpers';
interface MatchDetailsProps {
contextId: string;
eventId: string;
isDraggable?: boolean;
sourceField: string;
sourceValue: string;
}
@ -23,6 +24,7 @@ interface MatchDetailsProps {
export const MatchDetails: React.FC<MatchDetailsProps> = ({
contextId,
eventId,
isDraggable,
sourceField,
sourceValue,
}) => (
@ -40,6 +42,7 @@ export const MatchDetails: React.FC<MatchDetailsProps> = ({
data-test-subj="threat-match-details-source-field"
eventId={eventId}
field={INDICATOR_MATCHED_FIELD}
isDraggable={isDraggable}
value={sourceField}
/>
</EuiFlexItem>
@ -57,6 +60,7 @@ export const MatchDetails: React.FC<MatchDetailsProps> = ({
data-test-subj="threat-match-details-source-value"
eventId={eventId}
field={sourceField}
isDraggable={isDraggable}
value={sourceValue}
/>
</EuiFlexItem>

View file

@ -28,6 +28,7 @@ export interface ThreatMatchRowProps {
indicatorProvider: string | undefined;
indicatorReference: string | undefined;
indicatorType: string | undefined;
isDraggable?: boolean;
sourceField: string;
sourceValue: string;
}
@ -36,10 +37,12 @@ export const ThreatMatchRow = ({
contextId,
data,
eventId,
isDraggable,
}: {
contextId: string;
data: Fields;
eventId: string;
isDraggable?: boolean;
}) => {
const props = {
contextId,
@ -48,6 +51,7 @@ export const ThreatMatchRow = ({
indicatorReference: get(data, EVENT_REFERENCE)[0] as string | undefined,
indicatorProvider: get(data, PROVIDER)[0] as string | undefined,
indicatorType: get(data, MATCHED_TYPE)[0] as string | undefined,
isDraggable,
sourceField: get(data, MATCHED_FIELD)[0] as string,
sourceValue: get(data, MATCHED_ATOMIC)[0] as string,
};
@ -62,6 +66,7 @@ export const ThreatMatchRowView = ({
indicatorProvider,
indicatorReference,
indicatorType,
isDraggable,
sourceField,
sourceValue,
}: ThreatMatchRowProps) => {
@ -76,6 +81,7 @@ export const ThreatMatchRowView = ({
<MatchDetails
contextId={contextId}
eventId={eventId}
isDraggable={isDraggable}
sourceField={sourceField}
sourceValue={sourceValue}
/>
@ -88,6 +94,7 @@ export const ThreatMatchRowView = ({
indicatorProvider={indicatorProvider}
indicatorReference={indicatorReference}
indicatorType={indicatorType}
isDraggable={isDraggable}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -56,6 +56,7 @@ describe('threatMatchRowRenderer', () => {
const children = threatMatchRowRenderer.renderRow({
browserFields: {},
data: threatMatchData,
isDraggable: true,
timelineId: 'test',
});
const wrapper = shallow(<span>{children}</span>);

View file

@ -20,7 +20,7 @@ const SpacedContainer = styled.div`
margin: ${({ theme }) => theme.eui.paddingSizes.s} 0;
`;
export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) => {
export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, isDraggable, timelineId }) => {
const indicators = get(data, 'threat.indicator') as Fields[];
const eventId = get(data, ID_FIELD_NAME);
@ -31,7 +31,12 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId })
const contextId = `threat-match-row-${timelineId}-${eventId}-${index}`;
return (
<Fragment key={contextId}>
<ThreatMatchRow contextId={contextId} data={indicator} eventId={eventId} />
<ThreatMatchRow
contextId={contextId}
data={indicator}
eventId={eventId}
isDraggable={isDraggable}
/>
{index < indicators.length - 1 && <EuiHorizontalRule margin="s" />}
</Fragment>
);

View file

@ -20,46 +20,50 @@ interface Props {
browserFields: BrowserFields;
contextId: string;
data: Ecs;
isDraggable?: boolean;
timelineId: string;
}
export const DnsRequestEventDetails = React.memo<Props>(({ data, contextId, timelineId }) => {
const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data);
const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data);
const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data);
const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data);
const eventCode: string | null | undefined = get('event.code[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
const id = data._id;
const processExecutable: string | null | undefined = get('process.executable[0]', data);
const processName: string | null | undefined = get('process.name[0]', data);
const processPid: number | null | undefined = get('process.pid[0]', data);
const userDomain: string | null | undefined = get('user.domain[0]', data);
const userName: string | null | undefined = get('user.name[0]', data);
const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data);
export const DnsRequestEventDetails = React.memo<Props>(
({ data, contextId, isDraggable, timelineId }) => {
const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data);
const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data);
const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data);
const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data);
const eventCode: string | null | undefined = get('event.code[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
const id = data._id;
const processExecutable: string | null | undefined = get('process.executable[0]', data);
const processName: string | null | undefined = get('process.name[0]', data);
const processPid: number | null | undefined = get('process.pid[0]', data);
const userDomain: string | null | undefined = get('user.domain[0]', data);
const userName: string | null | undefined = get('user.name[0]', data);
const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data);
return (
<Details>
<DnsRequestEventDetailsLine
contextId={contextId}
dnsQuestionName={dnsQuestionName}
dnsQuestionType={dnsQuestionType}
dnsResolvedIp={dnsResolvedIp}
dnsResponseCode={dnsResponseCode}
eventCode={eventCode}
hostName={hostName}
id={id}
processExecutable={processExecutable}
processName={processName}
processPid={processPid}
userDomain={userDomain}
userName={userName}
winlogEventId={winlogEventId}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} timelineId={timelineId} />
</Details>
);
});
return (
<Details>
<DnsRequestEventDetailsLine
contextId={contextId}
dnsQuestionName={dnsQuestionName}
dnsQuestionType={dnsQuestionType}
dnsResolvedIp={dnsResolvedIp}
dnsResponseCode={dnsResponseCode}
eventCode={eventCode}
hostName={hostName}
id={id}
isDraggable={isDraggable}
processExecutable={processExecutable}
processName={processName}
processPid={processPid}
userDomain={userDomain}
userName={userName}
winlogEventId={winlogEventId}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} isDraggable={isDraggable} timelineId={timelineId} />
</Details>
);
}
);
DnsRequestEventDetails.displayName = 'DnsRequestEventDetails';

View file

@ -24,6 +24,7 @@ interface Props {
eventCode: string | null | undefined;
hostName: string | null | undefined;
id: string;
isDraggable?: boolean;
processExecutable: string | null | undefined;
processName: string | null | undefined;
processPid: number | null | undefined;
@ -42,6 +43,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
eventCode,
hostName,
id,
isDraggable,
processExecutable,
processName,
processPid,
@ -56,6 +58,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
hostName={hostName}
isDraggable={isDraggable}
userDomain={userDomain}
userName={userName}
workingDirectory={undefined}
@ -71,6 +74,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="dns.question.name"
isDraggable={isDraggable}
value={dnsQuestionName}
/>
</TokensFlexItem>
@ -87,6 +91,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="dns.question.type"
isDraggable={isDraggable}
value={dnsQuestionType}
/>
</TokensFlexItem>
@ -103,6 +108,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="dns.resolved_ip"
isDraggable={isDraggable}
value={dnsResolvedIp}
/>
</TokensFlexItem>
@ -122,6 +128,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="dns.response_code"
isDraggable={isDraggable}
value={dnsResponseCode}
/>
</TokensFlexItem>
@ -141,6 +148,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
endgamePid={undefined}
endgameProcessName={undefined}
eventId={id}
isDraggable={isDraggable}
processPid={processPid}
processName={processName}
processExecutable={processExecutable}
@ -155,6 +163,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="event.code"
isDraggable={isDraggable}
value={eventCode}
/>
</TokensFlexItem>
@ -165,6 +174,7 @@ export const DnsRequestEventDetailsLine = React.memo<Props>(
eventId={id}
iconType="logoWindows"
field="winlog.event_id"
isDraggable={isDraggable}
value={winlogEventId}
/>
</TokensFlexItem>

View file

@ -60,6 +60,7 @@ export const emptyColumnRenderer: ColumnRenderer = {
kqlQuery: '',
and: [],
}}
isDraggable={isDraggable}
key={`empty-column-renderer-draggable-wrapper-${timelineId}-${columnName}-${eventId}-${field.id}`}
render={(dataProvider, _, snapshot) =>
snapshot.isDragging ? (

View file

@ -20,65 +20,75 @@ interface Props {
browserFields: BrowserFields;
contextId: string;
data: Ecs;
isDraggable?: boolean;
timelineId: string;
}
export const EndgameSecurityEventDetails = React.memo<Props>(({ data, contextId, timelineId }) => {
const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data);
const endgameSubjectDomainName: string | null | undefined = get(
'endgame.subject_domain_name[0]',
data
);
const endgameSubjectLogonId: string | null | undefined = get('endgame.subject_logon_id[0]', data);
const endgameSubjectUserName: string | null | undefined = get(
'endgame.subject_user_name[0]',
data
);
const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data);
const endgameTargetDomainName: string | null | undefined = get(
'endgame.target_domain_name[0]',
data
);
const endgameTargetUserName: string | null | undefined = get('endgame.target_user_name[0]', data);
const eventAction: string | null | undefined = get('event.action[0]', data);
const eventCode: string | null | undefined = get('event.code[0]', data);
const eventOutcome: string | null | undefined = get('event.outcome[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
const id = data._id;
const processExecutable: string | null | undefined = get('process.executable[0]', data);
const processName: string | null | undefined = get('process.name[0]', data);
const processPid: number | null | undefined = get('process.pid[0]', data);
const userDomain: string | null | undefined = get('user.domain[0]', data);
const userName: string | null | undefined = get('user.name[0]', data);
const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data);
export const EndgameSecurityEventDetails = React.memo<Props>(
({ data, contextId, isDraggable, timelineId }) => {
const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data);
const endgameSubjectDomainName: string | null | undefined = get(
'endgame.subject_domain_name[0]',
data
);
const endgameSubjectLogonId: string | null | undefined = get(
'endgame.subject_logon_id[0]',
data
);
const endgameSubjectUserName: string | null | undefined = get(
'endgame.subject_user_name[0]',
data
);
const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data);
const endgameTargetDomainName: string | null | undefined = get(
'endgame.target_domain_name[0]',
data
);
const endgameTargetUserName: string | null | undefined = get(
'endgame.target_user_name[0]',
data
);
const eventAction: string | null | undefined = get('event.action[0]', data);
const eventCode: string | null | undefined = get('event.code[0]', data);
const eventOutcome: string | null | undefined = get('event.outcome[0]', data);
const hostName: string | null | undefined = get('host.name[0]', data);
const id = data._id;
const processExecutable: string | null | undefined = get('process.executable[0]', data);
const processName: string | null | undefined = get('process.name[0]', data);
const processPid: number | null | undefined = get('process.pid[0]', data);
const userDomain: string | null | undefined = get('user.domain[0]', data);
const userName: string | null | undefined = get('user.name[0]', data);
const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data);
return (
<Details>
<EndgameSecurityEventDetailsLine
contextId={contextId}
endgameLogonType={endgameLogonType}
endgameSubjectDomainName={endgameSubjectDomainName}
endgameSubjectLogonId={endgameSubjectLogonId}
endgameSubjectUserName={endgameSubjectUserName}
endgameTargetDomainName={endgameTargetDomainName}
endgameTargetLogonId={endgameTargetLogonId}
endgameTargetUserName={endgameTargetUserName}
eventAction={eventAction}
eventCode={eventCode}
eventOutcome={eventOutcome}
hostName={hostName}
id={id}
processExecutable={processExecutable}
processName={processName}
processPid={processPid}
userDomain={userDomain}
userName={userName}
winlogEventId={winlogEventId}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} timelineId={timelineId} />
</Details>
);
});
return (
<Details>
<EndgameSecurityEventDetailsLine
contextId={contextId}
endgameLogonType={endgameLogonType}
endgameSubjectDomainName={endgameSubjectDomainName}
endgameSubjectLogonId={endgameSubjectLogonId}
endgameSubjectUserName={endgameSubjectUserName}
endgameTargetDomainName={endgameTargetDomainName}
endgameTargetLogonId={endgameTargetLogonId}
endgameTargetUserName={endgameTargetUserName}
eventAction={eventAction}
eventCode={eventCode}
eventOutcome={eventOutcome}
hostName={hostName}
id={id}
isDraggable={isDraggable}
processExecutable={processExecutable}
processName={processName}
processPid={processPid}
userDomain={userDomain}
userName={userName}
winlogEventId={winlogEventId}
/>
<EuiSpacer size="s" />
<NetflowRenderer data={data} timelineId={timelineId} />
</Details>
);
}
);
EndgameSecurityEventDetails.displayName = 'EndgameSecurityEventDetails';

View file

@ -38,6 +38,7 @@ interface Props {
eventOutcome: string | null | undefined;
hostName: string | null | undefined;
id: string;
isDraggable?: boolean;
processExecutable: string | null | undefined;
processName: string | null | undefined;
processPid: number | null | undefined;
@ -61,6 +62,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
eventOutcome,
hostName,
id,
isDraggable,
processExecutable,
processName,
processPid,
@ -95,6 +97,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
eventId={id}
hostName={hostName}
hostNameSeparator={hostNameSeparator}
isDraggable={isDraggable}
userDomain={domain}
userDomainField={userDomainField}
userName={user}
@ -116,6 +119,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="endgame.logon_type"
isDraggable={isDraggable}
queryValue={String(endgameLogonType)}
value={`${endgameLogonType} - ${getHumanReadableLogonType(endgameLogonType)}`}
/>
@ -136,6 +140,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="endgame.target_logon_id"
isDraggable={isDraggable}
value={endgameTargetLogonId}
/>
</TokensFlexItem>
@ -155,6 +160,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
endgamePid={undefined}
endgameProcessName={undefined}
eventId={id}
isDraggable={isDraggable}
processPid={processPid}
processName={processName}
processExecutable={processExecutable}
@ -176,6 +182,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="endgame.subject_user_name"
isDraggable={isDraggable}
iconType="user"
value={endgameSubjectUserName}
/>
@ -197,6 +204,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="endgame.subject_domain_name"
isDraggable={isDraggable}
value={endgameSubjectDomainName}
/>
</TokensFlexItem>
@ -216,6 +224,7 @@ export const EndgameSecurityEventDetailsLine = React.memo<Props>(
contextId={contextId}
eventId={id}
field="endgame.subject_logon_id"
isDraggable={isDraggable}
value={endgameSubjectLogonId}
/>
</TokensFlexItem>

View file

@ -15,12 +15,13 @@ interface Props {
contextId: string;
endgameExitCode: string | null | undefined;
eventId: string;
isDraggable?: boolean;
processExitCode: number | null | undefined;
text: string | null | undefined;
}
export const ExitCodeDraggable = React.memo<Props>(
({ contextId, endgameExitCode, eventId, processExitCode, text }) => {
({ contextId, endgameExitCode, eventId, isDraggable, processExitCode, text }) => {
if (isNillEmptyOrNotFinite(processExitCode) && isNillEmptyOrNotFinite(endgameExitCode)) {
return null;
}
@ -39,6 +40,7 @@ export const ExitCodeDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="process.exit_code"
isDraggable={isDraggable}
value={`${processExitCode}`}
/>
</TokensFlexItem>
@ -50,6 +52,7 @@ export const ExitCodeDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="endgame.exit_code"
isDraggable={isDraggable}
value={endgameExitCode}
/>
</TokensFlexItem>

View file

@ -20,6 +20,7 @@ interface Props {
fileName: string | null | undefined;
filePath: string | null | undefined;
fileExtOriginalPath: string | null | undefined;
isDraggable?: boolean;
}
export const FileDraggable = React.memo<Props>(
@ -31,6 +32,7 @@ export const FileDraggable = React.memo<Props>(
fileExtOriginalPath,
fileName,
filePath,
isDraggable,
}) => {
if (
isNillEmptyOrNotFinite(fileName) &&
@ -52,6 +54,7 @@ export const FileDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="file.name"
isDraggable={isDraggable}
value={fileName}
iconType="document"
/>
@ -62,6 +65,7 @@ export const FileDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="endgame.file_name"
isDraggable={isDraggable}
value={endgameFileName}
iconType="document"
/>
@ -80,6 +84,7 @@ export const FileDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="file.path"
isDraggable={isDraggable}
value={filePath}
iconType="document"
/>
@ -90,6 +95,7 @@ export const FileDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="endgame.file_path"
isDraggable={isDraggable}
value={endgameFilePath}
iconType="document"
/>
@ -106,6 +112,7 @@ export const FileDraggable = React.memo<Props>(
contextId={contextId}
eventId={eventId}
field="file.Ext.original.path"
isDraggable={isDraggable}
value={fileExtOriginalPath}
iconType="document"
/>

View file

@ -21,9 +21,10 @@ interface Props {
contextId: string;
eventId: string;
fileHashSha256: string | null | undefined;
isDraggable?: boolean;
}
export const FileHash = React.memo<Props>(({ contextId, eventId, fileHashSha256 }) => {
export const FileHash = React.memo<Props>(({ contextId, eventId, fileHashSha256, isDraggable }) => {
if (isNillEmptyOrNotFinite(fileHashSha256)) {
return null;
}
@ -35,6 +36,7 @@ export const FileHash = React.memo<Props>(({ contextId, eventId, fileHashSha256
contextId={contextId}
eventId={eventId}
field="file.hash.sha256"
isDraggable={isDraggable}
iconType="number"
value={fileHashSha256}
/>

View file

@ -86,6 +86,7 @@ const FormattedFieldValueComponent: React.FC<{
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={null}
value={`${value}`}
>
@ -214,6 +215,7 @@ const FormattedFieldValueComponent: React.FC<{
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
value={`${value}`}
tooltipContent={
fieldType === DATE_FIELD_TYPE || fieldType === EVENT_DURATION_FIELD_NAME

View file

@ -82,6 +82,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`}
isDraggable={isDraggable}
tooltipContent={value}
value={value}
>
@ -95,6 +96,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`}
isDraggable={isDraggable}
tooltipContent={value}
value={`${value}`}
>
@ -150,6 +152,7 @@ export const renderEventModule = ({
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${moduleName}`}
isDraggable={isDraggable}
tooltipContent={value}
value={value}
>
@ -218,6 +221,7 @@ export const renderUrl = ({
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${urlName}`}
isDraggable={isDraggable}
tooltipContent={value}
value={value}
>

View file

@ -54,6 +54,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
isDraggable: true,
timelineId: 'test',
});
@ -66,6 +67,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -81,6 +83,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: suricata,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -99,6 +102,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: suricata,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -117,6 +121,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: zeek,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -135,6 +140,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: system,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(
@ -153,6 +159,7 @@ describe('get_column_renderer', () => {
const row = rowRenderer?.renderRow({
browserFields: mockBrowserFields,
data: auditd,
isDraggable: true,
timelineId: 'test',
});
const wrapper = mount(

View file

@ -93,6 +93,7 @@ const HostNameComponent: React.FC<Props> = ({
<DefaultDraggable
field={fieldName}
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
isDraggable={isDraggable}
tooltipContent={fieldName}
value={hostName}
>

View file

@ -17,10 +17,11 @@ interface Props {
eventId: string;
hostName: string | null | undefined;
workingDirectory: string | null | undefined;
isDraggable?: boolean;
}
export const HostWorkingDir = React.memo<Props>(
({ contextId, eventId, hostName, workingDirectory }) => (
({ contextId, eventId, hostName, workingDirectory, isDraggable }) => (
<>
<TokensFlexItem grow={false} component="span">
<DraggableBadge
@ -28,6 +29,7 @@ export const HostWorkingDir = React.memo<Props>(
eventId={eventId}
field="host.name"
value={hostName}
isDraggable={isDraggable}
/>
</TokensFlexItem>
{workingDirectory != null && (
@ -42,6 +44,7 @@ export const HostWorkingDir = React.memo<Props>(
field="process.working_directory"
value={workingDirectory}
iconType="folderOpen"
isDraggable={isDraggable}
/>
</TokensFlexItem>
</>

View file

@ -60,52 +60,58 @@ import {
interface NetflowRendererProps {
data: Ecs;
timelineId: string;
isDraggable?: boolean;
}
export const NetflowRenderer = React.memo<NetflowRendererProps>(({ data, timelineId }) => (
<Netflow
contextId={`netflow-renderer-${timelineId}-${data._id}`}
destinationBytes={asArrayIfExists(get(DESTINATION_BYTES_FIELD_NAME, data))}
destinationGeoContinentName={asArrayIfExists(
get(DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, data)
)}
destinationGeoCountryName={asArrayIfExists(get(DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, data))}
destinationGeoCountryIsoCode={asArrayIfExists(
get(DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, data)
)}
destinationGeoRegionName={asArrayIfExists(get(DESTINATION_GEO_REGION_NAME_FIELD_NAME, data))}
destinationGeoCityName={asArrayIfExists(get(DESTINATION_GEO_CITY_NAME_FIELD_NAME, data))}
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, data))}
destinationPackets={asArrayIfExists(get(DESTINATION_PACKETS_FIELD_NAME, data))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, data))}
eventDuration={asArrayIfExists(get(EVENT_DURATION_FIELD_NAME, data))}
eventId={get(ID_FIELD_NAME, data)}
eventEnd={asArrayIfExists(get(EVENT_END_FIELD_NAME, data))}
eventStart={asArrayIfExists(get(EVENT_START_FIELD_NAME, data))}
networkBytes={asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, data))}
networkCommunityId={asArrayIfExists(get(NETWORK_COMMUNITY_ID_FIELD_NAME, data))}
networkDirection={asArrayIfExists(get(NETWORK_DIRECTION_FIELD_NAME, data))}
networkPackets={asArrayIfExists(get(NETWORK_PACKETS_FIELD_NAME, data))}
networkProtocol={asArrayIfExists(get(NETWORK_PROTOCOL_FIELD_NAME, data))}
sourceBytes={asArrayIfExists(get(SOURCE_BYTES_FIELD_NAME, data))}
sourceGeoContinentName={asArrayIfExists(get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, data))}
sourceGeoCountryName={asArrayIfExists(get(SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, data))}
sourceGeoCountryIsoCode={asArrayIfExists(get(SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, data))}
sourceGeoRegionName={asArrayIfExists(get(SOURCE_GEO_REGION_NAME_FIELD_NAME, data))}
sourceGeoCityName={asArrayIfExists(get(SOURCE_GEO_CITY_NAME_FIELD_NAME, data))}
sourceIp={asArrayIfExists(get(SOURCE_IP_FIELD_NAME, data))}
sourcePackets={asArrayIfExists(get(SOURCE_PACKETS_FIELD_NAME, data))}
sourcePort={asArrayIfExists(get(SOURCE_PORT_FIELD_NAME, data))}
tlsClientCertificateFingerprintSha1={asArrayIfExists(
get(TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, data)
)}
tlsFingerprintsJa3Hash={asArrayIfExists(get(JA3_HASH_FIELD_NAME, data))}
tlsServerCertificateFingerprintSha1={asArrayIfExists(
get(TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, data)
)}
transport={asArrayIfExists(get(NETWORK_TRANSPORT_FIELD_NAME, data))}
userName={undefined}
/>
));
export const NetflowRenderer = React.memo<NetflowRendererProps>(
({ data, timelineId, isDraggable }) => (
<Netflow
contextId={`netflow-renderer-${timelineId}-${data._id}`}
destinationBytes={asArrayIfExists(get(DESTINATION_BYTES_FIELD_NAME, data))}
destinationGeoContinentName={asArrayIfExists(
get(DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, data)
)}
destinationGeoCountryName={asArrayIfExists(
get(DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, data)
)}
destinationGeoCountryIsoCode={asArrayIfExists(
get(DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, data)
)}
destinationGeoRegionName={asArrayIfExists(get(DESTINATION_GEO_REGION_NAME_FIELD_NAME, data))}
destinationGeoCityName={asArrayIfExists(get(DESTINATION_GEO_CITY_NAME_FIELD_NAME, data))}
destinationIp={asArrayIfExists(get(DESTINATION_IP_FIELD_NAME, data))}
destinationPackets={asArrayIfExists(get(DESTINATION_PACKETS_FIELD_NAME, data))}
destinationPort={asArrayIfExists(get(DESTINATION_PORT_FIELD_NAME, data))}
eventDuration={asArrayIfExists(get(EVENT_DURATION_FIELD_NAME, data))}
eventId={get(ID_FIELD_NAME, data)}
eventEnd={asArrayIfExists(get(EVENT_END_FIELD_NAME, data))}
eventStart={asArrayIfExists(get(EVENT_START_FIELD_NAME, data))}
isDraggable={isDraggable}
networkBytes={asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, data))}
networkCommunityId={asArrayIfExists(get(NETWORK_COMMUNITY_ID_FIELD_NAME, data))}
networkDirection={asArrayIfExists(get(NETWORK_DIRECTION_FIELD_NAME, data))}
networkPackets={asArrayIfExists(get(NETWORK_PACKETS_FIELD_NAME, data))}
networkProtocol={asArrayIfExists(get(NETWORK_PROTOCOL_FIELD_NAME, data))}
sourceBytes={asArrayIfExists(get(SOURCE_BYTES_FIELD_NAME, data))}
sourceGeoContinentName={asArrayIfExists(get(SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, data))}
sourceGeoCountryName={asArrayIfExists(get(SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, data))}
sourceGeoCountryIsoCode={asArrayIfExists(get(SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, data))}
sourceGeoRegionName={asArrayIfExists(get(SOURCE_GEO_REGION_NAME_FIELD_NAME, data))}
sourceGeoCityName={asArrayIfExists(get(SOURCE_GEO_CITY_NAME_FIELD_NAME, data))}
sourceIp={asArrayIfExists(get(SOURCE_IP_FIELD_NAME, data))}
sourcePackets={asArrayIfExists(get(SOURCE_PACKETS_FIELD_NAME, data))}
sourcePort={asArrayIfExists(get(SOURCE_PORT_FIELD_NAME, data))}
tlsClientCertificateFingerprintSha1={asArrayIfExists(
get(TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, data)
)}
tlsFingerprintsJa3Hash={asArrayIfExists(get(JA3_HASH_FIELD_NAME, data))}
tlsServerCertificateFingerprintSha1={asArrayIfExists(
get(TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, data)
)}
transport={asArrayIfExists(get(NETWORK_TRANSPORT_FIELD_NAME, data))}
userName={undefined}
/>
)
);
NetflowRenderer.displayName = 'NetflowRenderer';

Some files were not shown because too many files have changed in this diff Show more