mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Investigations] [Tech Debt] removes redundant code from timelines plugin (#130928)
## [Security Solution] [Investigations] [Tech Debt] removes redundant code from the timelines plugin This follow-up PR removes redundant code from the `timelines` plugin, identified while implementing https://github.com/elastic/kibana/pull/130740
This commit is contained in:
parent
4e39685c01
commit
3ad6452166
42 changed files with 2 additions and 4036 deletions
|
@ -1,26 +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 { Ecs } from '../../../../ecs';
|
||||
import { CursorType, Maybe } from '../../../common';
|
||||
|
||||
export interface TimelineEdges {
|
||||
node: TimelineItem;
|
||||
cursor: CursorType;
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
_id: string;
|
||||
_index?: Maybe<string>;
|
||||
data: TimelineNonEcsData[];
|
||||
ecs: Ecs;
|
||||
}
|
||||
|
||||
export interface TimelineNonEcsData {
|
||||
field: string;
|
||||
value?: Maybe<string[] | string>;
|
||||
}
|
|
@ -6,13 +6,6 @@
|
|||
*/
|
||||
|
||||
import * as runtimeTypes from 'io-ts';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// This type is for typing EuiDescriptionList
|
||||
export interface DescriptionList {
|
||||
title: NonNullable<ReactNode>;
|
||||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
|
||||
runtimeTypes.union([type, runtimeTypes.null]);
|
||||
|
|
|
@ -1,54 +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, { MouseEvent } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { EventsTdContent } from '../t_grid/styles';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../t_grid/body/constants';
|
||||
|
||||
interface ActionIconItemProps {
|
||||
ariaLabel?: string;
|
||||
width?: number;
|
||||
dataTestSubj?: string;
|
||||
content?: string;
|
||||
iconType?: string;
|
||||
isDisabled?: boolean;
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
|
||||
width = DEFAULT_ACTION_BUTTON_WIDTH,
|
||||
dataTestSubj,
|
||||
content,
|
||||
ariaLabel,
|
||||
iconType = '',
|
||||
isDisabled = false,
|
||||
onClick,
|
||||
children,
|
||||
}) => (
|
||||
<div>
|
||||
<EventsTdContent textAlign="center" width={width}>
|
||||
{children ?? (
|
||||
<EuiToolTip data-test-subj={`${dataTestSubj}-tool-tip`} content={content}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj={`${dataTestSubj}-button`}
|
||||
iconType={iconType}
|
||||
isDisabled={isDisabled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
);
|
||||
|
||||
ActionIconItemComponent.displayName = 'ActionIconItemComponent';
|
||||
|
||||
export const ActionIconItem = React.memo(ActionIconItemComponent);
|
|
@ -1,48 +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 { rgba } from 'polished';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface WidthProp {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const Field = styled.div.attrs<WidthProp>(({ width }) => {
|
||||
if (width) {
|
||||
return {
|
||||
style: {
|
||||
width: `${width}px`,
|
||||
},
|
||||
};
|
||||
}
|
||||
})<WidthProp>`
|
||||
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
|
||||
border: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)},
|
||||
0 1px 5px -2px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)};
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
line-height: ${({ theme }) => theme.eui.euiLineHeight};
|
||||
padding: ${({ theme }) => theme.eui.paddingSizes.xs};
|
||||
`;
|
||||
Field.displayName = 'Field';
|
||||
|
||||
/**
|
||||
* Renders a field (e.g. `event.action`) as a draggable badge
|
||||
*/
|
||||
|
||||
export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>(
|
||||
({ fieldId, fieldWidth }) => (
|
||||
<Field data-test-subj="field" width={fieldWidth}>
|
||||
{fieldId}
|
||||
</Field>
|
||||
)
|
||||
);
|
||||
|
||||
DraggableFieldBadge.displayName = 'DraggableFieldBadge';
|
|
@ -1,34 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CATEGORY = i18n.translate('xpack.timelines.draggables.field.categoryLabel', {
|
||||
defaultMessage: 'Category',
|
||||
});
|
||||
|
||||
export const COPY_TO_CLIPBOARD = i18n.translate(
|
||||
'xpack.timelines.eventDetails.copyToClipboardTooltip',
|
||||
{
|
||||
defaultMessage: 'Copy to Clipboard',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIELD = i18n.translate('xpack.timelines.draggables.field.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
||||
export const TYPE = i18n.translate('xpack.timelines.draggables.field.typeLabel', {
|
||||
defaultMessage: 'Type',
|
||||
});
|
||||
|
||||
export const VIEW_CATEGORY = i18n.translate(
|
||||
'xpack.timelines.draggables.field.viewCategoryTooltip',
|
||||
{
|
||||
defaultMessage: 'View Category',
|
||||
}
|
||||
);
|
|
@ -1,8 +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.
|
||||
*/
|
||||
|
||||
export * from './field_badge';
|
|
@ -1,7 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptyValue it renders against snapshot 1`] = `
|
||||
<p>
|
||||
(Empty String)
|
||||
</p>
|
||||
`;
|
|
@ -5,162 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import {
|
||||
defaultToEmptyTag,
|
||||
getEmptyString,
|
||||
getEmptyStringTag,
|
||||
getEmptyTagValue,
|
||||
getEmptyValue,
|
||||
getOrEmptyTag,
|
||||
} from '.';
|
||||
import { getMockTheme } from '../../mock/kibana_react.mock';
|
||||
import { getEmptyValue } from '.';
|
||||
|
||||
describe('EmptyValue', () => {
|
||||
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
|
||||
|
||||
test('it renders against snapshot', () => {
|
||||
const wrapper = shallow(<p>{getEmptyString()}</p>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('#getEmptyValue', () => {
|
||||
test('should return an empty value', () => expect(getEmptyValue()).toBe('—'));
|
||||
});
|
||||
|
||||
describe('#getEmptyString', () => {
|
||||
test('should turn into an empty string place holder', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getEmptyString()}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe('(Empty String)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEmptyTagValue', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getEmptyTagValue()}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
test('should return an empty tag value', () => expect(wrapper.text()).toBe('—'));
|
||||
});
|
||||
|
||||
describe('#getEmptyStringTag', () => {
|
||||
test('should turn into an span that has length of 1', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getEmptyStringTag()}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.find('span')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should turn into an empty string tag place holder', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getEmptyStringTag()}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultToEmptyTag', () => {
|
||||
test('should default to an empty value when a value is null', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{defaultToEmptyTag(null)}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyValue());
|
||||
});
|
||||
|
||||
test('should default to an empty value when a value is undefined', () => {
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{defaultToEmptyTag(undefined)}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyValue());
|
||||
});
|
||||
|
||||
test('should return a deep path value', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<p>{defaultToEmptyTag(test.a.b.c)}</p>);
|
||||
expect(wrapper.text()).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getOrEmptyTag', () => {
|
||||
test('should default empty value when a deep rooted value is null', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getOrEmptyTag('a.b.c', test)}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyValue());
|
||||
});
|
||||
|
||||
test('should default empty value when a deep rooted value is undefined', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getOrEmptyTag('a.b.c', test)}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyValue());
|
||||
});
|
||||
|
||||
test('should default empty value when a deep rooted value is missing', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<p>{getOrEmptyTag('a.b.c', test)}</p>
|
||||
</ThemeProvider>
|
||||
);
|
||||
expect(wrapper.text()).toBe(getEmptyValue());
|
||||
});
|
||||
|
||||
test('should return a deep path value', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<p>{getOrEmptyTag('a.b.c', test)}</p>);
|
||||
expect(wrapper.text()).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,45 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get, isString } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const EmptyWrapper = styled.span`
|
||||
color: ${(props) => props.theme.eui.euiColorMediumShade};
|
||||
`;
|
||||
|
||||
EmptyWrapper.displayName = 'EmptyWrapper';
|
||||
|
||||
export const getEmptyValue = () => '—';
|
||||
export const getEmptyString = () => `(${i18n.EMPTY_STRING})`;
|
||||
|
||||
export const getEmptyTagValue = () => <EmptyWrapper>{getEmptyValue()}</EmptyWrapper>;
|
||||
export const getEmptyStringTag = () => <EmptyWrapper>{getEmptyString()}</EmptyWrapper>;
|
||||
|
||||
export const defaultToEmptyTag = <T extends unknown>(item: T): JSX.Element => {
|
||||
if (item == null) {
|
||||
return getEmptyTagValue();
|
||||
} else if (isString(item) && item === '') {
|
||||
return getEmptyStringTag();
|
||||
} else {
|
||||
return <>{item}</>;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrEmptyTag = (path: string, item: unknown): JSX.Element => {
|
||||
const text = get(path, item);
|
||||
return getOrEmptyTagFromValue(text);
|
||||
};
|
||||
|
||||
export const getOrEmptyTagFromValue = (value: string | number | null | undefined): JSX.Element => {
|
||||
if (value == null) {
|
||||
return getEmptyTagValue();
|
||||
} else if (value === '') {
|
||||
return getEmptyStringTag();
|
||||
} else {
|
||||
return <>{value}</>;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,12 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EMPTY_STRING = i18n.translate('xpack.timelines.emptyString.emptyStringDescription', {
|
||||
defaultMessage: 'Empty String',
|
||||
});
|
|
@ -1,60 +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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../mock/test_providers';
|
||||
import * as i18n from './translations';
|
||||
import { ExitFullScreen, EXIT_FULL_SCREEN_CLASS_NAME } from '.';
|
||||
|
||||
describe('ExitFullScreen', () => {
|
||||
test('it returns null when fullScreen is false', () => {
|
||||
const exitFullScreen = mount(
|
||||
<TestProviders>
|
||||
<ExitFullScreen fullScreen={false} setFullScreen={jest.fn()} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders a button with the exported EXIT_FULL_SCREEN_CLASS_NAME class when fullScreen is true', () => {
|
||||
const exitFullScreen = mount(
|
||||
<TestProviders>
|
||||
<ExitFullScreen fullScreen={true} setFullScreen={jest.fn()} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(exitFullScreen.find(`button.${EXIT_FULL_SCREEN_CLASS_NAME}`).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders the expected button text when fullScreen is true', () => {
|
||||
const exitFullScreen = mount(
|
||||
<TestProviders>
|
||||
<ExitFullScreen fullScreen={true} setFullScreen={jest.fn()} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().text()).toBe(
|
||||
i18n.EXIT_FULL_SCREEN
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes setFullScreen with a value of false when the button is clicked', () => {
|
||||
const setFullScreen = jest.fn();
|
||||
|
||||
const exitFullScreen = mount(
|
||||
<TestProviders>
|
||||
<ExitFullScreen fullScreen={true} setFullScreen={setFullScreen} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().simulate('click');
|
||||
expect(setFullScreen).toBeCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -1,64 +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 { EuiButton, EuiWindowEvent } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const EXIT_FULL_SCREEN_CLASS_NAME = 'exit-full-screen';
|
||||
|
||||
const StyledEuiButton = styled(EuiButton)`
|
||||
margin: ${({ theme }) => theme.eui.paddingSizes.s};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
fullScreen: boolean;
|
||||
setFullScreen: (fullScreen: boolean) => void;
|
||||
}
|
||||
|
||||
const ExitFullScreenComponent: React.FC<Props> = ({ fullScreen, setFullScreen }) => {
|
||||
const exitFullScreen = useCallback(() => {
|
||||
setFullScreen(false);
|
||||
}, [setFullScreen]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
|
||||
exitFullScreen();
|
||||
}
|
||||
},
|
||||
[exitFullScreen]
|
||||
);
|
||||
|
||||
if (!fullScreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiWindowEvent event="keydown" handler={onKeyDown} />
|
||||
<StyledEuiButton
|
||||
className={EXIT_FULL_SCREEN_CLASS_NAME}
|
||||
data-test-subj="exit-full-screen"
|
||||
fullWidth={false}
|
||||
iconType="fullScreen"
|
||||
isDisabled={!fullScreen}
|
||||
onClick={exitFullScreen}
|
||||
>
|
||||
{i18n.EXIT_FULL_SCREEN}
|
||||
</StyledEuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExitFullScreenComponent.displayName = 'ExitFullScreenComponent';
|
||||
|
||||
export const ExitFullScreen = React.memo(ExitFullScreenComponent);
|
|
@ -1,12 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EXIT_FULL_SCREEN = i18n.translate('xpack.timelines.exitFullScreenButton', {
|
||||
defaultMessage: 'Exit full screen',
|
||||
});
|
|
@ -56,7 +56,6 @@ export const TGrid = (props: TGridComponent) => {
|
|||
export { TGrid as default };
|
||||
|
||||
export * from './drag_and_drop';
|
||||
export * from './draggables';
|
||||
export * from './last_updated';
|
||||
export * from './loading';
|
||||
export * from './fields_browser';
|
||||
|
|
|
@ -1,69 +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 } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import { EventsHeadingExtra, EventsLoading } from '../../../styles';
|
||||
import type { OnColumnRemoved } from '../../../types';
|
||||
import type { Sort } from '../../sort';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
header: ColumnHeaderOptions;
|
||||
isLoading: boolean;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
/** Given a `header`, returns the `SortDirection` applicable to it */
|
||||
|
||||
export const CloseButton = React.memo<{
|
||||
columnId: string;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
}>(({ columnId, onColumnRemoved }) => {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// To avoid a re-sorting when you delete a column
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onColumnRemoved(columnId);
|
||||
},
|
||||
[columnId, onColumnRemoved]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.REMOVE_COLUMN}
|
||||
color="text"
|
||||
data-test-subj="remove-column"
|
||||
iconType="cross"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CloseButton.displayName = 'CloseButton';
|
||||
|
||||
export const Actions = React.memo<Props>(({ header, onColumnRemoved, sort, isLoading }) => {
|
||||
return (
|
||||
<>
|
||||
{sort.some((i) => i.columnId === header.id) && isLoading ? (
|
||||
<EventsHeadingExtra className="siemEventsHeading__extra--loading">
|
||||
<EventsLoading data-test-subj="timeline-loading-spinner" />
|
||||
</EventsHeadingExtra>
|
||||
) : (
|
||||
<EventsHeadingExtra className="siemEventsHeading__extra--close">
|
||||
<CloseButton columnId={header.id} onColumnRemoved={onColumnRemoved} />
|
||||
</EventsHeadingExtra>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Actions.displayName = 'Actions';
|
|
@ -1,25 +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 { FC, memo, useEffect } from 'react';
|
||||
|
||||
interface DraggingContainerProps {
|
||||
children: JSX.Element;
|
||||
onDragging: Function;
|
||||
}
|
||||
|
||||
const DraggingContainerComponent: FC<DraggingContainerProps> = ({ children, onDragging }) => {
|
||||
useEffect(() => {
|
||||
onDragging(true);
|
||||
|
||||
return () => onDragging(false);
|
||||
});
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export const DraggingContainer = memo(DraggingContainerComponent);
|
|
@ -1,19 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FullHeightFlexGroup = styled(EuiFlexGroup)`
|
||||
height: 100%;
|
||||
`;
|
||||
FullHeightFlexGroup.displayName = 'FullHeightFlexGroup';
|
||||
|
||||
export const FullHeightFlexItem = styled(EuiFlexItem)`
|
||||
height: 100%;
|
||||
`;
|
||||
FullHeightFlexItem.displayName = 'FullHeightFlexItem';
|
|
@ -1,51 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Memo(HeaderContentComponent)
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "number",
|
||||
}
|
||||
}
|
||||
isLoading={false}
|
||||
isResizing={false}
|
||||
onClick={[Function]}
|
||||
showSortingCapability={true}
|
||||
sort={
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"columnType": "number",
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Actions
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "number",
|
||||
}
|
||||
}
|
||||
isLoading={false}
|
||||
onColumnRemoved={[Function]}
|
||||
sort={
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"columnType": "number",
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Memo(HeaderContentComponent)>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import { TruncatableText } from '../../../../truncatable_text';
|
||||
|
||||
import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles';
|
||||
import { Sort } from '../../sort';
|
||||
import { SortIndicator } from '../../sort/sort_indicator';
|
||||
import { HeaderToolTipContent } from '../header_tooltip_content';
|
||||
import { getSortDirection, getSortIndex } from './helpers';
|
||||
interface HeaderContentProps {
|
||||
children: React.ReactNode;
|
||||
header: ColumnHeaderOptions;
|
||||
isLoading: boolean;
|
||||
isResizing: boolean;
|
||||
onClick: () => void;
|
||||
showSortingCapability: boolean;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
const HeaderContentComponent: React.FC<HeaderContentProps> = ({
|
||||
children,
|
||||
header,
|
||||
isLoading,
|
||||
isResizing,
|
||||
onClick,
|
||||
showSortingCapability,
|
||||
sort,
|
||||
}) => (
|
||||
<EventsHeading data-test-subj={`header-${header.id}`} isLoading={isLoading}>
|
||||
{header.aggregatable && showSortingCapability ? (
|
||||
<EventsHeadingTitleButton
|
||||
data-test-subj="header-sort-button"
|
||||
onClick={!isResizing && !isLoading ? onClick : noop}
|
||||
>
|
||||
<TruncatableText data-test-subj={`header-text-${header.id}`}>
|
||||
<EuiToolTip
|
||||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>
|
||||
{React.isValidElement(header.display)
|
||||
? header.display
|
||||
: header.displayAsText ?? header.id}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
|
||||
<SortIndicator
|
||||
data-test-subj="header-sort-indicator"
|
||||
sortDirection={getSortDirection({ header, sort })}
|
||||
sortNumber={getSortIndex({ header, sort })}
|
||||
/>
|
||||
</EventsHeadingTitleButton>
|
||||
) : (
|
||||
<EventsHeadingTitleSpan>
|
||||
<TruncatableText data-test-subj={`header-text-${header.id}`}>
|
||||
<EuiToolTip
|
||||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>
|
||||
{React.isValidElement(header.display)
|
||||
? header.display
|
||||
: header.displayAsText ?? header.id}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
</EventsHeadingTitleSpan>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</EventsHeading>
|
||||
);
|
||||
|
||||
export const HeaderContent = React.memo(HeaderContentComponent);
|
|
@ -1,54 +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 { Direction } from '../../../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { assertUnreachable } from '../../../../../../common/utility_types';
|
||||
import { Sort, SortDirection } from '../../sort';
|
||||
|
||||
interface GetNewSortDirectionOnClickParams {
|
||||
clickedHeader: ColumnHeaderOptions;
|
||||
currentSort: Sort[];
|
||||
}
|
||||
|
||||
/** Given a `header`, returns the `SortDirection` applicable to it */
|
||||
export const getNewSortDirectionOnClick = ({
|
||||
clickedHeader,
|
||||
currentSort,
|
||||
}: GetNewSortDirectionOnClickParams): Direction =>
|
||||
currentSort.reduce<Direction>(
|
||||
(acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc),
|
||||
Direction.desc
|
||||
);
|
||||
|
||||
/** Given a current sort direction, it returns the next sort direction */
|
||||
export const getNextSortDirection = (currentSort: Sort): Direction => {
|
||||
switch (currentSort.sortDirection) {
|
||||
case Direction.desc:
|
||||
return Direction.asc;
|
||||
case Direction.asc:
|
||||
return Direction.desc;
|
||||
case 'none':
|
||||
return Direction.desc;
|
||||
default:
|
||||
return assertUnreachable(currentSort.sortDirection as never, 'Unhandled sort direction');
|
||||
}
|
||||
};
|
||||
|
||||
interface GetSortDirectionParams {
|
||||
header: ColumnHeaderOptions;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection =>
|
||||
sort.reduce<SortDirection>(
|
||||
(acc, item) => (header.id === item.columnId ? item.sortDirection : acc),
|
||||
'none'
|
||||
);
|
||||
|
||||
export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number =>
|
||||
sort.findIndex((s) => s.columnId === header.id);
|
|
@ -1,331 +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 { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { Sort } from '../../sort';
|
||||
import { CloseButton } from '../actions';
|
||||
import { defaultHeaders } from '../default_headers';
|
||||
|
||||
import { HeaderComponent } from '.';
|
||||
import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers';
|
||||
import { Direction } from '../../../../../../common/search_strategy';
|
||||
import { TestProviders } from '../../../../../mock';
|
||||
import { tGridActions } from '../../../../../store/t_grid';
|
||||
import { mockGlobalState } from '../../../../../mock/global_state';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('../../../../../hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: () => mockGlobalState.timelineById.test,
|
||||
useDeepEqualSelector: () => mockGlobalState.timelineById.test,
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useSelector: jest.fn(),
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Header', () => {
|
||||
const columnHeader = defaultHeaders[0];
|
||||
const sort: Sort[] = [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
const timelineId = 'test';
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the header text', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(columnHeader.id);
|
||||
});
|
||||
|
||||
test('it renders the header text alias when displayAsText is provided', () => {
|
||||
const displayAsText = 'Timestamp';
|
||||
const headerWithLabel = { ...columnHeader, displayAsText };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(displayAsText);
|
||||
});
|
||||
|
||||
test('it renders the header as a `ReactNode` when `display` is provided', () => {
|
||||
const display: React.ReactNode = (
|
||||
<div data-test-subj="rendered-via-display">
|
||||
{'The display property renders the column heading as a ReactNode'}
|
||||
</div>
|
||||
);
|
||||
const headerWithLabel = { ...columnHeader, display };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="rendered-via-display"]`).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it prefers to render `display` instead of `displayAsText` when both are provided', () => {
|
||||
const displayAsText = 'this text should NOT be rendered';
|
||||
const display: React.ReactNode = (
|
||||
<div data-test-subj="rendered-via-display">{'this text is rendered via display'}</div>
|
||||
);
|
||||
const headerWithLabel = { ...columnHeader, display, displayAsText };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe('this text is rendered via display');
|
||||
});
|
||||
|
||||
test('it falls back to rendering header.id when `display` is not a valid React node', () => {
|
||||
const display = {}; // a plain object is NOT a `ReactNode`
|
||||
const headerWithLabel = { ...columnHeader, display };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(columnHeader.id);
|
||||
});
|
||||
|
||||
test('it renders a sort indicator', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-indicator"]').first().exists()).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onColumnSorted', () => {
|
||||
test('it invokes the onColumnSorted callback when the header sort button is clicked', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click');
|
||||
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
tGridActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.asc, // (because the previous state was Direction.desc)
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render the header sort button when aggregatable is false', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: false };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('it does NOT render the header sort button when aggregatable is missing', () => {
|
||||
const headerSortable = { ...columnHeader };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => {
|
||||
const mockOnColumnSorted = jest.fn();
|
||||
const headerSortable = { ...columnHeader, aggregatable: undefined };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click');
|
||||
|
||||
expect(mockOnColumnSorted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CloseButton', () => {
|
||||
test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => {
|
||||
const mockOnColumnRemoved = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<CloseButton columnId={columnHeader.id} onColumnRemoved={mockOnColumnRemoved} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="remove-column"]').first().simulate('click');
|
||||
|
||||
expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortDirection', () => {
|
||||
test('it returns the sort direction when the header id matches the sort column id', () => {
|
||||
expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection);
|
||||
});
|
||||
|
||||
test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => {
|
||||
const nonMatching: Sort[] = [
|
||||
{
|
||||
columnId: 'differentSocks',
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextSortDirection', () => {
|
||||
test('it returns "asc" when the current direction is "desc"', () => {
|
||||
const sortDescending: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.desc,
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortDescending)).toEqual('asc');
|
||||
});
|
||||
|
||||
test('it returns "desc" when the current direction is "asc"', () => {
|
||||
const sortAscending: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.asc,
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc);
|
||||
});
|
||||
|
||||
test('it returns "desc" by default', () => {
|
||||
const sortNone: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: 'none',
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortNone)).toEqual(Direction.desc);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewSortDirectionOnClick', () => {
|
||||
test('it returns the expected new sort direction when the header id matches the sort column id', () => {
|
||||
const sortMatches: Sort[] = [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
getNewSortDirectionOnClick({
|
||||
clickedHeader: columnHeader,
|
||||
currentSort: sortMatches,
|
||||
})
|
||||
).toEqual(Direction.asc);
|
||||
});
|
||||
|
||||
test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => {
|
||||
const sortDoesNotMatch: Sort[] = [
|
||||
{
|
||||
columnId: 'someOtherColumn',
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
sortDirection: 'none',
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
getNewSortDirectionOnClick({
|
||||
clickedHeader: columnHeader,
|
||||
currentSort: sortDoesNotMatch,
|
||||
})
|
||||
).toEqual(Direction.desc);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text truncation styling', () => {
|
||||
test('truncates the header text with an ellipsis', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).at(1)
|
||||
).toHaveStyleRule('text-overflow', 'ellipsis');
|
||||
});
|
||||
});
|
||||
|
||||
describe('header tooltip', () => {
|
||||
test('it has a tooltip to display the properties of the field', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,95 +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, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isDataViewFieldSubtypeNested } from '@kbn/es-query';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import type { Sort } from '../../sort';
|
||||
import { Actions } from '../actions';
|
||||
import { getNewSortDirectionOnClick } from './helpers';
|
||||
import { HeaderContent } from './header_content';
|
||||
import { tGridActions, tGridSelectors } from '../../../../../store/t_grid';
|
||||
import { useDeepEqualSelector } from '../../../../../hooks/use_selector';
|
||||
interface Props {
|
||||
header: ColumnHeaderOptions;
|
||||
sort: Sort[];
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const HeaderComponent: React.FC<Props> = ({ header, sort, timelineId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onColumnSort = useCallback(() => {
|
||||
const columnId = header.id;
|
||||
const columnType = header.type ?? 'text';
|
||||
const sortDirection = getNewSortDirectionOnClick({
|
||||
clickedHeader: header,
|
||||
currentSort: sort,
|
||||
});
|
||||
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
|
||||
let newSort = [];
|
||||
if (headerIndex === -1) {
|
||||
newSort = [
|
||||
...sort,
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
sortDirection,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
newSort = [
|
||||
...sort.slice(0, headerIndex),
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
sortDirection,
|
||||
},
|
||||
...sort.slice(headerIndex + 1),
|
||||
];
|
||||
}
|
||||
dispatch(
|
||||
tGridActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: newSort,
|
||||
})
|
||||
);
|
||||
}, [dispatch, header, sort, timelineId]);
|
||||
|
||||
const onColumnRemoved = useCallback(
|
||||
(columnId) => dispatch(tGridActions.removeColumn({ id: timelineId, columnId })),
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId ?? ''));
|
||||
const showSortingCapability = !isDataViewFieldSubtypeNested(header);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderContent
|
||||
header={header}
|
||||
isLoading={isLoading}
|
||||
isResizing={false}
|
||||
onClick={onColumnSort}
|
||||
showSortingCapability={showSortingCapability}
|
||||
sort={sort}
|
||||
>
|
||||
<Actions
|
||||
header={header}
|
||||
isLoading={isLoading}
|
||||
onColumnRemoved={onColumnRemoved}
|
||||
sort={sort}
|
||||
/>
|
||||
</HeaderContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = React.memo(HeaderComponent);
|
|
@ -1,66 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HeaderToolTipContent it renders the expected table content 1`] = `
|
||||
<Fragment>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="category"
|
||||
>
|
||||
Category
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="category-value"
|
||||
>
|
||||
base
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="field"
|
||||
>
|
||||
Field
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="field-value"
|
||||
>
|
||||
@timestamp
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="type"
|
||||
>
|
||||
Type
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue>
|
||||
<IconType
|
||||
data-test-subj="type-icon"
|
||||
type="clock"
|
||||
/>
|
||||
<span
|
||||
data-test-subj="type-value"
|
||||
>
|
||||
date
|
||||
</span>
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="description"
|
||||
>
|
||||
Description
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="description-value"
|
||||
>
|
||||
Date/time when the event originated.
|
||||
For log events this is the date/time when the event was generated, and not when it was read.
|
||||
Required field for all events.
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,72 +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 { mount, shallow } from 'enzyme';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import { HeaderToolTipContent } from '.';
|
||||
import { defaultHeaders } from '../../../../../mock/header';
|
||||
|
||||
describe('HeaderToolTipContent', () => {
|
||||
let header: ColumnHeaderOptions;
|
||||
beforeEach(() => {
|
||||
header = cloneDeep(defaultHeaders[0]);
|
||||
});
|
||||
|
||||
test('it renders the category', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual(
|
||||
header.category
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the name of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="field-value"]').first().text()).toEqual(header.id);
|
||||
});
|
||||
|
||||
test('it renders the expected icon for the header type', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock');
|
||||
});
|
||||
|
||||
test('it renders the type of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="type-value"]').first().text()).toEqual(header.type);
|
||||
});
|
||||
|
||||
test('it renders the description of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="description-value"]').first().text()).toEqual(
|
||||
header.description
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render the description column when the field does NOT contain a description', () => {
|
||||
const noDescription = {
|
||||
...header,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const wrapper = mount(<HeaderToolTipContent header={noDescription} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('it renders the expected table content', () => {
|
||||
const wrapper = shallow(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,81 +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 { EuiIcon } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import { getIconFromType } from '../../../../utils/helpers';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const IconType = styled(EuiIcon)`
|
||||
margin-right: 3px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
`;
|
||||
IconType.displayName = 'IconType';
|
||||
|
||||
const P = styled.span`
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
P.displayName = 'P';
|
||||
|
||||
const ToolTipTableMetadata = styled.span`
|
||||
margin-right: 5px;
|
||||
display: block;
|
||||
`;
|
||||
ToolTipTableMetadata.displayName = 'ToolTipTableMetadata';
|
||||
|
||||
const ToolTipTableValue = styled.span`
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
ToolTipTableValue.displayName = 'ToolTipTableValue';
|
||||
|
||||
export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => (
|
||||
<>
|
||||
{!isEmpty(header.category) && (
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="category">
|
||||
{i18n.CATEGORY}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="category-value">{header.category}</ToolTipTableValue>
|
||||
</P>
|
||||
)}
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="field">
|
||||
{i18n.FIELD}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="field-value">{header.id}</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="type">
|
||||
{i18n.TYPE}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue>
|
||||
<IconType data-test-subj="type-icon" type={getIconFromType(header.type)} />
|
||||
<span data-test-subj="type-value">{header.type}</span>
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
{!isEmpty(header.description) && (
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="description">
|
||||
{i18n.DESCRIPTION}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="description-value">
|
||||
{header.description}
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
HeaderToolTipContent.displayName = 'HeaderToolTipContent';
|
|
@ -24,14 +24,8 @@ import { euiThemeVars } from '@kbn/ui-theme';
|
|||
export const DEFAULT_ACTION_BUTTON_WIDTH =
|
||||
parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px
|
||||
|
||||
/** Additional column width to include when checkboxes are shown **/
|
||||
export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px;
|
||||
|
||||
/** The default minimum width of a column (when a width for the column type is not specified) */
|
||||
export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px
|
||||
|
||||
/** The minimum width of a resized column */
|
||||
export const RESIZED_COLUMN_MIN_WITH = 70; // px
|
||||
|
||||
/** The default minimum width of a column of type `date` */
|
||||
export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px
|
||||
|
|
|
@ -1,967 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Columns it renders the expected columns 1`] = `
|
||||
<styled.div
|
||||
data-test-subj="data-driven-columns"
|
||||
>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={0}
|
||||
key="message"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={1}
|
||||
key="event.category"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={2}
|
||||
key="event.action"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={3}
|
||||
key="host.name"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={4}
|
||||
key="source.ip"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={5}
|
||||
key="destination.ip"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
<TgridTdCell
|
||||
_id="1"
|
||||
ariaRowindex={2}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"field": "@timestamp",
|
||||
"value": Array [
|
||||
"2018-11-05T19:03:25.937Z",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.severity",
|
||||
"value": Array [
|
||||
"3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.category",
|
||||
"value": Array [
|
||||
"Access",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.action",
|
||||
"value": Array [
|
||||
"Action",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "host.name",
|
||||
"value": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "source.ip",
|
||||
"value": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.ip",
|
||||
"value": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "destination.bytes",
|
||||
"value": Array [
|
||||
"123456",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "user.name",
|
||||
"value": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
ecsData={
|
||||
Object {
|
||||
"_id": "1",
|
||||
"destination": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.3",
|
||||
],
|
||||
"port": Array [
|
||||
6343,
|
||||
],
|
||||
},
|
||||
"event": Object {
|
||||
"action": Array [
|
||||
"Action",
|
||||
],
|
||||
"category": Array [
|
||||
"Access",
|
||||
],
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"module": Array [
|
||||
"nginx",
|
||||
],
|
||||
"severity": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
"geo": Object {
|
||||
"country_iso_code": Array [
|
||||
"xx",
|
||||
],
|
||||
"region_name": Array [
|
||||
"xx",
|
||||
],
|
||||
},
|
||||
"host": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"name": Array [
|
||||
"apache",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"ip": Array [
|
||||
"192.168.0.1",
|
||||
],
|
||||
"port": Array [
|
||||
80,
|
||||
],
|
||||
},
|
||||
"timestamp": "2018-11-05T19:03:25.937Z",
|
||||
"user": Object {
|
||||
"id": Array [
|
||||
"1",
|
||||
],
|
||||
"name": Array [
|
||||
"john.dee",
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
hasRowRenderers={false}
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name",
|
||||
"initialWidth": 180,
|
||||
}
|
||||
}
|
||||
index={6}
|
||||
key="user.name"
|
||||
renderCellValue={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
</styled.div>
|
||||
`;
|
|
@ -1,59 +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 { shallow } from 'enzyme';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { defaultHeaders } from '../column_headers/default_headers';
|
||||
|
||||
import { DataDrivenColumns } from '.';
|
||||
import { mockTimelineData } from '../../../../mock/mock_timeline_data';
|
||||
import { TestCellRenderer } from '../../../../mock/cell_renderer';
|
||||
|
||||
window.matchMedia = jest.fn().mockImplementation((query) => {
|
||||
return {
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Columns', () => {
|
||||
const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp');
|
||||
|
||||
test('it renders the expected columns', () => {
|
||||
const wrapper = shallow(
|
||||
<DataDrivenColumns
|
||||
ariaRowindex={2}
|
||||
id={mockTimelineData[0]._id}
|
||||
actionsColumnWidth={50}
|
||||
checked={false}
|
||||
columnHeaders={headersSansTimestamp}
|
||||
data={mockTimelineData[0].data}
|
||||
ecsData={mockTimelineData[0].ecs}
|
||||
hasRowRenderers={false}
|
||||
renderCellValue={TestCellRenderer}
|
||||
timelineId="test"
|
||||
columnValues={'abc def'}
|
||||
showCheckboxes={false}
|
||||
selectedEventIds={{}}
|
||||
loadingEventIds={[]}
|
||||
onEventDetailsPanelOpened={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
leadingControlColumns={[]}
|
||||
trailingControlColumns={[]}
|
||||
setEventsLoading={jest.fn()}
|
||||
setEventsDeleted={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -5,396 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { getOr } from 'lodash/fp';
|
||||
|
||||
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
import { OnRowSelected } from '../../types';
|
||||
|
||||
import {
|
||||
EventsTd,
|
||||
EVENTS_TD_CLASS_NAME,
|
||||
EventsTdContent,
|
||||
EventsTdGroupData,
|
||||
EventsTdGroupActions,
|
||||
} from '../../styles';
|
||||
|
||||
import { StatefulCell } from './stateful_cell';
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
TimelineTabs,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import type {
|
||||
ActionProps,
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
RowCellRender,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||
import type { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
interface CellProps {
|
||||
_id: string;
|
||||
ariaRowindex: number;
|
||||
index: number;
|
||||
header: ColumnHeaderOptions;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
hasRowRenderers: boolean;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
interface DataDrivenColumnProps {
|
||||
id: string;
|
||||
actionsColumnWidth: number;
|
||||
ariaRowindex: number;
|
||||
checked: boolean;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
columnValues: string;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
onRowSelected: OnRowSelected;
|
||||
onRuleChange?: () => void;
|
||||
hasRowRenderers: boolean;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
}
|
||||
|
||||
const SPACE = ' ';
|
||||
|
||||
export const shouldForwardKeyDownEvent = (key: string): boolean => {
|
||||
switch (key) {
|
||||
case SPACE: // fall through
|
||||
case 'Enter':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
|
||||
const { altKey, ctrlKey, key, metaKey, shiftKey, target, type } = keyboardEvent;
|
||||
|
||||
const targetElement = target as Element;
|
||||
|
||||
// we *only* forward the event to the (child) draggable keyboard wrapper
|
||||
// if the keyboard event originated from the container (TD) element
|
||||
if (shouldForwardKeyDownEvent(key) && targetElement.className?.includes(EVENTS_TD_CLASS_NAME)) {
|
||||
const draggableKeyboardWrapper = targetElement.querySelector<HTMLDivElement>(
|
||||
`.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}`
|
||||
);
|
||||
|
||||
const newEvent = new KeyboardEvent(type, {
|
||||
altKey,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey,
|
||||
key,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
});
|
||||
|
||||
if (key === ' ') {
|
||||
// prevent the default behavior of scrolling the table when space is pressed
|
||||
keyboardEvent.preventDefault();
|
||||
}
|
||||
|
||||
draggableKeyboardWrapper?.dispatchEvent(newEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const TgridActionTdCell = ({
|
||||
action: Action,
|
||||
width,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnId,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
eventIdToNoteIds,
|
||||
index,
|
||||
isEventPinned,
|
||||
isEventViewer,
|
||||
eventId,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
rowIndex,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
selectedEventIds = {},
|
||||
showCheckboxes,
|
||||
showNotes = false,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}: ActionProps & {
|
||||
columnId: string;
|
||||
hasRowRenderers: boolean;
|
||||
actionsColumnWidth: number;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
}) => {
|
||||
const displayWidth = width ? width : actionsColumnWidth;
|
||||
return (
|
||||
<EventsTdGroupActions
|
||||
width={displayWidth}
|
||||
data-test-subj="event-actions-container"
|
||||
tabIndex={0}
|
||||
>
|
||||
<EventsTd
|
||||
$ariaColumnIndex={index + ARIA_COLUMN_INDEX_OFFSET}
|
||||
key={tabType != null ? `${eventId}_${tabType}` : `${eventId}`}
|
||||
onKeyDown={onKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
width={width}
|
||||
>
|
||||
<EventsTdContent data-test-subj="cell-container">
|
||||
<>
|
||||
<EuiScreenReaderOnly data-test-subj="screenReaderOnly">
|
||||
<p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
{Action && (
|
||||
<Action
|
||||
ariaRowindex={ariaRowindex}
|
||||
width={width}
|
||||
checked={Object.keys(selectedEventIds).includes(eventId)}
|
||||
columnId={columnId}
|
||||
columnValues={columnValues}
|
||||
eventId={eventId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
index={index}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
onRowSelected={onRowSelected}
|
||||
rowIndex={rowIndex}
|
||||
onRuleChange={onRuleChange}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={showNotes}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</EventsTdContent>
|
||||
{hasRowRenderers ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
</EventsTd>
|
||||
</EventsTdGroupActions>
|
||||
);
|
||||
};
|
||||
|
||||
const TgridTdCell = ({
|
||||
_id,
|
||||
ariaRowindex,
|
||||
index,
|
||||
header,
|
||||
data,
|
||||
ecsData,
|
||||
hasRowRenderers,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
}: CellProps) => {
|
||||
const ariaColIndex = index + ARIA_COLUMN_INDEX_OFFSET;
|
||||
return (
|
||||
<EventsTd
|
||||
$ariaColumnIndex={ariaColIndex}
|
||||
key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`}
|
||||
onKeyDown={onKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
width={header.initialWidth}
|
||||
>
|
||||
<EventsTdContent data-test-subj="cell-container">
|
||||
<>
|
||||
<EuiScreenReaderOnly data-test-subj="screenReaderOnly">
|
||||
<p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: ariaColIndex })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
<StatefulCell
|
||||
rowIndex={ariaRowindex - 1}
|
||||
colIndex={ariaColIndex - 1}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={_id}
|
||||
linkValues={getOr([], header.linkField ?? '', ecsData)}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
</EventsTdContent>
|
||||
{hasRowRenderers ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
</EventsTd>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataDrivenColumns = React.memo<DataDrivenColumnProps>(
|
||||
({
|
||||
ariaRowindex,
|
||||
actionsColumnWidth,
|
||||
columnHeaders,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
isEventViewer,
|
||||
id: _id,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
selectedEventIds = {},
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
trailingControlColumns,
|
||||
leadingControlColumns,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}) => {
|
||||
const trailingActionCells = useMemo(
|
||||
() =>
|
||||
trailingControlColumns ? trailingControlColumns.map((column) => column.rowCellRender) : [],
|
||||
[trailingControlColumns]
|
||||
);
|
||||
const leadingAndDataColumnCount = useMemo(
|
||||
() => leadingControlColumns.length + columnHeaders.length,
|
||||
[leadingControlColumns, columnHeaders]
|
||||
);
|
||||
const TrailingActions = useMemo(
|
||||
() =>
|
||||
trailingActionCells.map((Action: RowCellRender | undefined, index) => {
|
||||
return (
|
||||
Action && (
|
||||
<TgridActionTdCell
|
||||
action={Action}
|
||||
width={trailingControlColumns[index].width}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
checked={Object.keys(selectedEventIds).includes(_id)}
|
||||
columnId={trailingControlColumns[index].id || ''}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
eventId={_id}
|
||||
data={data}
|
||||
key={index}
|
||||
index={leadingAndDataColumnCount + index}
|
||||
rowIndex={ariaRowindex}
|
||||
ecsData={ecsData}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
isEventViewer={isEventViewer}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}),
|
||||
[
|
||||
trailingControlColumns,
|
||||
_id,
|
||||
data,
|
||||
ecsData,
|
||||
onRowSelected,
|
||||
isEventViewer,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
hasRowRenderers,
|
||||
leadingAndDataColumnCount,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
trailingActionCells,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
]
|
||||
);
|
||||
const ColumnHeaders = useMemo(
|
||||
() =>
|
||||
columnHeaders.map((header, index) => (
|
||||
<TgridTdCell
|
||||
_id={_id}
|
||||
index={index}
|
||||
header={header}
|
||||
key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`}
|
||||
ariaRowindex={ariaRowindex}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
)),
|
||||
[
|
||||
_id,
|
||||
ariaRowindex,
|
||||
columnHeaders,
|
||||
data,
|
||||
ecsData,
|
||||
hasRowRenderers,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<EventsTdGroupData data-test-subj="data-driven-columns">
|
||||
{ColumnHeaders}
|
||||
{TrailingActions}
|
||||
</EventsTdGroupData>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DataDrivenColumns.displayName = 'DataDrivenColumns';
|
||||
|
||||
export const getMappedNonEcsValue = ({
|
||||
data,
|
||||
|
|
|
@ -1,180 +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 { mount } from 'enzyme';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { StatefulCell } from './stateful_cell';
|
||||
import { getMappedNonEcsValue } from '.';
|
||||
import { defaultHeaders } from '../../../../mock/header';
|
||||
import {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
TimelineTabs,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import { TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import { mockTimelineData } from '../../../../mock/mock_timeline_data';
|
||||
|
||||
/**
|
||||
* This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface,
|
||||
* as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid
|
||||
*
|
||||
* Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`.
|
||||
* The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface,
|
||||
* is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example:
|
||||
* https://codesandbox.io/s/zhxmo
|
||||
*/
|
||||
const RenderCellValue: React.FC<CellValueElementProps> = ({ columnId, data, setCellProps }) => {
|
||||
useEffect(() => {
|
||||
// branching logic that conditionally renders a specific cell green:
|
||||
if (columnId === defaultHeaders[0].id) {
|
||||
const value = getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
if (value?.length) {
|
||||
setCellProps({
|
||||
style: {
|
||||
backgroundColor: 'green',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [columnId, data, setCellProps]);
|
||||
|
||||
return (
|
||||
<div data-test-subj="renderCellValue">
|
||||
{getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: columnId,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe('StatefulCell', () => {
|
||||
const rowIndex = 123;
|
||||
const colIndex = 0;
|
||||
const eventId = '_id-123';
|
||||
const linkValues = ['foo', 'bar', '@baz'];
|
||||
const tabType = TimelineTabs.query;
|
||||
const timelineId = 'test';
|
||||
|
||||
let header: ColumnHeaderOptions;
|
||||
let data: TimelineNonEcsData[];
|
||||
beforeEach(() => {
|
||||
data = cloneDeep(mockTimelineData[0].data);
|
||||
header = cloneDeep(defaultHeaders[0]);
|
||||
});
|
||||
|
||||
test('it invokes renderCellValue with the expected arguments when tabType is specified', () => {
|
||||
const renderCellValue = jest.fn();
|
||||
|
||||
mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderCellValue).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
timelineId: `${timelineId}-${tabType}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => {
|
||||
const renderCellValue = jest.fn();
|
||||
|
||||
mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderCellValue).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
timelineId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the React.Node returned by renderCellValue', () => {
|
||||
const renderCellValue = () => <div data-test-subj="renderCellValue" />;
|
||||
|
||||
const wrapper = mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => {
|
||||
const wrapper = mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={RenderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style')
|
||||
).toEqual('background-color: green;');
|
||||
});
|
||||
});
|
|
@ -1,68 +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, { HTMLAttributes, useState } from 'react';
|
||||
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
} from '../../../../../common/types/timeline';
|
||||
|
||||
export interface CommonProps {
|
||||
className?: string;
|
||||
'aria-label'?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const StatefulCellComponent = ({
|
||||
rowIndex,
|
||||
colIndex,
|
||||
data,
|
||||
header,
|
||||
eventId,
|
||||
linkValues,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
data: TimelineNonEcsData[];
|
||||
header: ColumnHeaderOptions;
|
||||
eventId: string;
|
||||
linkValues: string[] | undefined;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
}) => {
|
||||
const [cellProps, setCellProps] = useState<CommonProps & HTMLAttributes<HTMLDivElement>>({});
|
||||
return (
|
||||
<div data-test-subj="statefulCell" {...cellProps}>
|
||||
{renderCellValue({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isDraggable: true,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
setCellProps,
|
||||
timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StatefulCellComponent.displayName = 'StatefulCellComponent';
|
||||
|
||||
export const StatefulCell = React.memo(StatefulCellComponent);
|
|
@ -1,28 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: number }) =>
|
||||
i18n.translate('xpack.timelines.timeline.youAreInATableCellScreenReaderOnly', {
|
||||
values: { column, row },
|
||||
defaultMessage: 'You are in a table cell. row: {row}, column: {column}',
|
||||
});
|
||||
|
||||
export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) =>
|
||||
i18n.translate('xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly', {
|
||||
values: { row },
|
||||
defaultMessage:
|
||||
'The event in row {row} has an event renderer. Press shift + down arrow to focus it.',
|
||||
});
|
||||
|
||||
export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) =>
|
||||
i18n.translate('xpack.timelines.timeline.eventHasNotesScreenReaderOnly', {
|
||||
values: { notesCount, row },
|
||||
defaultMessage:
|
||||
'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.',
|
||||
});
|
|
@ -1,118 +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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { getActionsColumnWidth } from '../column_headers/helpers';
|
||||
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { TestCellRenderer } from '../../../../mock/cell_renderer';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { TestProviders } from '../../../../mock/test_providers';
|
||||
import { testLeadingControlColumn } from '../../../../mock/mock_timeline_control_columns';
|
||||
import { mockGlobalState } from '../../../../mock/global_state';
|
||||
|
||||
jest.mock('../../../../hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: () => mockGlobalState.timelineById.test,
|
||||
useDeepEqualSelector: () => mockGlobalState.timelineById.test,
|
||||
}));
|
||||
|
||||
describe('EventColumnView', () => {
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const props = {
|
||||
ariaRowindex: 2,
|
||||
id: 'event-id',
|
||||
actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT),
|
||||
associateNote: jest.fn(),
|
||||
columnHeaders: [],
|
||||
columnRenderers: [],
|
||||
data: [
|
||||
{
|
||||
field: 'host.name',
|
||||
},
|
||||
],
|
||||
ecsData: {
|
||||
_id: 'id',
|
||||
},
|
||||
eventIdToNoteIds: {},
|
||||
expanded: false,
|
||||
hasRowRenderers: false,
|
||||
loading: false,
|
||||
loadingEventIds: [],
|
||||
notesCount: 0,
|
||||
onEventDetailsPanelOpened: jest.fn(),
|
||||
onPinEvent: jest.fn(),
|
||||
onRowSelected: jest.fn(),
|
||||
onUnPinEvent: jest.fn(),
|
||||
refetch: jest.fn(),
|
||||
renderCellValue: TestCellRenderer,
|
||||
selectedEventIds: {},
|
||||
showCheckboxes: false,
|
||||
showNotes: false,
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId: TimelineId.active,
|
||||
toggleShowNotes: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
isEventPinned: false,
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
setEventsLoading: jest.fn(),
|
||||
setEventsDeleted: jest.fn(),
|
||||
};
|
||||
|
||||
// TODO: next 3 tests will be re-enabled in the future.
|
||||
test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.detectionsPage} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsRulesDetailsPage', () => {
|
||||
const wrapper = mount(
|
||||
<EventColumnView {...props} timelineId={TimelineId.detectionsRulesDetailsPage} />,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test.skip('it render AddToCaseAction if timelineId === TimelineId.active', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.active} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test.skip('it does NOT render AddToCaseAction when timelineId is not in the allowed list', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} timelineId="timeline-test" />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders a custom control column in addition to the default control column', () => {
|
||||
const wrapper = mount(
|
||||
<EventColumnView
|
||||
{...props}
|
||||
timelineId="timeline-test"
|
||||
leadingControlColumns={[testLeadingControlColumn]}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,192 +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, { useMemo } from 'react';
|
||||
|
||||
import type { OnRowSelected } from '../../types';
|
||||
import { EventsTrData, EventsTdGroupActions } from '../../styles';
|
||||
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
RowCellRender,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import type { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
actionsColumnWidth: number;
|
||||
ariaRowindex: number;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
onRowSelected: OnRowSelected;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
hasRowRenderers: boolean;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
}
|
||||
|
||||
export const EventColumnView = React.memo<Props>(
|
||||
({
|
||||
id,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnHeaders,
|
||||
data,
|
||||
ecsData,
|
||||
isEventViewer = false,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
selectedEventIds = {},
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}) => {
|
||||
// Each action button shall announce itself to screen readers via an `aria-label`
|
||||
// in the following format:
|
||||
// "button description, for the event in row {ariaRowindex}, with columns {columnValues}",
|
||||
// so we combine the column values here:
|
||||
const columnValues = useMemo(
|
||||
() =>
|
||||
columnHeaders
|
||||
.map(
|
||||
(header) =>
|
||||
getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: header.id,
|
||||
}) ?? []
|
||||
)
|
||||
.join(' '),
|
||||
[columnHeaders, data]
|
||||
);
|
||||
const leadingActionCells = useMemo(
|
||||
() =>
|
||||
leadingControlColumns ? leadingControlColumns.map((column) => column.rowCellRender) : [],
|
||||
[leadingControlColumns]
|
||||
);
|
||||
const LeadingActions = useMemo(
|
||||
() =>
|
||||
leadingActionCells.map((Action: RowCellRender | undefined, index) => {
|
||||
const width = leadingControlColumns[index].width
|
||||
? leadingControlColumns[index].width
|
||||
: actionsColumnWidth;
|
||||
return (
|
||||
<EventsTdGroupActions
|
||||
width={width}
|
||||
data-test-subj="event-actions-container"
|
||||
tabIndex={0}
|
||||
key={index}
|
||||
>
|
||||
{Action && (
|
||||
<Action
|
||||
width={width}
|
||||
rowIndex={ariaRowindex}
|
||||
ariaRowindex={ariaRowindex}
|
||||
checked={Object.keys(selectedEventIds).includes(id)}
|
||||
columnId={leadingControlColumns[index].id || ''}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
eventId={id}
|
||||
data={data}
|
||||
index={index}
|
||||
ecsData={ecsData}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
isEventViewer={isEventViewer}
|
||||
onRuleChange={onRuleChange}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
)}
|
||||
</EventsTdGroupActions>
|
||||
);
|
||||
}),
|
||||
[
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
id,
|
||||
isEventViewer,
|
||||
leadingActionCells,
|
||||
leadingControlColumns,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<EventsTrData data-test-subj="event-column-view">
|
||||
{LeadingActions}
|
||||
<DataDrivenColumns
|
||||
id={id}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
columnHeaders={columnHeaders}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
checked={Object.keys(selectedEventIds).includes(id)}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
isEventViewer={isEventViewer}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
</EventsTrData>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EventColumnView.displayName = 'EventColumnView';
|
|
@ -1,99 +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 { isEmpty } from 'lodash';
|
||||
|
||||
import { EventsTbody } from '../../styles';
|
||||
import { StatefulEvent } from './stateful_event';
|
||||
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
OnRowSelected,
|
||||
RowRenderer,
|
||||
} from '../../../../../common/types/timeline';
|
||||
|
||||
import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
|
||||
/** This offset begins at two, because the header row counts as "row 1", and aria-rowindex starts at "1" */
|
||||
const ARIA_ROW_INDEX_OFFSET = 2;
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
data: TimelineItem[];
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
lastFocusedAriaColindex: number;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onRowSelected: OnRowSelected;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
}
|
||||
|
||||
const EventsComponent: React.FC<Props> = ({
|
||||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
containerRef,
|
||||
data,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
lastFocusedAriaColindex,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
}) => (
|
||||
<EventsTbody data-test-subj="events">
|
||||
{data.map((event, i) => (
|
||||
<StatefulEvent
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={i + ARIA_ROW_INDEX_OFFSET}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
containerRef={containerRef}
|
||||
event={event}
|
||||
isEventViewer={isEventViewer}
|
||||
key={`${id}_${tabType}_${event._id}_${event._index}_${
|
||||
!isEmpty(event.ecs.eql?.sequenceNumber) ? event.ecs.eql?.sequenceNumber : ''
|
||||
}`}
|
||||
lastFocusedAriaColindex={lastFocusedAriaColindex}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onRowSelected={onRowSelected}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
tabType={tabType}
|
||||
timelineId={id}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
/>
|
||||
))}
|
||||
</EventsTbody>
|
||||
);
|
||||
|
||||
export const Events = React.memo(EventsComponent);
|
|
@ -1,218 +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, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
|
||||
import { EventsTrGroup, EventsTrSupplement } from '../../styles';
|
||||
import type { OnRowSelected } from '../../types';
|
||||
import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { getRowRenderer } from '../renderers/get_row_renderer';
|
||||
import { StatefulRowRenderer } from './stateful_row_renderer';
|
||||
import { getMappedNonEcsValue } from '../data_driven_columns';
|
||||
import { StatefulEventContext } from './stateful_event_context';
|
||||
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
TimelineTabs,
|
||||
} from '../../../../../common/types/timeline';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
ControlColumnProps,
|
||||
RowRenderer,
|
||||
TimelineExpandedDetailType,
|
||||
} from '../../../../../common/types/timeline';
|
||||
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import { tGridActions, tGridSelectors } from '../../../../store/t_grid';
|
||||
import { useDeepEqualSelector } from '../../../../hooks/use_selector';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
event: TimelineItem;
|
||||
isEventViewer?: boolean;
|
||||
lastFocusedAriaColindex: number;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onRowSelected: OnRowSelected;
|
||||
ariaRowindex: number;
|
||||
onRuleChange?: () => void;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
}
|
||||
|
||||
const StatefulEventComponent: React.FC<Props> = ({
|
||||
actionsColumnWidth,
|
||||
browserFields,
|
||||
containerRef,
|
||||
columnHeaders,
|
||||
event,
|
||||
isEventViewer = false,
|
||||
lastFocusedAriaColindex,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
onRuleChange,
|
||||
ariaRowindex,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
}) => {
|
||||
const trGroupRef = useRef<HTMLDivElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
|
||||
const [activeStatefulEventContext] = useState({ timelineID: timelineId, tabType });
|
||||
const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []);
|
||||
const expandedDetail = useDeepEqualSelector(
|
||||
(state) => getTGrid(state, timelineId).expandedDetail ?? {}
|
||||
);
|
||||
const hostName = useMemo(() => {
|
||||
const hostNameArr = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' });
|
||||
return hostNameArr && hostNameArr.length > 0 ? hostNameArr[0] : null;
|
||||
}, [event?.data]);
|
||||
|
||||
const hostIPAddresses = useMemo(() => {
|
||||
const hostIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' }) ?? [];
|
||||
const sourceIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'source.ip' }) ?? [];
|
||||
const destinationIpList =
|
||||
getMappedNonEcsValue({
|
||||
data: event?.data,
|
||||
fieldName: 'destination.ip',
|
||||
}) ?? [];
|
||||
return new Set([...hostIpList, ...sourceIpList, ...destinationIpList]);
|
||||
}, [event?.data]);
|
||||
|
||||
const activeTab = tabType ?? TimelineTabs.query;
|
||||
const activeExpandedDetail = expandedDetail[activeTab];
|
||||
|
||||
const isDetailPanelExpanded: boolean =
|
||||
(activeExpandedDetail?.panelView === 'eventDetail' &&
|
||||
activeExpandedDetail?.params?.eventId === event._id) ||
|
||||
(activeExpandedDetail?.panelView === 'hostDetail' &&
|
||||
activeExpandedDetail?.params?.hostName === hostName) ||
|
||||
(activeExpandedDetail?.panelView === 'networkDetail' &&
|
||||
activeExpandedDetail?.params?.ip &&
|
||||
hostIPAddresses?.has(activeExpandedDetail?.params?.ip)) ||
|
||||
false;
|
||||
|
||||
const hasRowRenderers: boolean = useMemo(
|
||||
() => getRowRenderer(event.ecs, rowRenderers) != null,
|
||||
[event.ecs, rowRenderers]
|
||||
);
|
||||
|
||||
const handleOnEventDetailPanelOpened = useCallback(() => {
|
||||
const eventId = event._id;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const indexName = event._index!;
|
||||
|
||||
const updatedExpandedDetail: TimelineExpandedDetailType = {
|
||||
panelView: 'eventDetail',
|
||||
params: {
|
||||
eventId,
|
||||
indexName,
|
||||
},
|
||||
};
|
||||
|
||||
dispatch(
|
||||
tGridActions.toggleDetailPanel({
|
||||
...updatedExpandedDetail,
|
||||
tabType,
|
||||
timelineId,
|
||||
})
|
||||
);
|
||||
}, [dispatch, event._id, event._index, tabType, timelineId]);
|
||||
|
||||
const setEventsLoading = useCallback<SetEventsLoading>(
|
||||
({ eventIds, isLoading }) => {
|
||||
dispatch(tGridActions.setEventsLoading({ id: timelineId, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback<SetEventsDeleted>(
|
||||
({ eventIds, isDeleted }) => {
|
||||
dispatch(tGridActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const RowRendererContent = useMemo(
|
||||
() => (
|
||||
<EventsTrSupplement>
|
||||
<StatefulRowRenderer
|
||||
ariaRowindex={ariaRowindex}
|
||||
containerRef={containerRef}
|
||||
event={event}
|
||||
lastFocusedAriaColindex={lastFocusedAriaColindex}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EventsTrSupplement>
|
||||
),
|
||||
[ariaRowindex, containerRef, event, lastFocusedAriaColindex, rowRenderers, timelineId]
|
||||
);
|
||||
|
||||
return (
|
||||
<StatefulEventContext.Provider value={activeStatefulEventContext}>
|
||||
<EventsTrGroup
|
||||
$ariaRowindex={ariaRowindex}
|
||||
className={STATEFUL_EVENT_CSS_CLASS_NAME}
|
||||
data-test-subj="event"
|
||||
eventType={getEventType(event.ecs)}
|
||||
isBuildingBlockType={isEventBuildingBlockType(event.ecs)}
|
||||
isEvenEqlSequence={isEvenEqlSequence(event.ecs)}
|
||||
isExpanded={isDetailPanelExpanded}
|
||||
ref={trGroupRef}
|
||||
showLeftBorder={!isEventViewer}
|
||||
>
|
||||
<EventColumnView
|
||||
id={event._id}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
columnHeaders={columnHeaders}
|
||||
data={event.data}
|
||||
ecsData={event.ecs}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
isEventViewer={isEventViewer}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
|
||||
onRowSelected={onRowSelected}
|
||||
renderCellValue={renderCellValue}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
|
||||
<div>{RowRendererContent}</div>
|
||||
</EventsTrGroup>
|
||||
</StatefulEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatefulEvent = React.memo(StatefulEventComponent);
|
|
@ -8,13 +8,11 @@
|
|||
import { omit } from 'lodash/fp';
|
||||
|
||||
import { ColumnHeaderOptions } from '../../../../common/types';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import {
|
||||
allowSorting,
|
||||
hasCellActions,
|
||||
mapSortDirectionToDirection,
|
||||
mapSortingColumns,
|
||||
stringifyEvent,
|
||||
addBuildingBlockStyle,
|
||||
} from './helpers';
|
||||
|
||||
|
@ -22,173 +20,6 @@ import { euiThemeVars } from '@kbn/ui-theme';
|
|||
import { mockDnsEvent } from '../../../mock';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('stringifyEvent', () => {
|
||||
test('it omits __typename when it appears at arbitrary levels', () => {
|
||||
const toStringify: Ecs = {
|
||||
__typename: 'level 0',
|
||||
_id: '4',
|
||||
timestamp: '2018-11-08T19:03:25.937Z',
|
||||
host: {
|
||||
__typename: 'level 1',
|
||||
name: ['suricata'],
|
||||
ip: ['192.168.0.1'],
|
||||
},
|
||||
event: {
|
||||
id: ['4'],
|
||||
category: ['Attempted Administrator Privilege Gain'],
|
||||
type: ['Alert'],
|
||||
module: ['suricata'],
|
||||
severity: [1],
|
||||
},
|
||||
source: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [53],
|
||||
},
|
||||
destination: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [6343],
|
||||
},
|
||||
suricata: {
|
||||
eve: {
|
||||
flow_id: [4],
|
||||
proto: [''],
|
||||
alert: {
|
||||
signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'],
|
||||
signature_id: [4],
|
||||
__typename: 'level 2',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
id: ['4'],
|
||||
name: ['jack.black'],
|
||||
},
|
||||
geo: {
|
||||
region_name: ['neither'],
|
||||
country_iso_code: ['sasquatch'],
|
||||
},
|
||||
} as Ecs; // as cast so that `__typename` can be added for the tests even though it is not part of ECS
|
||||
const expected: Ecs = {
|
||||
_id: '4',
|
||||
timestamp: '2018-11-08T19:03:25.937Z',
|
||||
host: {
|
||||
name: ['suricata'],
|
||||
ip: ['192.168.0.1'],
|
||||
},
|
||||
event: {
|
||||
id: ['4'],
|
||||
category: ['Attempted Administrator Privilege Gain'],
|
||||
type: ['Alert'],
|
||||
module: ['suricata'],
|
||||
severity: [1],
|
||||
},
|
||||
source: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [53],
|
||||
},
|
||||
destination: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [6343],
|
||||
},
|
||||
suricata: {
|
||||
eve: {
|
||||
flow_id: [4],
|
||||
proto: [''],
|
||||
alert: {
|
||||
signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'],
|
||||
signature_id: [4],
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
id: ['4'],
|
||||
name: ['jack.black'],
|
||||
},
|
||||
geo: {
|
||||
region_name: ['neither'],
|
||||
country_iso_code: ['sasquatch'],
|
||||
},
|
||||
};
|
||||
expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it omits null and undefined values at arbitrary levels, for arbitrary data types', () => {
|
||||
const expected: Ecs = {
|
||||
_id: '4',
|
||||
host: {},
|
||||
event: {
|
||||
id: ['4'],
|
||||
category: ['theory'],
|
||||
type: ['Alert'],
|
||||
module: ['me'],
|
||||
severity: [1],
|
||||
},
|
||||
source: {
|
||||
port: [53],
|
||||
},
|
||||
destination: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [6343],
|
||||
},
|
||||
suricata: {
|
||||
eve: {
|
||||
flow_id: [4],
|
||||
proto: [''],
|
||||
alert: {
|
||||
signature: ['dance moves'],
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
id: ['4'],
|
||||
name: ['no use for a'],
|
||||
},
|
||||
geo: {
|
||||
region_name: ['bizzaro'],
|
||||
country_iso_code: ['world'],
|
||||
},
|
||||
};
|
||||
const toStringify: Ecs = {
|
||||
_id: '4',
|
||||
host: {},
|
||||
event: {
|
||||
id: ['4'],
|
||||
category: ['theory'],
|
||||
type: ['Alert'],
|
||||
module: ['me'],
|
||||
severity: [1],
|
||||
},
|
||||
source: {
|
||||
ip: undefined,
|
||||
port: [53],
|
||||
},
|
||||
destination: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [6343],
|
||||
},
|
||||
suricata: {
|
||||
eve: {
|
||||
flow_id: [4],
|
||||
proto: [''],
|
||||
alert: {
|
||||
signature: ['dance moves'],
|
||||
signature_id: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
id: ['4'],
|
||||
name: ['no use for a'],
|
||||
},
|
||||
geo: {
|
||||
region_name: ['bizzaro'],
|
||||
country_iso_code: ['world'],
|
||||
},
|
||||
};
|
||||
expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapSortDirectionToDirection', () => {
|
||||
test('it returns the expected direction when sortDirection is `asc`', () => {
|
||||
expect(mapSortDirectionToDirection('asc')).toBe('asc');
|
||||
|
|
|
@ -20,15 +20,8 @@ import type {
|
|||
ColumnHeaderOptions,
|
||||
SortColumnTimeline,
|
||||
SortDirection,
|
||||
TimelineEventsType,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitTypenameAndEmpty = (k: string, v: any): any | undefined =>
|
||||
k !== '__typename' && v != null ? v : undefined;
|
||||
|
||||
export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2);
|
||||
|
||||
/**
|
||||
* Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field
|
||||
* data necessary for custom timeline actions in conjunction with selection state
|
||||
|
@ -76,27 +69,6 @@ export const getEventIdToDataMapping = (
|
|||
export const isEventBuildingBlockType = (event: Ecs): boolean =>
|
||||
!isEmpty(event.kibana?.alert?.building_block_type);
|
||||
|
||||
export const isEvenEqlSequence = (event: Ecs): boolean => {
|
||||
if (!isEmpty(event.eql?.sequenceNumber)) {
|
||||
try {
|
||||
const sequenceNumber = (event.eql?.sequenceNumber ?? '').split('-')[0];
|
||||
return parseInt(sequenceNumber, 10) % 2 === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/** Return eventType raw or signal or eql */
|
||||
export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => {
|
||||
if (!isEmpty(event.signal?.rule?.id)) {
|
||||
return 'signal';
|
||||
} else if (!isEmpty(event.eql?.parentId)) {
|
||||
return 'eql';
|
||||
}
|
||||
return 'raw';
|
||||
};
|
||||
|
||||
/** Maps (Redux) `SortDirection` to the `direction` values used by `EuiDataGrid` */
|
||||
export const mapSortDirectionToDirection = (sortDirection: SortDirection): 'asc' | 'desc' => {
|
||||
switch (sortDirection) {
|
||||
|
|
|
@ -5,13 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type {
|
||||
OnColumnSorted,
|
||||
OnColumnsSorted,
|
||||
OnColumnRemoved,
|
||||
OnColumnResized,
|
||||
OnChangePage,
|
||||
OnRowSelected,
|
||||
OnSelectAll,
|
||||
OnUpdateColumns,
|
||||
} from '../../../common/types/timeline';
|
||||
export type { OnChangePage, OnRowSelected, OnSelectAll } from '../../../common/types/timeline';
|
||||
|
|
|
@ -28050,13 +28050,6 @@
|
|||
"xpack.timelines.clipboard.copy.to.the.clipboard": "Copier dans le presse-papiers",
|
||||
"xpack.timelines.clipboard.to.the.clipboard": "dans le presse-papiers",
|
||||
"xpack.timelines.copyToClipboardTooltip": "Copier dans le presse-papiers",
|
||||
"xpack.timelines.draggables.field.categoryLabel": "Catégorie",
|
||||
"xpack.timelines.draggables.field.fieldLabel": "Champ",
|
||||
"xpack.timelines.draggables.field.typeLabel": "Type",
|
||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "Afficher la catégorie",
|
||||
"xpack.timelines.emptyString.emptyStringDescription": "Chaîne vide",
|
||||
"xpack.timelines.eventDetails.copyToClipboardTooltip": "Copier dans le presse-papiers",
|
||||
"xpack.timelines.exitFullScreenButton": "Quitter le plein écran",
|
||||
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, =1 {catégorie} other {catégories}}",
|
||||
"xpack.timelines.fieldBrowser.categoriesTitle": "Catégories",
|
||||
"xpack.timelines.fieldBrowser.categoryLabel": "Catégorie",
|
||||
|
@ -28147,8 +28140,6 @@
|
|||
"xpack.timelines.timeline.closedAlertSuccessToastMessage": "Fermeture réussie de {totalAlerts} {totalAlerts, plural, =1 {alerte} other {alertes}}.",
|
||||
"xpack.timelines.timeline.closeSelectedTitle": "Marquer comme fermé",
|
||||
"xpack.timelines.timeline.descriptionTooltip": "Description",
|
||||
"xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "L'événement de la ligne {row} possède un outil de rendu d'événement. Appuyez sur Maj + flèche vers le bas pour faire la mise au point dessus.",
|
||||
"xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "L'événement de la ligne {row} possède {notesCount, plural, =1 {une note} other {{notesCount} des notes}}. Appuyez sur Maj + flèche vers la droite pour faire la mise au point sur les notes.",
|
||||
"xpack.timelines.timeline.eventsTableAriaLabel": "events; Page {activePage} sur {totalPages}",
|
||||
"xpack.timelines.timeline.fieldTooltip": "Champ",
|
||||
"xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "Retirer une colonne",
|
||||
|
@ -28165,7 +28156,6 @@
|
|||
"xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, =1 {alerte a été mise à jour} other {alertes ont été mises à jour}} correctement, mais { conflicts } n'ont pas pu être mis à jour\n car { conflicts, plural, =1 {elle était} other {elles étaient}} déjà en cours de modification.",
|
||||
"xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "Impossible de mettre à jour l'alerte, car elle était déjà en cours de modification.",
|
||||
"xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.",
|
||||
"xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}",
|
||||
"xpack.timelines.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie",
|
||||
"xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "Effacer la sélection",
|
||||
"xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "Sélectionner un total de {totalAlertsFormatted} {totalAlerts, plural, =1 {alerte} other {alertes}}",
|
||||
|
|
|
@ -28227,13 +28227,6 @@
|
|||
"xpack.timelines.clipboard.copy.to.the.clipboard": "クリップボードにコピー",
|
||||
"xpack.timelines.clipboard.to.the.clipboard": "クリップボードに",
|
||||
"xpack.timelines.copyToClipboardTooltip": "クリップボードにコピー",
|
||||
"xpack.timelines.draggables.field.categoryLabel": "カテゴリー",
|
||||
"xpack.timelines.draggables.field.fieldLabel": "フィールド",
|
||||
"xpack.timelines.draggables.field.typeLabel": "型",
|
||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "カテゴリーを表示します",
|
||||
"xpack.timelines.emptyString.emptyStringDescription": "空の文字列",
|
||||
"xpack.timelines.eventDetails.copyToClipboardTooltip": "クリップボードにコピー",
|
||||
"xpack.timelines.exitFullScreenButton": "全画面を終了",
|
||||
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}",
|
||||
"xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー",
|
||||
"xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー",
|
||||
|
@ -28324,8 +28317,6 @@
|
|||
"xpack.timelines.timeline.closedAlertSuccessToastMessage": "{totalAlerts} {totalAlerts, plural, other {件のアラート}}を正常にクローズしました。",
|
||||
"xpack.timelines.timeline.closeSelectedTitle": "クローズ済みに設定",
|
||||
"xpack.timelines.timeline.descriptionTooltip": "説明",
|
||||
"xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "行{row}のイベントにはイベントレンダラーがあります。Shiftと下矢印を押すとフォーカスします。",
|
||||
"xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "行{row}のイベントには{notesCount, plural, other {{notesCount}個のメモ}}があります。Shiftと右矢印を押すとメモをフォーカスします。",
|
||||
"xpack.timelines.timeline.eventsTableAriaLabel": "イベント; {activePage}/{totalPages} ページ",
|
||||
"xpack.timelines.timeline.fieldTooltip": "フィールド",
|
||||
"xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "列を削除",
|
||||
|
@ -28342,7 +28333,6 @@
|
|||
"xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } {updated, plural, other {アラート}}が正常に更新されましたが、{ conflicts }は更新できませんでした。\n { conflicts, plural, other {}}すでに修正されています。",
|
||||
"xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "アラートを更新できませんでした。アラートはすでに修正されています。",
|
||||
"xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。",
|
||||
"xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "表セルの行 {row}、列 {column} にいます",
|
||||
"xpack.timelines.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました",
|
||||
"xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "選択した項目をクリア",
|
||||
"xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, other {件のアラート}}を選択",
|
||||
|
|
|
@ -28261,13 +28261,6 @@
|
|||
"xpack.timelines.clipboard.copy.to.the.clipboard": "复制到剪贴板",
|
||||
"xpack.timelines.clipboard.to.the.clipboard": "至剪贴板",
|
||||
"xpack.timelines.copyToClipboardTooltip": "复制到剪贴板",
|
||||
"xpack.timelines.draggables.field.categoryLabel": "类别",
|
||||
"xpack.timelines.draggables.field.fieldLabel": "字段",
|
||||
"xpack.timelines.draggables.field.typeLabel": "类型",
|
||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "查看类别",
|
||||
"xpack.timelines.emptyString.emptyStringDescription": "空字符串",
|
||||
"xpack.timelines.eventDetails.copyToClipboardTooltip": "复制到剪贴板",
|
||||
"xpack.timelines.exitFullScreenButton": "退出全屏",
|
||||
"xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}",
|
||||
"xpack.timelines.fieldBrowser.categoriesTitle": "类别",
|
||||
"xpack.timelines.fieldBrowser.categoryLabel": "类别",
|
||||
|
@ -28358,8 +28351,6 @@
|
|||
"xpack.timelines.timeline.closedAlertSuccessToastMessage": "已成功关闭 {totalAlerts} 个{totalAlerts, plural, other {告警}}。",
|
||||
"xpack.timelines.timeline.closeSelectedTitle": "标记为已关闭",
|
||||
"xpack.timelines.timeline.descriptionTooltip": "描述",
|
||||
"xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly": "位于行 {row} 的事件具有事件呈现程序。按 shift + 向下箭头键以对其聚焦。",
|
||||
"xpack.timelines.timeline.eventHasNotesScreenReaderOnly": "位于行 {row} 的事件有{notesCount, plural, =1 {备注} other { {notesCount} 个备注}}。按 shift + 右箭头键以聚焦备注。",
|
||||
"xpack.timelines.timeline.eventsTableAriaLabel": "事件;第 {activePage} 页,共 {totalPages} 页",
|
||||
"xpack.timelines.timeline.fieldTooltip": "字段",
|
||||
"xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "移除列",
|
||||
|
@ -28376,7 +28367,6 @@
|
|||
"xpack.timelines.timeline.updateAlertStatusFailedDetailed": "{ updated } 个{updated, plural, other {告警}}已成功更新,但是 { conflicts } 个无法更新,\n 因为{ conflicts, plural, other {其}}已被修改。",
|
||||
"xpack.timelines.timeline.updateAlertStatusFailedSingleAlert": "无法更新告警,因为它已被修改。",
|
||||
"xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。",
|
||||
"xpack.timelines.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}",
|
||||
"xpack.timelines.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误",
|
||||
"xpack.timelines.toolbar.bulkActions.clearSelectionTitle": "清除所选内容",
|
||||
"xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle": "选择全部 {totalAlertsFormatted} 个{totalAlerts, plural, other {告警}}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue