[Security Solution] Deletes Old Timeline Code (#196243)

## Summary

Fixes https://github.com/elastic/security-team/issues/9676

This is follow up of the PR :
https://github.com/elastic/kibana/pull/195959 where we had removed
feature flag `unifiedComponentsInTimelineDisabled`.

This PR completes unified components migration in Timeline. 

## Changes

Most changes in this PR are : 

1. Deletion for the obsolete code.
2. Refactoring of needed utils ( only 3. have marked in comments below )
3. Updating import path of above utils.
This commit is contained in:
Jatin Kathuria 2024-11-07 11:39:55 +01:00 committed by GitHub
parent 3fff48a2ca
commit c9d167f1ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 212 additions and 7104 deletions

View file

@ -19,12 +19,12 @@ import type {
SetEventsLoading,
ControlColumnProps,
} from '../../../../../common/types';
import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useTourContext } from '../../guided_onboarding_tour';
import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config';
import { getMappedNonEcsValue } from '../../../utils/get_mapped_non_ecs_value';
export type RowActionProps = EuiDataGridCellValueElementProps & {
columnHeaders: ColumnHeaderOptions[];

View file

@ -36,7 +36,6 @@ import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
import type { EuiDataGridRowHeightsOptions } from '@elastic/eui';
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../../../common/constants';
import type { Sort } from '../../../timelines/components/timeline/body/sort';
import type {
ControlColumnProps,
OnRowSelected,
@ -44,7 +43,7 @@ import type {
SetEventsDeleted,
SetEventsLoading,
} from '../../../../common/types';
import type { RowRenderer } from '../../../../common/types/timeline';
import type { RowRenderer, SortColumnTimeline as Sort } from '../../../../common/types/timeline';
import { InputsModelId } from '../../store/inputs/constants';
import type { State } from '../../store';
import { inputsActions } from '../../store/actions';

View file

@ -10,9 +10,9 @@ import { EuiButtonIcon, EuiToolTip, EuiCheckbox } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { isFullScreen } from '../../../timelines/components/timeline/helpers';
import type { HeaderActionProps } from '../../../../common/types';
import { TimelineId } from '../../../../common/types';
import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers';
import { isActiveTimeline } from '../../../helpers';
import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineActions } from '../../../timelines/store';

View file

@ -0,0 +1,56 @@
/*
* 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 type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import { getMappedNonEcsValue, useGetMappedNonEcsValue } from './get_mapped_non_ecs_value';
import { renderHook } from '@testing-library/react-hooks';
describe('getMappedNonEcsValue', () => {
it('should return the correct value', () => {
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
const fieldName = 'field1';
const result = getMappedNonEcsValue({ data, fieldName });
expect(result).toEqual(['value1']);
});
it('should return undefined if item is null', () => {
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
const fieldName = 'field2';
const result = getMappedNonEcsValue({ data, fieldName });
expect(result).toEqual(undefined);
});
it('should return undefined if item.value is null', () => {
const data: TimelineNonEcsData[] = [{ field: 'field1', value: null }];
const fieldName = 'non_existent_field';
const result = getMappedNonEcsValue({ data, fieldName });
expect(result).toEqual(undefined);
});
it('should return undefined if data is undefined', () => {
const data = undefined;
const fieldName = 'field1';
const result = getMappedNonEcsValue({ data, fieldName });
expect(result).toEqual(undefined);
});
it('should return undefined if data is empty', () => {
const data: TimelineNonEcsData[] = [];
const fieldName = 'field1';
const result = getMappedNonEcsValue({ data, fieldName });
expect(result).toEqual(undefined);
});
});
describe('useGetMappedNonEcsValue', () => {
it('should return the correct value', () => {
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
const fieldName = 'field1';
const { result } = renderHook(() => useGetMappedNonEcsValue({ data, fieldName }));
expect(result.current).toEqual(['value1']);
});
});

View file

@ -0,0 +1,40 @@
/*
* 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 type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import { useMemo } from 'react';
export const getMappedNonEcsValue = ({
data,
fieldName,
}: {
data?: TimelineNonEcsData[];
fieldName: string;
}): string[] | undefined => {
/*
While data _should_ always be defined
There is the potential for race conditions where a component using this function
is still visible in the UI, while the data has since been removed.
To cover all scenarios where this happens we'll check for the presence of data here
*/
if (!data || data.length === 0) return undefined;
const item = data.find((d) => d.field === fieldName);
if (item != null && item.value != null) {
return item.value;
}
return undefined;
};
export const useGetMappedNonEcsValue = ({
data,
fieldName,
}: {
data?: TimelineNonEcsData[];
fieldName: string;
}): string[] | undefined => {
return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]);
};

View file

@ -57,7 +57,7 @@ import {
} from '@kbn/lists-plugin/common/constants.mock';
import { of } from 'rxjs';
import { timelineDefaults } from '../../../timelines/store/defaults';
import { defaultUdtHeaders } from '../../../timelines/components/timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
jest.mock('../../../timelines/containers/api', () => ({
getTimelineTemplate: jest.fn(),

View file

@ -33,7 +33,7 @@ import { getField } from '../../../../helpers';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { defaultUdtHeaders } from '../../../../timelines/components/timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
interface UseInvestigateInTimelineActionProps {
ecsRowData?: Ecs | Ecs[] | null;

View file

@ -12,9 +12,9 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import { ALERT_DURATION, ALERT_REASON, ALERT_SEVERITY, ALERT_STATUS } from '@kbn/rule-data-utils';
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
import { TruncatableText } from '../../../../common/components/truncatable_text';
import { Severity } from '../../../components/severity';
import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { Status } from '../../../components/status';

View file

@ -9,10 +9,10 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import { ALERT_SEVERITY, ALERT_REASON } from '@kbn/rule-data-utils';
import React from 'react';
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
import { DefaultDraggable } from '../../../../common/components/draggables';
import { TruncatableText } from '../../../../common/components/truncatable_text';
import { Severity } from '../../../components/severity';
import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';

View file

@ -29,12 +29,12 @@ import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../common/containers/use_full_screen';
import { isFullScreen } from '../timeline/body/column_headers';
import { inputsActions } from '../../../common/store/actions';
import { Resolver } from '../../../resolver/view';
import { useTimelineDataFilters } from '../../containers/use_timeline_data_filters';
import { timelineSelectors } from '../../store';
import { timelineDefaults } from '../../store/defaults';
import { isFullScreen } from '../timeline/helpers';
const SESSION_VIEW_FULL_SCREEN = 'sessionViewFullScreen';

View file

@ -12,7 +12,7 @@ import { TimelineId } from '../../../../../common/types';
import { timelineActions } from '../../../store';
import { TestProviders } from '../../../../common/mock';
import { RowRendererValues } from '../../../../../common/api/timeline';
import { defaultUdtHeaders } from '../../timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../../timeline/body/column_headers/default_headers';
jest.mock('../../../../common/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../../../common/hooks/use_selector');

View file

@ -17,7 +17,7 @@ import {
TimelineTypeEnum,
} from '../../../../common/api/timeline';
import { TestProviders } from '../../../common/mock';
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
jest.mock('../../../common/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../../common/hooks/use_selector');

View file

@ -35,7 +35,7 @@ import {
mockTemplate as mockSelectedTemplate,
} from './__mocks__';
import { resolveTimeline } from '../../containers/api';
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
jest.mock('../../../common/hooks/use_experimental_features');

View file

@ -35,7 +35,10 @@ import { useUpdateTimeline } from './use_update_timeline';
import type { TimelineModel } from '../../store/model';
import { timelineDefaults } from '../../store/defaults';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import {
defaultColumnHeaderType,
defaultUdtHeaders,
} from '../timeline/body/column_headers/default_headers';
import type { OpenTimelineResult, TimelineErrorCallback } from './types';
import { IS_OPERATOR } from '../timeline/data_providers/data_provider';
@ -46,7 +49,6 @@ import {
DEFAULT_TO_MOMENT,
} from '../../../common/utils/default_date_settings';
import { resolveTimeline } from '../../containers/api';
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
import { timelineActions } from '../../store';
export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline';

View file

@ -53,7 +53,7 @@ import { SourcererScopeName } from '../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../sourcerer/containers';
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions';
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
import { timelineDefaults } from '../../store/defaults';
interface OwnProps<TCache = object> {

View file

@ -1,513 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
<ColumnHeadersComponent
actionsColumnWidth={124}
browserFields={
Object {
"agent": Object {
"fields": Object {
"agent.ephemeral_id": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "agent.ephemeral_id",
"searchable": true,
"type": "string",
},
"agent.hostname": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "agent.hostname",
"searchable": true,
"type": "string",
},
"agent.id": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "agent.id",
"searchable": true,
"type": "string",
},
"agent.name": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "agent.name",
"searchable": true,
"type": "string",
},
},
},
"auditd": Object {
"fields": Object {
"auditd.data.a0": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "auditd.data.a0",
"searchable": true,
"type": "string",
},
"auditd.data.a1": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "auditd.data.a1",
"searchable": true,
"type": "string",
},
"auditd.data.a2": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "auditd.data.a2",
"searchable": true,
"type": "string",
},
},
},
"base": Object {
"fields": Object {
"@timestamp": Object {
"aggregatable": true,
"esTypes": Array [
"date",
],
"name": "@timestamp",
"readFromDocValues": true,
"searchable": true,
"type": "date",
},
"_id": Object {
"aggregatable": false,
"esTypes": Array [],
"name": "_id",
"searchable": true,
"type": "string",
},
"message": Object {
"aggregatable": false,
"esTypes": Array [
"text",
],
"format": Object {
"id": "string",
},
"name": "message",
"searchable": true,
"type": "string",
},
},
},
"client": Object {
"fields": Object {
"client.address": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "client.address",
"searchable": true,
"type": "string",
},
"client.bytes": Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"name": "client.bytes",
"searchable": true,
"type": "number",
},
"client.domain": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "client.domain",
"searchable": true,
"type": "string",
},
"client.geo.country_iso_code": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "client.geo.country_iso_code",
"searchable": true,
"type": "string",
},
},
},
"cloud": Object {
"fields": Object {
"cloud.account.id": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "cloud.account.id",
"searchable": true,
"type": "string",
},
"cloud.availability_zone": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "cloud.availability_zone",
"searchable": true,
"type": "string",
},
},
},
"container": Object {
"fields": Object {
"container.id": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "container.id",
"searchable": true,
"type": "string",
},
"container.image.name": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "container.image.name",
"searchable": true,
"type": "string",
},
"container.image.tag": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "container.image.tag",
"searchable": true,
"type": "string",
},
},
},
"destination": Object {
"fields": Object {
"destination.address": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "destination.address",
"searchable": true,
"type": "string",
},
"destination.bytes": Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"name": "destination.bytes",
"searchable": true,
"type": "number",
},
"destination.domain": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "destination.domain",
"searchable": true,
"type": "string",
},
"destination.ip": Object {
"aggregatable": true,
"esTypes": Array [
"ip",
],
"name": "destination.ip",
"searchable": true,
"type": "ip",
},
"destination.port": Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"name": "destination.port",
"searchable": true,
"type": "number",
},
},
},
"event": Object {
"fields": Object {
"event.action": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"format": Object {
"id": "string",
},
"name": "event.action",
"searchable": true,
"type": "string",
},
"event.category": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"format": Object {
"id": "string",
},
"name": "event.category",
"searchable": true,
"type": "string",
},
"event.end": Object {
"aggregatable": true,
"esTypes": Array [
"date",
],
"name": "event.end",
"searchable": true,
"type": "date",
},
"event.kind": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"format": Object {
"id": "string",
},
"name": "event.kind",
"searchable": true,
"type": "string",
},
"event.severity": Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"format": Object {
"id": "number",
},
"name": "event.severity",
"searchable": true,
"type": "number",
},
},
},
"host": Object {
"fields": Object {
"host.name": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"format": Object {
"id": "string",
},
"name": "host.name",
"searchable": true,
"type": "string",
},
},
},
"nestedField": Object {
"fields": Object {
"nestedField.firstAttributes": Object {
"aggregatable": false,
"name": "nestedField.firstAttributes",
"searchable": true,
"subType": Object {
"nested": Object {
"path": "nestedField",
},
},
"type": "string",
},
"nestedField.secondAttributes": Object {
"aggregatable": false,
"name": "nestedField.secondAttributes",
"searchable": true,
"subType": Object {
"nested": Object {
"path": "nestedField",
},
},
"type": "string",
},
"nestedField.thirdAttributes": Object {
"aggregatable": false,
"name": "nestedField.thirdAttributes",
"searchable": true,
"subType": Object {
"nested": Object {
"path": "nestedField",
},
},
"type": "date",
},
},
},
"process": Object {
"fields": Object {
"process.args": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "process.args",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
},
},
"source": Object {
"fields": Object {
"source.ip": Object {
"aggregatable": true,
"esTypes": Array [
"ip",
],
"name": "source.ip",
"searchable": true,
"type": "ip",
},
"source.port": Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"name": "source.port",
"searchable": true,
"type": "number",
},
},
},
"user": Object {
"fields": Object {
"user.name": Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"format": Object {
"id": "string",
},
"name": "user.name",
"searchable": true,
"type": "string",
},
},
},
}
}
columnHeaders={
Array [
Object {
"columnHeaderType": "not-filtered",
"esTypes": Array [
"date",
],
"id": "@timestamp",
"initialWidth": 190,
"type": "date",
},
Object {
"columnHeaderType": "not-filtered",
"id": "message",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.category",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "event.action",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "host.name",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "source.ip",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "destination.ip",
"initialWidth": 180,
},
Object {
"columnHeaderType": "not-filtered",
"id": "user.name",
"initialWidth": 180,
},
]
}
isSelectAllChecked={false}
leadingControlColumns={
Array [
Object {
"headerCellRender": Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": [Function],
},
},
"id": "default-timeline-control-column",
"rowCellRender": Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": [Function],
},
"width": 124,
},
]
}
onSelectAll={[Function]}
show={true}
showEventsSelect={false}
showSelectAllCheckbox={false}
sort={
Array [
Object {
"columnId": "@timestamp",
"columnType": "date",
"esTypes": Array [
"date",
],
"sortDirection": "desc",
},
]
}
tabType="query"
timelineId="timeline-test"
trailingControlColumns={Array []}
/>
`;

View file

@ -1,70 +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';
import type { OnColumnRemoved } from '../../../events';
import { EventsHeadingExtra, EventsLoading } from '../../../styles';
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';

View file

@ -1,307 +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 type { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import type { DraggableChildrenFn } from '@hello-pangea/dnd';
import { Draggable } from '@hello-pangea/dnd';
import type { ResizeCallback } from 're-resizable';
import { Resizable } from 're-resizable';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants';
import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers';
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
import { TimelineTabs } from '../../../../../../common/types/timeline';
import { Direction } from '../../../../../../common/search_strategy';
import type { OnFilterChange } from '../../events';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
import type { Sort } from '../sort';
import { Header } from './header';
import { timelineActions } from '../../../../store';
import * as i18n from './translations';
const ContextMenu = styled(EuiContextMenu)`
width: 115px;
& .euiContextMenuItem {
font-size: 12px;
padding: 4px 8px;
width: 115px;
}
`;
const PopoverContainer = styled.div<{ $width: number }>`
& .euiPopover {
padding-right: 8px;
width: ${({ $width }) => $width}px;
}
`;
const RESIZABLE_ENABLE = { right: true };
interface ColumneHeaderProps {
draggableIndex: number;
header: ColumnHeaderOptions;
isDragging: boolean;
onFilterChange?: OnFilterChange;
sort: Sort[];
tabType: TimelineTabs;
timelineId: string;
}
const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
draggableIndex,
header,
timelineId,
isDragging,
onFilterChange,
sort,
tabType,
}) => {
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
const dispatch = useDispatch();
const resizableSize = useMemo(
() => ({
width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH,
height: 'auto',
}),
[header.initialWidth]
);
const resizableStyle: {
position: 'absolute' | 'relative';
} = useMemo(
() => ({
position: isDragging ? 'absolute' : 'relative',
}),
[isDragging]
);
const resizableHandleComponent = useMemo(
() => ({
right: <EventsHeadingHandle />,
}),
[]
);
const handleResizeStop: ResizeCallback = useCallback(
(e, direction, ref, delta) => {
dispatch(
timelineActions.applyDeltaToColumnWidth({
columnId: header.id,
delta: delta.width,
id: timelineId,
})
);
},
[dispatch, header.id, timelineId]
);
const draggableId = useMemo(
() =>
getDraggableFieldId({
contextId: `timeline-column-headers-${tabType}-${timelineId}`,
fieldId: header.id,
}),
[tabType, timelineId, header.id]
);
const onColumnSort = useCallback(
(sortDirection: Direction) => {
const columnId = header.id;
const columnType = header.type ?? '';
const esTypes = header.esTypes ?? [];
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
const newSort =
headerIndex === -1
? [
...sort,
{
columnId,
columnType,
esTypes,
sortDirection,
},
]
: [
...sort.slice(0, headerIndex),
{
columnId,
columnType,
esTypes,
sortDirection,
},
...sort.slice(headerIndex + 1),
];
dispatch(
timelineActions.updateSort({
id: timelineId,
sort: newSort,
})
);
},
[dispatch, header, sort, timelineId]
);
const handleClosePopOverTrigger = useCallback(() => {
setHoverActionsOwnFocus(false);
restoreFocus();
}, [restoreFocus]);
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
{
id: 0,
items: [
{
icon: <EuiIcon type="eyeClosed" size="s" />,
name: i18n.HIDE_COLUMN,
onClick: () => {
dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id }));
handleClosePopOverTrigger();
},
},
...(tabType !== TimelineTabs.eql
? [
{
disabled: !header.aggregatable,
icon: <EuiIcon type="sortUp" size="s" />,
name: i18n.SORT_AZ,
onClick: () => {
onColumnSort(Direction.asc);
handleClosePopOverTrigger();
},
},
{
disabled: !header.aggregatable,
icon: <EuiIcon type="sortDown" size="s" />,
name: i18n.SORT_ZA,
onClick: () => {
onColumnSort(Direction.desc);
handleClosePopOverTrigger();
},
},
]
: []),
],
},
],
[
dispatch,
handleClosePopOverTrigger,
header.aggregatable,
header.id,
onColumnSort,
tabType,
timelineId,
]
);
const headerButton = useMemo(
() => (
<Header timelineId={timelineId} header={header} onFilterChange={onFilterChange} sort={sort} />
),
[header, onFilterChange, sort, timelineId]
);
const DraggableContent = useCallback<DraggableChildrenFn>(
(dragProvided) => (
<EventsTh
data-test-subj="draggable-header"
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
ref={dragProvided.innerRef}
>
<EventsThContent>
<PopoverContainer $width={header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH}>
<EuiPopover
anchorPosition="downLeft"
button={headerButton}
closePopover={handleClosePopOverTrigger}
isOpen={hoverActionsOwnFocus}
ownFocus
panelPaddingSize="none"
>
<ContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</PopoverContainer>
</EventsThContent>
</EventsTh>
),
[handleClosePopOverTrigger, headerButton, header.initialWidth, hoverActionsOwnFocus, panels]
);
const onFocus = useCallback(() => {
keyboardHandlerRef.current?.focus();
}, []);
const openPopover = useCallback(() => {
setHoverActionsOwnFocus(true);
}, []);
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
closePopover: handleClosePopOverTrigger,
draggableId,
fieldName: header.id,
keyboardHandlerRef,
openPopover,
});
const keyDownHandler = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (!hoverActionsOwnFocus) {
onKeyDown(keyboardEvent);
}
},
[hoverActionsOwnFocus, onKeyDown]
);
return (
<Resizable
enable={RESIZABLE_ENABLE}
size={resizableSize}
style={resizableStyle}
handleComponent={resizableHandleComponent}
onResizeStop={handleResizeStop}
>
<div
aria-colindex={
draggableIndex != null ? draggableIndex + ARIA_COLUMN_INDEX_OFFSET : undefined
}
className={DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}
data-test-subj="draggableWrapperKeyboardHandler"
onClick={onFocus}
onBlur={onBlur}
onKeyDown={keyDownHandler}
ref={keyboardHandlerRef}
role="columnheader"
tabIndex={0}
>
<Draggable
data-test-subj="draggable"
// Required for drag events while hovering the sort button to work: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md#interactive-child-elements-within-a-draggable-
disableInteractiveElementBlocking
draggableId={draggableId}
index={draggableIndex}
key={header.id}
>
{DraggableContent}
</Draggable>
</div>
</Resizable>
);
};
export const ColumnHeader = React.memo(ColumnHeaderComponent);

View file

@ -6,51 +6,48 @@
*/
import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../../common/types';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
} from '../constants';
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
export const defaultHeaders: ColumnHeaderOptions[] = [
export const defaultUdtHeaders: ColumnHeaderOptions[] = [
{
columnHeaderType: defaultColumnHeaderType,
id: '@timestamp',
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
esTypes: ['date'],
type: 'date',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'message',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'event.category',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'event.action',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'host.name',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'source.ip',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'destination.ip',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'user.name',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
];

View file

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Filter renders correctly against snapshot 1`] = `
<TextFilter
columnId="@timestamp"
minWidth={190}
onFilterChange={[Function]}
/>
`;

View file

@ -1,55 +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 { defaultHeaders } from '../default_headers';
import { Filter } from '.';
import type { ColumnHeaderType } from '../../../../../../../common/types';
const textFilter: ColumnHeaderType = 'text-filter';
const notFiltered: ColumnHeaderType = 'not-filtered';
describe('Filter', () => {
test('renders correctly against snapshot', () => {
const textFilterColumnHeader = {
...defaultHeaders[0],
columnHeaderType: textFilter,
};
const wrapper = shallow(<Filter header={textFilterColumnHeader} />);
expect(wrapper).toMatchSnapshot();
});
describe('rendering', () => {
test('it renders a text filter when the columnHeaderType is "text-filter"', () => {
const textFilterColumnHeader = {
...defaultHeaders[0],
columnHeaderType: textFilter,
};
const wrapper = mount(<Filter header={textFilterColumnHeader} />);
expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty(
'placeholder'
);
});
test('it does NOT render a filter when the columnHeaderType is "not-filtered"', () => {
const notFilteredHeader = {
...defaultHeaders[0],
columnHeaderType: notFiltered,
};
const wrapper = mount(<Filter header={notFilteredHeader} />);
expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false);
});
});
});

View file

@ -1,39 +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 { noop } from 'lodash/fp';
import React from 'react';
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants';
import type { OnFilterChange } from '../../../events';
import { TextFilter } from '../text_filter';
interface Props {
header: ColumnHeaderOptions;
onFilterChange?: OnFilterChange;
}
/** Renders a header's filter, based on the `columnHeaderType` */
export const Filter = React.memo<Props>(({ header, onFilterChange = noop }) => {
switch (header.columnHeaderType) {
case 'text-filter':
return (
<TextFilter
columnId={header.id}
minWidth={header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH}
onFilterChange={onFilterChange}
placeholder={header.placeholder}
/>
);
case 'not-filtered': // fall through
default:
return null;
}
});
Filter.displayName = 'Filter';

View file

@ -1,77 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header renders correctly against snapshot 1`] = `
<Fragment>
<Memo(HeaderContentComponent)
header={
Object {
"columnHeaderType": "not-filtered",
"esTypes": Array [
"date",
],
"id": "@timestamp",
"initialWidth": 190,
"type": "date",
}
}
isLoading={false}
isResizing={false}
onClick={[Function]}
showSortingCapability={true}
sort={
Array [
Object {
"columnId": "@timestamp",
"columnType": "date",
"esTypes": Array [
"date",
],
"sortDirection": "desc",
},
]
}
>
<Actions
header={
Object {
"columnHeaderType": "not-filtered",
"esTypes": Array [
"date",
],
"id": "@timestamp",
"initialWidth": 190,
"type": "date",
}
}
isLoading={false}
onColumnRemoved={[Function]}
sort={
Array [
Object {
"columnId": "@timestamp",
"columnType": "date",
"esTypes": Array [
"date",
],
"sortDirection": "desc",
},
]
}
/>
</Memo(HeaderContentComponent)>
<Filter
header={
Object {
"columnHeaderType": "not-filtered",
"esTypes": Array [
"date",
],
"id": "@timestamp",
"initialWidth": 190,
"type": "date",
}
}
onFilterChange={[Function]}
/>
</Fragment>
`;

View file

@ -1,84 +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 '../../../../../../common/components/truncatable_text';
import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles';
import type { 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);

View file

@ -1,56 +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,
SortDirection,
} from '../../../../../../../common/types/timeline';
import type { Sort } 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 Direction.desc;
}
};
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);

View file

@ -1,364 +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, { type ReactNode } from 'react';
import { timelineActions } from '../../../../../store';
import { TestProviders } from '../../../../../../common/mock';
import type { 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 { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector';
import type { ColumnHeaderType } from '../../../../../../../common/types';
import { TimelineId } from '../../../../../../../common/types/timeline';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useSelector: jest.fn(),
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
useDeepEqualSelector: jest.fn(),
}));
const filteredColumnHeader: ColumnHeaderType = 'text-filter';
describe('Header', () => {
const columnHeader = defaultHeaders[0];
const sort: Sort[] = [
{
columnId: columnHeader.id,
columnType: columnHeader.type ?? '',
esTypes: columnHeader.esTypes ?? [],
sortDirection: Direction.desc,
},
];
const timelineId = TimelineId.test;
beforeEach(() => {
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
});
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 = {} as unknown as ReactNode; // 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
);
});
test('it renders a filter', () => {
const columnWithFilter = {
...columnHeader,
columnHeaderType: filteredColumnHeader,
};
const wrapper = mount(
<TestProviders>
<HeaderComponent header={columnWithFilter} sort={sort} timelineId={timelineId} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty(
'placeholder'
);
});
});
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(
timelineActions.updateSort({
id: timelineId,
sort: [
{
columnId: columnHeader.id,
columnType: columnHeader.type ?? 'number',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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',
esTypes: columnHeader.esTypes ?? [],
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);
});
});
});

View file

@ -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 { noop } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { isDataViewFieldSubtypeNested } from '@kbn/es-query';
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
import {
useDeepEqualSelector,
useShallowEqualSelector,
} from '../../../../../../common/hooks/use_selector';
import { timelineActions, timelineSelectors } from '../../../../../store';
import type { OnColumnRemoved, OnFilterChange } from '../../../events';
import type { Sort } from '../../sort';
import { Actions } from '../actions';
import { Filter } from '../filter';
import { getNewSortDirectionOnClick } from './helpers';
import { HeaderContent } from './header_content';
import { isEqlOnSelector } from './selectors';
interface Props {
header: ColumnHeaderOptions;
onFilterChange?: OnFilterChange;
sort: Sort[];
timelineId: string;
}
export const HeaderComponent: React.FC<Props> = ({
header,
onFilterChange = noop,
sort,
timelineId,
}) => {
const dispatch = useDispatch();
const getIsEqlOn = useMemo(() => isEqlOnSelector(), []);
const isEqlOn = useShallowEqualSelector((state) => getIsEqlOn(state, timelineId));
const onColumnSort = useCallback(() => {
const columnId = header.id;
const columnType = header.type ?? '';
const esTypes = header.esTypes ?? [];
const sortDirection = getNewSortDirectionOnClick({
clickedHeader: header,
currentSort: sort,
});
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
let newSort = [];
if (headerIndex === -1) {
newSort = [
...sort,
{
columnId,
columnType,
esTypes,
sortDirection,
},
];
} else {
newSort = [
...sort.slice(0, headerIndex),
{
columnId,
columnType,
esTypes,
sortDirection,
},
...sort.slice(headerIndex + 1),
];
}
dispatch(
timelineActions.updateSort({
id: timelineId,
sort: newSort,
})
);
}, [dispatch, header, sort, timelineId]);
const onColumnRemoved = useCallback<OnColumnRemoved>(
(columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })),
[dispatch, timelineId]
);
const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { isLoading } = useDeepEqualSelector(
(state) => getManageTimeline(state, timelineId) || { isLoading: false }
);
const showSortingCapability = !isEqlOn && !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>
<Filter header={header} onFilterChange={onFilterChange} />
</>
);
};
export const Header = React.memo(HeaderComponent);

View file

@ -1,13 +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 { createSelector } from 'reselect';
import { TimelineTabs } from '../../../../../../../common/types/timeline';
import { selectTimeline } from '../../../../../store/selectors';
export const isEqlOnSelector = () =>
createSelector(selectTimeline, (timeline) => timeline?.activeTab === TimelineTabs.eql);

View file

@ -1,67 +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"
/>
<EuiBadge
data-test-subj="type-value-date"
key="date"
>
date
</EuiBadge>
</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>
`;

View file

@ -1,89 +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 type { ColumnHeaderOptions } from '../../../../../../../common/types';
import { defaultHeaders } from '../../../../../../common/mock';
import { HeaderToolTipContent } from '.';
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-${header.esTypes?.at(0)}"]`)
.first()
.text()
).toEqual(header.esTypes?.at(0));
});
test('it renders multiple `esTypes`', () => {
const hasMultipleTypes = { ...header, esTypes: ['long', 'date'] };
const wrapper = mount(<HeaderToolTipContent header={hasMultipleTypes} />);
hasMultipleTypes.esTypes.forEach((esType) => {
expect(wrapper.find(`[data-test-subj="type-value-${esType}"]`).first().text()).toEqual(
esType
);
});
});
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();
});
});

View file

@ -1,86 +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, EuiBadge } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
import { getIconFromType } from '../../../../../../common/components/event_details/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;
font-weight: bold;
`;
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)} />
{header.esTypes?.map((esType) => (
<EuiBadge data-test-subj={`type-value-${esType}`} key={esType}>
{esType}
</EuiBadge>
))}
</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';

View file

@ -9,8 +9,7 @@ import { mockBrowserFields } from '../../../../../common/containers/source/mock'
import type { BrowserFields } from '../../../../../../common/search_strategy';
import type { ColumnHeaderOptions } from '../../../../../../common/types';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
import { defaultHeaders } from './default_headers';
import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { defaultUdtHeaders } from './default_headers';
import {
getColumnWidthFromType,
getColumnHeaders,
@ -88,7 +87,7 @@ describe('helpers', () => {
});
});
test('should return the expected metadata in case of unified header', () => {
test('should return the expected metadata in case of default header', () => {
const inputHeaders = defaultUdtHeaders;
expect(getColumnHeader('@timestamp', inputHeaders)).toEqual({
columnHeaderType: 'not-filtered',
@ -112,7 +111,7 @@ describe('helpers', () => {
searchable: true,
type: 'date',
esTypes: ['date'],
initialWidth: 190,
initialWidth: 215,
},
{
aggregatable: true,
@ -122,7 +121,6 @@ describe('helpers', () => {
searchable: true,
type: 'ip',
esTypes: ['ip'],
initialWidth: 180,
},
{
aggregatable: true,
@ -132,10 +130,9 @@ describe('helpers', () => {
searchable: true,
type: 'ip',
esTypes: ['ip'],
initialWidth: 180,
},
];
const mockHeader = defaultHeaders.filter((h) =>
const mockHeader = defaultUdtHeaders.filter((h) =>
['@timestamp', 'source.ip', 'destination.ip'].includes(h.id)
);
expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData);

View file

@ -1,336 +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 './default_headers';
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
import type { Sort } from '../sort';
import { TestProviders } from '../../../../../common/mock/test_providers';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
import type { ColumnHeadersComponentProps } from '.';
import { ColumnHeadersComponent } from '.';
import { cloneDeep } from 'lodash/fp';
import { timelineActions } from '../../../../store';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { Direction } from '../../../../../../common/search_strategy';
import { getDefaultControlColumn } from '../control_columns';
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
import type { UseFieldBrowserOptionsProps } from '../../../fields_browser';
import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
import { getActionsColumnWidth } from '../../../../../common/components/header_actions';
jest.mock('../../../../../common/lib/kibana', () => ({
useKibana: () => ({
services: {
timelines: mockTimelines,
triggersActionsUi: mockTriggersActionsUi,
},
}),
}));
const mockUseFieldBrowserOptions = jest.fn();
jest.mock('../../../fields_browser', () => ({
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),
}));
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
const timelineId = TimelineId.test;
describe('ColumnHeaders', () => {
const mount = useMountAppended();
const ACTION_BUTTON_COUNT = 4;
const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT);
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
...x,
headerCellRender: HeaderActions,
}));
const sort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
];
const defaultProps: ColumnHeadersComponentProps = {
actionsColumnWidth,
browserFields: mockBrowserFields,
columnHeaders: defaultHeaders,
isSelectAllChecked: false,
onSelectAll: jest.fn,
show: true,
showEventsSelect: false,
showSelectAllCheckbox: false,
sort,
tabType: TimelineTabs.query,
timelineId,
leadingControlColumns,
trailingControlColumns: [],
};
describe('rendering', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<TestProviders>
<ColumnHeadersComponent {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot();
});
test('it renders the field browser', () => {
const mockCloseEditor = jest.fn();
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
editorActionsRef.current = { closeEditor: mockCloseEditor };
return {};
});
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true);
});
test('it renders every column header', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent {...defaultProps} />
</TestProviders>
);
defaultHeaders.forEach((h) => {
expect(wrapper.find('[data-test-subj="headers-group"]').first().text()).toContain(h.id);
});
});
});
describe('#onColumnsSorted', () => {
let mockSort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'string',
esTypes: [],
sortDirection: Direction.asc,
},
];
let mockDefaultHeaders = cloneDeep(
defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true }))
);
beforeEach(() => {
mockDefaultHeaders = cloneDeep(
defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true }))
);
mockSort = [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'string',
esTypes: [],
sortDirection: Direction.asc,
},
];
});
test('Add column `event.category` as desc sorting', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
{...{ ...defaultProps, columnHeaders: mockDefaultHeaders, sort: mockSort }}
/>
</TestProviders>
);
wrapper
.find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]')
.first()
.simulate('click');
expect(mockDispatch).toHaveBeenCalledWith(
timelineActions.updateSort({
id: timelineId,
sort: [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: 'string',
esTypes: [],
sortDirection: Direction.asc,
},
{
columnId: 'event.category',
columnType: '',
esTypes: [],
sortDirection: Direction.desc,
},
],
})
);
});
test('Change order of column `@timestamp` from desc to asc without changing index position', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
{...{ ...defaultProps, columnHeaders: mockDefaultHeaders, sort: mockSort }}
/>
</TestProviders>
);
wrapper
.find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]')
.first()
.simulate('click');
expect(mockDispatch).toHaveBeenCalledWith(
timelineActions.updateSort({
id: timelineId,
sort: [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.asc,
},
{
columnId: 'host.name',
columnType: 'string',
esTypes: [],
sortDirection: Direction.asc,
},
],
})
);
});
test('Change order of column `host.name` from asc to desc without changing index position', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
{...{
...defaultProps,
columnHeaders: mockDefaultHeaders,
sort: mockSort,
}}
/>
</TestProviders>
);
wrapper
.find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]')
.first()
.simulate('click');
expect(mockDispatch).toHaveBeenCalledWith(
timelineActions.updateSort({
id: timelineId,
sort: [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
{
columnId: 'host.name',
columnType: '',
esTypes: [],
sortDirection: Direction.desc,
},
],
})
);
});
test('Does not render the default leading action column header and renders a custom trailing header', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
{...{
...defaultProps,
columnHeaders: mockDefaultHeaders,
sort: mockSort,
leadingControlColumns: [],
trailingControlColumns: testTrailingControlColumns,
}}
/>
</TestProviders>
);
expect(wrapper.exists('[data-test-subj="field-browser"]')).toBeFalsy();
expect(wrapper.exists('[data-test-subj="test-header-action-cell"]')).toBeTruthy();
});
});
describe('Field Editor', () => {
test('Closes field editor when the timeline is unmounted', () => {
const mockCloseEditor = jest.fn();
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
editorActionsRef.current = { closeEditor: mockCloseEditor };
return {};
});
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent {...defaultProps} />
</TestProviders>
);
expect(mockCloseEditor).not.toHaveBeenCalled();
wrapper.unmount();
expect(mockCloseEditor).toHaveBeenCalled();
});
test('Closes field editor when the timeline is closed', () => {
const mockCloseEditor = jest.fn();
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
editorActionsRef.current = { closeEditor: mockCloseEditor };
return {};
});
const Proxy = (props: ColumnHeadersComponentProps) => (
<TestProviders>
<ColumnHeadersComponent {...props} />
</TestProviders>
);
const wrapper = mount(<Proxy {...defaultProps} />);
expect(mockCloseEditor).not.toHaveBeenCalled();
wrapper.setProps({ ...defaultProps, show: false });
expect(mockCloseEditor).toHaveBeenCalled();
});
});
});

View file

@ -1,316 +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, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import type { DraggableChildrenFn, DroppableProps } from '@hello-pangea/dnd';
import { Droppable } from '@hello-pangea/dnd';
import { useDispatch } from 'react-redux';
import type { ControlColumnProps, HeaderActionProps } from '../../../../../../common/types';
import { removeColumn, upsertColumn } from '../../../../store/actions';
import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper';
import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge';
import type { BrowserFields } from '../../../../../common/containers/source';
import {
DRAG_TYPE_FIELD,
droppableTimelineColumnsPrefix,
} from '../../../../../common/components/drag_and_drop/helpers';
import type { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline';
import type { OnSelectAll } from '../../events';
import {
EventsTh,
EventsThead,
EventsThGroupData,
EventsTrHeader,
EventsThGroupActions,
} from '../../styles';
import type { Sort } from '../sort';
import { ColumnHeader } from './column_header';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import type { FieldEditorActions } from '../../../fields_browser';
import { useFieldBrowserOptions } from '../../../fields_browser';
export interface ColumnHeadersComponentProps {
actionsColumnWidth: number;
browserFields: BrowserFields;
columnHeaders: ColumnHeaderOptions[];
isEventViewer?: boolean;
isSelectAllChecked: boolean;
onSelectAll: OnSelectAll;
show: boolean;
showEventsSelect: boolean;
showSelectAllCheckbox: boolean;
sort: Sort[];
tabType: TimelineTabs;
timelineId: string;
leadingControlColumns: ControlColumnProps[];
trailingControlColumns: ControlColumnProps[];
}
interface DraggableContainerProps {
children: React.ReactNode;
onMount: () => void;
onUnmount: () => void;
}
export const DraggableContainer = React.memo<DraggableContainerProps>(
({ children, onMount, onUnmount }) => {
useEffect(() => {
onMount();
return () => onUnmount();
}, [onMount, onUnmount]);
return <>{children}</>;
}
);
DraggableContainer.displayName = 'DraggableContainer';
export const isFullScreen = ({
globalFullScreen,
isActiveTimelines,
timelineFullScreen,
}: {
globalFullScreen: boolean;
isActiveTimelines: boolean;
timelineFullScreen: boolean;
}) =>
(isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen);
/** Renders the timeline header columns */
export const ColumnHeadersComponent = ({
actionsColumnWidth,
browserFields,
columnHeaders,
isEventViewer = false,
isSelectAllChecked,
onSelectAll,
show,
showEventsSelect,
showSelectAllCheckbox,
sort,
tabType,
timelineId,
leadingControlColumns,
trailingControlColumns,
}: ColumnHeadersComponentProps) => {
const dispatch = useDispatch();
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
const fieldEditorActionsRef = useRef<FieldEditorActions>(null);
useEffect(() => {
return () => {
if (fieldEditorActionsRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
fieldEditorActionsRef.current.closeEditor();
}
};
}, []);
useEffect(() => {
if (!show && fieldEditorActionsRef.current) {
fieldEditorActionsRef.current.closeEditor();
}
}, [show]);
const renderClone: DraggableChildrenFn = useCallback(
(dragProvided, _dragSnapshot, rubric) => {
const index = rubric.source.index;
const header = columnHeaders[index];
const onMount = () => setDraggingIndex(index);
const onUnmount = () => setDraggingIndex(null);
return (
<EventsTh
data-test-subj="draggable-header"
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
ref={dragProvided.innerRef}
>
<DraggableContainer onMount={onMount} onUnmount={onUnmount}>
<DragEffects>
<DraggableFieldBadge fieldId={header.id} fieldWidth={header.initialWidth} />
</DragEffects>
</DraggableContainer>
</EventsTh>
);
},
[columnHeaders, setDraggingIndex]
);
const ColumnHeaderList = useMemo(
() =>
columnHeaders.map((header, draggableIndex) => (
<ColumnHeader
key={header.id}
draggableIndex={draggableIndex}
timelineId={timelineId}
header={header}
isDragging={draggingIndex === draggableIndex}
sort={sort}
tabType={tabType}
/>
)),
[columnHeaders, timelineId, draggingIndex, sort, tabType]
);
const DroppableContent = useCallback<DroppableProps['children']>(
(dropProvided, snapshot) => (
<>
<EventsThGroupData
data-test-subj="headers-group"
ref={dropProvided.innerRef}
isDragging={snapshot.isDraggingOver}
{...dropProvided.droppableProps}
>
{ColumnHeaderList}
</EventsThGroupData>
</>
),
[ColumnHeaderList]
);
const leadingHeaderCells = useMemo(
() =>
leadingControlColumns ? leadingControlColumns.map((column) => column.headerCellRender) : [],
[leadingControlColumns]
);
const trailingHeaderCells = useMemo(
() =>
trailingControlColumns ? trailingControlColumns.map((column) => column.headerCellRender) : [],
[trailingControlColumns]
);
const fieldBrowserOptions = useFieldBrowserOptions({
sourcererScope: SourcererScopeName.timeline,
editorActionsRef: fieldEditorActionsRef,
upsertColumn: (column, index) => dispatch(upsertColumn({ column, id: timelineId, index })),
removeColumn: (columnId) => dispatch(removeColumn({ columnId, id: timelineId })),
});
const LeadingHeaderActions = useMemo(() => {
return leadingHeaderCells.map(
(Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => {
const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width;
const width = passedWidth ? passedWidth : actionsColumnWidth;
return (
<EventsThGroupActions
actionsColumnWidth={width}
data-test-subj="actions-container"
isEventViewer={isEventViewer}
key={index}
>
{Header && (
<Header
width={width}
browserFields={browserFields}
columnHeaders={columnHeaders}
isEventViewer={isEventViewer}
isSelectAllChecked={isSelectAllChecked}
onSelectAll={onSelectAll}
showEventsSelect={showEventsSelect}
showSelectAllCheckbox={showSelectAllCheckbox}
sort={sort}
tabType={tabType}
timelineId={timelineId}
fieldBrowserOptions={fieldBrowserOptions}
/>
)}
</EventsThGroupActions>
);
}
);
}, [
leadingHeaderCells,
leadingControlColumns,
actionsColumnWidth,
browserFields,
columnHeaders,
fieldBrowserOptions,
isEventViewer,
isSelectAllChecked,
onSelectAll,
showEventsSelect,
showSelectAllCheckbox,
sort,
tabType,
timelineId,
]);
const TrailingHeaderActions = useMemo(() => {
return trailingHeaderCells.map(
(Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => {
const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width;
const width = passedWidth ? passedWidth : actionsColumnWidth;
return (
<EventsThGroupActions
actionsColumnWidth={width}
data-test-subj="actions-container"
isEventViewer={isEventViewer}
key={index}
>
{Header && (
<Header
width={width}
browserFields={browserFields}
columnHeaders={columnHeaders}
isEventViewer={isEventViewer}
isSelectAllChecked={isSelectAllChecked}
onSelectAll={onSelectAll}
showEventsSelect={showEventsSelect}
showSelectAllCheckbox={showSelectAllCheckbox}
sort={sort}
tabType={tabType}
timelineId={timelineId}
fieldBrowserOptions={fieldBrowserOptions}
/>
)}
</EventsThGroupActions>
);
}
);
}, [
trailingHeaderCells,
trailingControlColumns,
actionsColumnWidth,
browserFields,
columnHeaders,
fieldBrowserOptions,
isEventViewer,
isSelectAllChecked,
onSelectAll,
showEventsSelect,
showSelectAllCheckbox,
sort,
tabType,
timelineId,
]);
return (
<EventsThead data-test-subj="column-headers">
<EventsTrHeader>
{LeadingHeaderActions}
<Droppable
direction={'horizontal'}
droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`}
isDropDisabled={false}
type={DRAG_TYPE_FIELD}
renderClone={renderClone}
>
{DroppableContent}
</Droppable>
{TrailingHeaderActions}
</EventsTrHeader>
</EventsThead>
);
};
export const ColumnHeaders = React.memo(ColumnHeadersComponent);

View file

@ -1,47 +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 { RangePicker } from '.';
import { Ranges } from './ranges';
describe('RangePicker', () => {
describe('rendering', () => {
test('it renders the ranges', () => {
const wrapper = mount(<RangePicker selected={'1 Week'} onRangeSelected={jest.fn()} />);
Ranges.forEach((range) => {
expect(wrapper.text()).toContain(range);
});
});
test('it selects the option specified by the "selected" prop', () => {
const selected = '1 Month';
const wrapper = mount(<RangePicker selected={selected} onRangeSelected={jest.fn()} />);
expect(wrapper.find('select').props().value).toBe(selected);
});
});
describe('#onRangeSelected', () => {
test('it invokes the onRangeSelected callback when a new range is selected', () => {
const oldSelection = '1 Week';
const newSelection = '1 Day';
const mockOnRangeSelected = jest.fn();
const wrapper = mount(
<RangePicker selected={oldSelection} onRangeSelected={mockOnRangeSelected} />
);
wrapper.find('select').simulate('change', { target: { value: newSelection } });
expect(mockOnRangeSelected).toBeCalledWith(newSelection);
});
});
});

View file

@ -1,51 +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 { EuiSelect } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import type { OnRangeSelected } from '../../../events';
import { Ranges } from './ranges';
interface Props {
selected: string;
onRangeSelected: OnRangeSelected;
}
export const rangePickerWidth = 120;
// TODO: Upgrade Eui library and use EuiSuperSelect
const SelectContainer = styled.div`
cursor: pointer;
width: ${rangePickerWidth}px;
`;
SelectContainer.displayName = 'SelectContainer';
/** Renders a time range picker for the MiniMap (e.g. 1 Day, 1 Week...) */
export const RangePicker = React.memo<Props>(({ selected, onRangeSelected }) => {
const onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
onRangeSelected(event.target.value);
};
return (
<SelectContainer>
<EuiSelect
data-test-subj="rangePicker"
value={selected}
options={Ranges.map((range) => ({
text: range,
}))}
onChange={onChange}
/>
</SelectContainer>
);
});
RangePicker.displayName = 'RangePicker';

View file

@ -1,11 +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 * as i18n from './translations';
/** Enables runtime enumeration of valid `Range`s */
export const Ranges: string[] = [i18n.ONE_DAY, i18n.ONE_WEEK, i18n.ONE_MONTH, i18n.ONE_YEAR];

View file

@ -1,24 +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 ONE_DAY = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneDay', {
defaultMessage: '1 Day',
});
export const ONE_WEEK = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneWeek', {
defaultMessage: '1 Week',
});
export const ONE_MONTH = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneMonth', {
defaultMessage: '1 Month',
});
export const ONE_YEAR = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneYear', {
defaultMessage: '1 Year',
});

View file

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextFilter rendering renders correctly against snapshot 1`] = `
<FieldText
data-test-subj="textFilter"
minwidth="100px"
onChange={[Function]}
placeholder="Filter"
value=""
/>
`;

View file

@ -1,79 +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 { DEFAULT_PLACEHOLDER, TextFilter } from '.';
describe('TextFilter', () => {
describe('rendering', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(<TextFilter columnId="foo" minWidth={100} />);
expect(wrapper).toMatchSnapshot();
});
describe('placeholder', () => {
test('it renders the default placeholder when no filter is specified, and a placeholder is NOT provided', () => {
const wrapper = mount(<TextFilter columnId="foo" minWidth={100} />);
expect(wrapper.find(`input[placeholder="${DEFAULT_PLACEHOLDER}"]`).exists()).toEqual(true);
});
test('it renders the default placeholder when no filter is specified, a placeholder is provided', () => {
const placeholder = 'Make a jazz noise here';
const wrapper = mount(
<TextFilter columnId="foo" placeholder={placeholder} minWidth={100} />
);
expect(wrapper.find(`input[placeholder="${placeholder}"]`).exists()).toEqual(true);
});
});
describe('minWidth', () => {
test('it applies the value of the minwidth prop to the input', () => {
const minWidth = 150;
const wrapper = mount(<TextFilter columnId="foo" minWidth={minWidth} />);
expect(wrapper.find('input').props()).toHaveProperty('minwidth', `${minWidth}px`);
});
});
describe('value', () => {
test('it renders the value of the filter prop', () => {
const filter = 'out the noise';
const wrapper = mount(<TextFilter columnId="foo" filter={filter} minWidth={100} />);
expect(wrapper.find('input').prop('value')).toEqual(filter);
});
});
describe('#onFilterChange', () => {
test('it invokes the onFilterChange callback when the input is updated', () => {
const columnId = 'foo';
const oldFilter = 'abcdef';
const newFilter = `${oldFilter}g`;
const onFilterChange = jest.fn();
const wrapper = mount(
<TextFilter
columnId={columnId}
filter={oldFilter}
minWidth={100}
onFilterChange={onFilterChange}
/>
);
wrapper.find('input').simulate('change', { target: { value: newFilter } });
expect(onFilterChange).toBeCalledWith({
columnId,
filter: newFilter,
});
});
});
});
});

View file

@ -1,57 +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 { EuiFieldText } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import type { OnFilterChange } from '../../../events';
import type { ColumnId } from '../../column_id';
interface Props {
columnId: ColumnId;
filter?: string;
minWidth: number;
onFilterChange?: OnFilterChange;
placeholder?: string;
}
export const DEFAULT_PLACEHOLDER = 'Filter';
const FieldText = styled(EuiFieldText)<{ minwidth: string }>`
min-width: ${(props) => props.minwidth};
`;
FieldText.displayName = 'FieldText';
/** Renders a text-based column filter */
export const TextFilter = React.memo<Props>(
({
columnId,
minWidth,
filter = '',
onFilterChange = noop,
placeholder = DEFAULT_PLACEHOLDER,
}) => {
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
onFilterChange({ columnId, filter: event.target.value });
};
return (
<FieldText
data-test-subj="textFilter"
minwidth={`${minWidth}px`}
placeholder={placeholder}
value={filter}
onChange={onChange}
/>
);
}
);
TextFilter.displayName = 'TextFilter';

View file

@ -1,93 +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 { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
import { mockTimelineData } from '../../../../../common/mock';
import { defaultHeaders } from '../column_headers/default_headers';
import { getDefaultControlColumn } from '../control_columns';
import { DataDrivenColumns, getMappedNonEcsValue } from '.';
describe('Columns', () => {
const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp');
const ACTION_BUTTON_COUNT = 4;
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT);
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}
notesCount={0}
renderCellValue={DefaultCellRenderer}
timelineId="test"
columnValues={'abc def'}
showCheckboxes={false}
selectedEventIds={{}}
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
onRowSelected={jest.fn()}
showNotes={false}
isEventPinned={false}
toggleShowNotes={jest.fn()}
refetch={jest.fn()}
eventIdToNoteIds={{}}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
setEventsLoading={jest.fn()}
setEventsDeleted={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});
describe('getMappedNonEcsValue', () => {
const existingField = 'Descarte';
const existingValue = ['IThinkThereforeIAm'];
test('should return the value if the fieldName is found', () => {
const result = getMappedNonEcsValue({
data: [{ field: existingField, value: existingValue }],
fieldName: existingField,
});
expect(result).toBe(existingValue);
});
test('should return undefined if the value cannot be found in the array', () => {
const result = getMappedNonEcsValue({
data: [{ field: existingField, value: existingValue }],
fieldName: 'nonExistent',
});
expect(result).toBeUndefined();
});
test('should return undefined when data is an empty array', () => {
const result = getMappedNonEcsValue({ data: [], fieldName: existingField });
expect(result).toBeUndefined();
});
test('should return undefined when data is undefined', () => {
const result = getMappedNonEcsValue({ data: undefined, fieldName: existingField });
expect(result).toBeUndefined();
});
});
});

View file

@ -1,472 +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 { 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 type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type {
SetEventsDeleted,
SetEventsLoading,
ActionProps,
ControlColumnProps,
RowCellRender,
} from '../../../../../../common/types';
import type {
CellValueElementProps,
ColumnHeaderOptions,
TimelineTabs,
} from '../../../../../../common/types/timeline';
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
import type { OnRowSelected } from '../../events';
import type { inputsModel } from '../../../../../common/store';
import {
EventsTd,
EVENTS_TD_CLASS_NAME,
EventsTdContent,
EventsTdGroupData,
EventsTdGroupActions,
} from '../../styles';
import { StatefulCell } from './stateful_cell';
import * as i18n from './translations';
interface CellProps {
_id: string;
ariaRowindex: number;
index: number;
header: ColumnHeaderOptions;
data: TimelineNonEcsData[];
ecsData: Ecs;
hasRowRenderers: boolean;
notesCount: number;
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;
eventIdToNoteIds: Readonly<Record<string, string[]>>;
isEventPinned: boolean;
isEventViewer?: boolean;
loadingEventIds: Readonly<string[]>;
notesCount: number;
onEventDetailsPanelOpened: () => void;
onRowSelected: OnRowSelected;
refetch: inputsModel.Refetch;
onRuleChange?: () => void;
hasRowRenderers: boolean;
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
showCheckboxes: boolean;
showNotes: boolean;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
tabType?: TimelineTabs;
timelineId: string;
toggleShowNotes: () => void;
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,
notesCount,
onEventDetailsPanelOpened,
onRowSelected,
refetch,
rowIndex,
hasRowRenderers,
onRuleChange,
selectedEventIds,
showCheckboxes,
showNotes,
tabType,
timelineId,
toggleShowNotes,
setEventsLoading,
setEventsDeleted,
}: ActionProps & {
columnId: string;
hasRowRenderers: boolean;
actionsColumnWidth: number;
notesCount: 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}
refetch={refetch}
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}
{notesCount ? (
<EuiScreenReaderOnly data-test-subj="hasNotesScreenReaderOnly">
<p>{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}</p>
</EuiScreenReaderOnly>
) : null}
</EventsTd>
</EventsTdGroupActions>
);
};
const TgridTdCell = ({
_id,
ariaRowindex,
index,
header,
data,
ecsData,
hasRowRenderers,
notesCount,
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}
{notesCount ? (
<EuiScreenReaderOnly data-test-subj="hasNotesScreenReaderOnly">
<p>{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}</p>
</EuiScreenReaderOnly>
) : null}
</EventsTd>
);
};
export const DataDrivenColumns = React.memo<DataDrivenColumnProps>(
({
ariaRowindex,
actionsColumnWidth,
columnHeaders,
columnValues,
data,
ecsData,
eventIdToNoteIds,
isEventPinned,
isEventViewer,
id: _id,
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
onRowSelected,
refetch,
hasRowRenderers,
onRuleChange,
renderCellValue,
selectedEventIds,
showCheckboxes,
showNotes,
tabType,
timelineId,
toggleShowNotes,
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}
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
notesCount={notesCount}
refetch={refetch}
hasRowRenderers={hasRowRenderers}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showNotes={showNotes}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={toggleShowNotes}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
/>
)
);
}),
[
trailingControlColumns,
_id,
data,
ecsData,
onRowSelected,
isEventPinned,
isEventViewer,
actionsColumnWidth,
ariaRowindex,
columnValues,
eventIdToNoteIds,
hasRowRenderers,
leadingAndDataColumnCount,
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
onRuleChange,
refetch,
selectedEventIds,
showCheckboxes,
showNotes,
tabType,
timelineId,
toggleShowNotes,
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}
notesCount={notesCount}
renderCellValue={renderCellValue}
tabType={tabType}
timelineId={timelineId}
/>
)),
[
_id,
ariaRowindex,
columnHeaders,
data,
ecsData,
hasRowRenderers,
notesCount,
renderCellValue,
tabType,
timelineId,
]
);
return (
<EventsTdGroupData data-test-subj="data-driven-columns">
{ColumnHeaders}
{TrailingActions}
</EventsTdGroupData>
);
}
);
DataDrivenColumns.displayName = 'DataDrivenColumns';
export const getMappedNonEcsValue = ({
data,
fieldName,
}: {
data?: TimelineNonEcsData[];
fieldName: string;
}): string[] | undefined => {
/*
While data _should_ always be defined
There is the potential for race conditions where a component using this function
is still visible in the UI, while the data has since been removed.
To cover all scenarios where this happens we'll check for the presence of data here
*/
if (!data || data.length === 0) return undefined;
const item = data.find((d) => d.field === fieldName);
if (item != null && item.value != null) {
return item.value;
}
return undefined;
};
export const useGetMappedNonEcsValue = ({
data,
fieldName,
}: {
data?: TimelineNonEcsData[];
fieldName: string;
}): string[] | undefined => {
return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]);
};

View file

@ -1,171 +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 { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
import type {
ColumnHeaderOptions,
CellValueElementProps,
} from '../../../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { StatefulCell } from './stateful_cell';
import { useGetMappedNonEcsValue } from '.';
/**
* 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 }) => {
const value = useGetMappedNonEcsValue({
data,
fieldName: columnId,
});
useEffect(() => {
// branching logic that conditionally renders a specific cell green:
if (columnId === defaultHeaders[0].id) {
if (value?.length) {
setCellProps({
style: {
backgroundColor: 'green',
},
});
}
}
}, [columnId, data, setCellProps, value]);
return <div data-test-subj="renderCellValue">{value}</div>;
};
describe('StatefulCell', () => {
const rowIndex = 123;
const colIndex = 0;
const eventId = '_id-123';
const linkValues = ['foo', 'bar', '@baz'];
const timelineId = 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,
scopeId: timelineId,
})
);
});
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,
scopeId: 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;');
});
});

View file

@ -1,71 +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 type { HTMLAttributes } from 'react';
import React, { useState } from 'react';
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
import type {
ColumnHeaderOptions,
CellValueElementProps,
TimelineTabs,
} 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,
isTimeline: true,
linkValues,
rowIndex,
colIndex,
setCellProps,
scopeId: timelineId,
key: tabType != null ? `${timelineId}-${tabType}` : timelineId,
})}
</div>
);
};
StatefulCellComponent.displayName = 'StatefulCellComponent';
export const StatefulCell = React.memo(StatefulCellComponent);

View file

@ -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.securitySolution.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.securitySolution.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.securitySolution.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.',
});

View file

@ -1,164 +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, type ComponentType as EnzymeComponentType } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../../common/mock';
import { EventColumnView } from './event_column_view';
import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { TimelineTypeEnum } from '../../../../../../common/api/timeline';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { getDefaultControlColumn } from '../control_columns';
import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { getActionsColumnWidth } from '../../../../../common/components/header_actions';
jest.mock('../../../../../common/components/header_actions/add_note_icon_item', () => {
return {
AddEventNoteAction: jest.fn(() => <div data-test-subj="add-note-button-mock" />),
};
});
jest.mock('../../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
useDeepEqualSelector: jest.fn(),
}));
jest.mock('../../../../../common/components/user_privileges', () => {
return {
useUserPrivileges: () => ({
listPrivileges: { loading: false, error: undefined, result: undefined },
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
endpointPrivileges: {},
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
}),
};
});
jest.mock('../../../../../common/components/guided_onboarding_tour/tour_step');
jest.mock('../../../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
return {
...originalModule,
useKibana: () => ({
services: {
timelines: { ...mockTimelines },
data: {
search: jest.fn(),
query: jest.fn(),
},
application: {
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
},
},
cases: mockCasesContract(),
},
}),
useNavigateTo: () => ({
navigateTo: jest.fn(),
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
remove: jest.fn(),
}),
};
});
describe('EventColumnView', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineTypeEnum.default);
const ACTION_BUTTON_COUNT = 4;
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT);
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(),
onRowSelected: jest.fn(),
refetch: jest.fn(),
renderCellValue: DefaultCellRenderer,
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(),
};
test('it does NOT render a notes button when isEventsViewer is true', () => {
const wrapper = mount(<EventColumnView {...props} isEventViewer={true} />, {
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
});
expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false);
});
test('it does NOT render a notes button when showNotes is false', () => {
const wrapper = mount(<EventColumnView {...props} showNotes={false} />, {
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
});
expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false);
});
test('it does NOT render a pin button when isEventViewer is true', () => {
const wrapper = mount(<EventColumnView {...props} isEventViewer={true} />, {
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
});
expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false);
});
test('it renders a custom control column in addition to the default control column', () => {
const wrapper = mount(
<EventColumnView
{...props}
timelineId={TimelineId.test}
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
/>,
{
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
}
);
expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy();
});
});

View file

@ -1,225 +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 { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type {
ControlColumnProps,
RowCellRender,
SetEventsDeleted,
SetEventsLoading,
} from '../../../../../../common/types';
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
import type { OnRowSelected } from '../../events';
import { EventsTrData, EventsTdGroupActions } from '../../styles';
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
import type { inputsModel } from '../../../../../common/store';
import type {
ColumnHeaderOptions,
CellValueElementProps,
TimelineTabs,
} from '../../../../../../common/types/timeline';
interface Props {
id: string;
actionsColumnWidth: number;
ariaRowindex: number;
columnHeaders: ColumnHeaderOptions[];
data: TimelineNonEcsData[];
ecsData: Ecs;
eventIdToNoteIds: Readonly<Record<string, string[]>>;
isEventPinned: boolean;
isEventViewer?: boolean;
loadingEventIds: Readonly<string[]>;
notesCount: number;
onEventDetailsPanelOpened: () => void;
onRowSelected: OnRowSelected;
refetch: inputsModel.Refetch;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
onRuleChange?: () => void;
hasRowRenderers: boolean;
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
showCheckboxes: boolean;
showNotes: boolean;
tabType?: TimelineTabs;
timelineId: string;
toggleShowNotes: (eventId?: string) => void;
leadingControlColumns: ControlColumnProps[];
trailingControlColumns: ControlColumnProps[];
setEventsLoading: SetEventsLoading;
setEventsDeleted: SetEventsDeleted;
}
export const EventColumnView = React.memo<Props>(
({
id,
actionsColumnWidth,
ariaRowindex,
columnHeaders,
data,
ecsData,
eventIdToNoteIds,
isEventPinned = false,
isEventViewer = false,
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
onRowSelected,
refetch,
hasRowRenderers,
onRuleChange,
renderCellValue,
selectedEventIds,
showCheckboxes,
showNotes,
tabType,
timelineId,
toggleShowNotes,
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}
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
onRuleChange={onRuleChange}
refetch={refetch}
showNotes={showNotes}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={toggleShowNotes}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
disablePinAction={false}
/>
)}
</EventsTdGroupActions>
);
}),
[
actionsColumnWidth,
ariaRowindex,
columnValues,
data,
ecsData,
eventIdToNoteIds,
id,
isEventPinned,
isEventViewer,
leadingActionCells,
leadingControlColumns,
loadingEventIds,
onEventDetailsPanelOpened,
onRowSelected,
onRuleChange,
refetch,
selectedEventIds,
showCheckboxes,
tabType,
timelineId,
toggleShowNotes,
setEventsLoading,
setEventsDeleted,
showNotes,
]
);
return (
<EventsTrData data-test-subj="event-column-view">
{LeadingActions}
<DataDrivenColumns
id={id}
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={ariaRowindex}
columnHeaders={columnHeaders}
data={data}
ecsData={ecsData}
hasRowRenderers={hasRowRenderers}
notesCount={notesCount}
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}
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
refetch={refetch}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showNotes={showNotes}
toggleShowNotes={toggleShowNotes}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
/>
</EventsTrData>
);
}
);
EventColumnView.displayName = 'EventColumnView';

View file

@ -1,111 +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 type { ControlColumnProps } from '../../../../../../common/types';
import type { inputsModel } from '../../../../../common/store';
import type {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
import type {
ColumnHeaderOptions,
CellValueElementProps,
RowRenderer,
TimelineTabs,
} from '../../../../../../common/types/timeline';
import type { OnRowSelected } from '../../events';
import { EventsTbody } from '../../styles';
import { StatefulEvent } from './stateful_event';
import { eventIsPinned } from '../helpers';
/** 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;
columnHeaders: ColumnHeaderOptions[];
containerRef: React.MutableRefObject<HTMLDivElement | null>;
data: TimelineItem[];
eventIdToNoteIds: Readonly<Record<string, string[]>>;
id: string;
isEventViewer?: boolean;
lastFocusedAriaColindex: number;
loadingEventIds: Readonly<string[]>;
onRowSelected: OnRowSelected;
pinnedEventIds: Readonly<Record<string, boolean>>;
refetch: inputsModel.Refetch;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
onRuleChange?: () => void;
rowRenderers: RowRenderer[];
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
showCheckboxes: boolean;
tabType?: TimelineTabs;
leadingControlColumns: ControlColumnProps[];
trailingControlColumns: ControlColumnProps[];
onToggleShowNotes?: (eventId?: string) => void;
}
const EventsComponent: React.FC<Props> = ({
actionsColumnWidth,
columnHeaders,
containerRef,
data,
eventIdToNoteIds,
id,
isEventViewer = false,
lastFocusedAriaColindex,
loadingEventIds,
onRowSelected,
pinnedEventIds,
refetch,
onRuleChange,
renderCellValue,
rowRenderers,
selectedEventIds,
showCheckboxes,
tabType,
leadingControlColumns,
trailingControlColumns,
onToggleShowNotes,
}) => (
<EventsTbody data-test-subj="events">
{data.map((event, i) => (
<StatefulEvent
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={i + ARIA_ROW_INDEX_OFFSET}
columnHeaders={columnHeaders}
containerRef={containerRef}
event={event}
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })}
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}
refetch={refetch}
rowRenderers={rowRenderers}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
tabType={tabType}
timelineId={id}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
onToggleShowNotes={onToggleShowNotes}
/>
))}
</EventsTbody>
);
export const Events = React.memo(EventsComponent);

View file

@ -1,266 +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 type { PropsWithChildren } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { useKibana } from '../../../../../common/lib/kibana';
import type {
ColumnHeaderOptions,
CellValueElementProps,
RowRenderer,
TimelineTabs,
} from '../../../../../../common/types/timeline';
import type {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
import type { OnRowSelected } from '../../events';
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles';
import { getEventType, isEvenEqlSequence } from '../helpers';
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
import { EventColumnView } from './event_column_view';
import type { inputsModel } from '../../../../../common/store';
import { appSelectors } from '../../../../../common/store';
import { timelineActions } from '../../../../store';
import type { TimelineResultNote } from '../../../open_timeline/types';
import { getRowRenderer } from '../renderers/get_row_renderer';
import { StatefulRowRenderer } from './stateful_row_renderer';
import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import type {
ControlColumnProps,
SetEventsDeleted,
SetEventsLoading,
} from '../../../../../../common/types';
interface Props {
actionsColumnWidth: number;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
columnHeaders: ColumnHeaderOptions[];
event: TimelineItem;
eventIdToNoteIds: Readonly<Record<string, string[]>>;
isEventViewer?: boolean;
lastFocusedAriaColindex: number;
loadingEventIds: Readonly<string[]>;
onRowSelected: OnRowSelected;
isEventPinned: boolean;
refetch: inputsModel.Refetch;
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[];
onToggleShowNotes?: (eventId?: string) => void;
}
const emptyNotes: string[] = [];
const EventsTrSupplementContainerWrapper = React.memo<PropsWithChildren<unknown>>(
({ children }) => {
const width = useEventDetailsWidthContext();
return <EventsTrSupplementContainer width={width}>{children}</EventsTrSupplementContainer>;
}
);
EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper';
const StatefulEventComponent: React.FC<Props> = ({
actionsColumnWidth,
containerRef,
columnHeaders,
event,
eventIdToNoteIds,
isEventViewer = false,
isEventPinned = false,
lastFocusedAriaColindex,
loadingEventIds,
onRowSelected,
refetch,
renderCellValue,
rowRenderers,
onRuleChange,
ariaRowindex,
selectedEventIds,
showCheckboxes,
tabType,
timelineId,
leadingControlColumns,
trailingControlColumns,
onToggleShowNotes,
}) => {
const { telemetry } = useKibana().services;
const trGroupRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
const { openFlyout } = useExpandableFlyoutApi();
// 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,
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
tabType,
});
const [, setFocusedNotes] = useState<{ [eventId: string]: boolean }>({});
const eventId = event._id;
const isDetailPanelExpanded: boolean = false;
const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
const notesById = useDeepEqualSelector(getNotesByIds);
const noteIds: string[] = eventIdToNoteIds[eventId] || emptyNotes;
const notes: TimelineResultNote[] = useMemo(
() =>
appSelectors.getNotes(notesById, noteIds).map((note) => ({
savedObjectId: note.saveObjectId,
note: note.note,
noteId: note.id,
updated: (note.lastEdit ?? note.created).getTime(),
updatedBy: note.user,
})),
[notesById, noteIds]
);
const hasRowRenderers: boolean = useMemo(
() => getRowRenderer({ data: event.ecs, rowRenderers }) != null,
[event.ecs, rowRenderers]
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const indexName = event._index!;
const onToggleShowNotesHandler = useCallback(
(currentEventId?: string) => {
onToggleShowNotes?.(currentEventId);
setFocusedNotes((prevShowNotes) => {
if (prevShowNotes[eventId]) {
// notes are closing, so focus the notes button on the next tick, after escaping the EuiFocusTrap
setTimeout(() => {
const notesButtonElement = trGroupRef.current?.querySelector<HTMLButtonElement>(
`.${NOTES_BUTTON_CLASS_NAME}`
);
notesButtonElement?.focus();
}, 0);
}
return { ...prevShowNotes, [eventId]: !prevShowNotes[eventId] };
});
},
[onToggleShowNotes, eventId]
);
const handleOnEventDetailPanelOpened = useCallback(() => {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
}, [eventId, indexName, openFlyout, timelineId, telemetry]);
const setEventsLoading = useCallback<SetEventsLoading>(
({ eventIds, isLoading }) => {
dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading }));
},
[dispatch, timelineId]
);
const setEventsDeleted = useCallback<SetEventsDeleted>(
({ eventIds, isDeleted }) => {
dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted }));
},
[dispatch, 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={eventId}
actionsColumnWidth={actionsColumnWidth}
ariaRowindex={ariaRowindex}
columnHeaders={columnHeaders}
data={event.data}
ecsData={event.ecs}
eventIdToNoteIds={eventIdToNoteIds}
hasRowRenderers={hasRowRenderers}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
loadingEventIds={loadingEventIds}
notesCount={notes.length}
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
onRowSelected={onRowSelected}
refetch={refetch}
renderCellValue={renderCellValue}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={showCheckboxes}
showNotes={true}
tabType={tabType}
timelineId={timelineId}
toggleShowNotes={onToggleShowNotesHandler}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
/>
<EventsTrSupplementContainerWrapper>
<EuiFlexGroup gutterSize="none" justifyContent="center">
<EuiFlexItem grow={false}>
<EventsTrSupplement>
<StatefulRowRenderer
ariaRowindex={ariaRowindex}
containerRef={containerRef}
event={event}
lastFocusedAriaColindex={lastFocusedAriaColindex}
rowRenderers={rowRenderers}
timelineId={timelineId}
/>
</EventsTrSupplement>
</EuiFlexItem>
</EuiFlexGroup>
</EventsTrSupplementContainerWrapper>
</EventsTrGroup>
</StatefulEventContext.Provider>
);
};
export const StatefulEvent = React.memo(StatefulEventComponent);

View file

@ -1,407 +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 { mount, type ComponentType as EnzymeComponentType } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useKibana, useCurrentUser } from '../../../../common/lib/kibana';
import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer';
import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { Direction } from '../../../../../common/search_strategy';
import {
defaultHeaders,
mockGlobalState,
mockTimelineData,
createMockStore,
TestProviders,
} from '../../../../common/mock';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import type { Props } from '.';
import { StatefulBody } from '.';
import type { Sort } from './sort';
import { getDefaultControlColumn } from './control_columns';
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
import type { State } from '../../../../common/store';
import type { UseFieldBrowserOptionsProps } from '../../fields_browser';
import type {
DraggableProvided,
DraggableStateSnapshot,
DroppableProvided,
DroppableStateSnapshot,
} from '@hello-pangea/dnd';
import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys';
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
import { createExpandableFlyoutApiMock } from '../../../../common/mock/expandable_flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('../../../../common/components/guided_onboarding_tour/tour_step');
jest.mock(
'../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'
);
jest.mock('../../../../common/hooks/use_upselling', () => ({
useUpsellingMessage: jest.fn(),
}));
jest.mock('../../../../common/components/user_privileges', () => {
return {
useUserPrivileges: () => ({
listPrivileges: { loading: false, error: undefined, result: undefined },
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
endpointPrivileges: {},
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
}),
};
});
const mockUseFieldBrowserOptions = jest.fn();
const mockUseKibana = useKibana as jest.Mock;
const mockUseCurrentUser = useCurrentUser as jest.Mock<Partial<ReturnType<typeof useCurrentUser>>>;
const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks');
jest.mock('../../fields_browser', () => ({
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),
}));
const useAddToTimeline = () => ({
beginDrag: jest.fn(),
cancelDrag: jest.fn(),
dragToLocation: jest.fn(),
endDrag: jest.fn(),
hasDraggableLock: jest.fn(),
startDragToTimeline: jest.fn(),
});
jest.mock('../../../../common/lib/kibana');
const mockSort: Sort[] = [
{
columnId: '@timestamp',
columnType: 'date',
esTypes: ['date'],
sortDirection: Direction.desc,
},
];
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
const mockOpenFlyout = jest.fn();
jest.mock('@kbn/expandable-flyout');
const mockedTelemetry = createTelemetryServiceMock();
jest.mock('../../../../common/components/link_to', () => {
const originalModule = jest.requireActual('../../../../common/components/link_to');
return {
...originalModule,
useGetSecuritySolutionUrl: () =>
jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`),
useNavigateTo: () => {
return { navigateTo: jest.fn() };
},
useAppUrl: () => {
return { getAppUrl: jest.fn() };
},
};
});
jest.mock('../../../../common/components/links', () => {
const originalModule = jest.requireActual('../../../../common/components/links');
return {
...originalModule,
useGetSecuritySolutionUrl: () =>
jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`),
useNavigateTo: () => {
return { navigateTo: jest.fn() };
},
useAppUrl: () => {
return { getAppUrl: jest.fn() };
},
};
});
// Prevent Resolver from rendering
jest.mock('../../graph_overlay');
jest.mock('../../fields_browser/create_field_button', () => ({
useCreateFieldButton: () => <></>,
}));
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiScreenReaderOnly: () => <></>,
};
});
jest.mock('suricata-sid-db', () => {
return {
db: [],
};
});
jest.mock(
'../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions',
() => {
return {
useAddToCaseActions: () => {
return {
addToCaseActionItems: [],
};
},
};
}
);
jest.mock('@hello-pangea/dnd', () => ({
Droppable: ({
children,
}: {
children: (a: DroppableProvided, b: DroppableStateSnapshot) => void;
}) =>
children(
{
droppableProps: {
'data-rfd-droppable-context-id': '123',
'data-rfd-droppable-id': '123',
},
innerRef: jest.fn(),
placeholder: null,
},
{
isDraggingOver: false,
draggingOverWith: null,
draggingFromThisWith: null,
isUsingPlaceholder: false,
}
),
Draggable: ({
children,
}: {
children: (a: DraggableProvided, b: DraggableStateSnapshot) => void;
}) =>
children(
{
draggableProps: {
'data-rfd-draggable-context-id': '123',
'data-rfd-draggable-id': '123',
},
innerRef: jest.fn(),
dragHandleProps: null,
},
{
isDragging: false,
isDropAnimating: false,
isClone: false,
dropAnimation: null,
draggingOver: null,
combineWith: null,
combineTargetFor: null,
mode: null,
}
),
DragDropContext: ({ children }: { children: React.ReactNode }) => children,
}));
describe('Body', () => {
const getWrapper = async (
childrenComponent: JSX.Element,
store?: { store: ReturnType<typeof createMockStore> }
) => {
const wrapper = mount(childrenComponent, {
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
wrappingComponentProps: store ?? {},
});
await waitFor(() => wrapper.find('[data-test-subj="suricataRefs"]').exists());
return wrapper;
};
const mockRefetch = jest.fn();
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
beforeEach(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue({
...createExpandableFlyoutApiMock(),
openFlyout: mockOpenFlyout,
});
mockUseCurrentUser.mockReturnValue({ username: 'test-username' });
mockUseKibana.mockReturnValue({
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
},
},
cases: mockCasesContract.mockCasesContract(),
data: {
search: jest.fn(),
query: jest.fn(),
dataViews: jest.fn(),
},
uiSettings: {
get: jest.fn(),
},
savedObjects: {
client: {},
},
telemetry: mockedTelemetry,
timelines: {
getLastUpdated: jest.fn(),
getLoadingPanel: jest.fn(),
getFieldBrowser: jest.fn(),
getUseAddToTimeline: () => useAddToTimeline,
},
},
useNavigateTo: jest.fn().mockReturnValue({
navigateTo: jest.fn(),
}),
});
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
const ACTION_BUTTON_COUNT = 4;
const props: Props = {
activePage: 0,
browserFields: mockBrowserFields,
data: [mockTimelineData[0]],
id: TimelineId.test,
refetch: mockRefetch,
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
sort: mockSort,
tabType: TimelineTabs.query,
totalPages: 1,
leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT),
trailingControlColumns: [],
};
describe('rendering', () => {
beforeEach(() => {
mockDispatch.mockClear();
});
test('it renders the column headers', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} />);
expect(wrapper.find('[data-test-subj="column-headers"]').first().exists()).toEqual(true);
});
test('it renders the scroll container', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} />);
expect(wrapper.find('[data-test-subj="timeline-body"]').first().exists()).toEqual(true);
});
test('it renders events', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} />);
expect(wrapper.find('[data-test-subj="events"]').first().exists()).toEqual(true);
});
test('it renders a tooltip for timestamp', async () => {
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
const state: State = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
...mockGlobalState.timeline.timelineById,
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
id: TimelineId.test,
columns: headersJustTimestamp,
},
},
},
};
const store = createMockStore(state);
const wrapper = await getWrapper(<StatefulBody {...props} />, { store });
headersJustTimestamp.forEach(() => {
expect(
wrapper
.find('[data-test-subj="data-driven-columns"]')
.first()
.find('[data-test-subj="localized-date-tool-tip"]')
.exists()
).toEqual(true);
});
});
});
describe('event details', () => {
beforeEach(() => {
mockDispatch.mockReset();
});
test('open the expandable flyout to show event details for query tab', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} />);
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
wrapper.update();
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockOpenFlyout).toHaveBeenCalledWith({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: '1',
indexName: undefined,
scopeId: 'timeline-test',
},
},
});
});
test('open the expandable flyout to show event details for pinned tab', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} tabType={TimelineTabs.pinned} />);
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
wrapper.update();
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockOpenFlyout).toHaveBeenCalledWith({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: '1',
indexName: undefined,
scopeId: 'timeline-test',
},
},
});
});
test('open the expandable flyout to show event details for notes tab', async () => {
const wrapper = await getWrapper(<StatefulBody {...props} tabType={TimelineTabs.notes} />);
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
wrapper.update();
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockOpenFlyout).toHaveBeenCalledWith({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: '1',
indexName: undefined,
scopeId: 'timeline-test',
},
},
});
});
});
});

View file

@ -1,271 +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 { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
FIRST_ARIA_INDEX,
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '@kbn/timelines-plugin/public';
import { getActionsColumnWidth } from '../../../../common/components/header_actions';
import type { ControlColumnProps } from '../../../../../common/types';
import type { CellValueElementProps } from '../cell_rendering';
import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
import type { RowRenderer, TimelineTabs } from '../../../../../common/types/timeline';
import { RowRendererCount } from '../../../../../common/api/timeline';
import type { BrowserFields } from '../../../../common/containers/source';
import type { TimelineItem } from '../../../../../common/search_strategy/timeline';
import type { inputsModel, State } from '../../../../common/store';
import { timelineActions } from '../../../store';
import type { OnRowSelected, OnSelectAll } from '../events';
import { getColumnHeaders } from './column_headers/helpers';
import { getEventIdToDataMapping } from './helpers';
import type { Sort } from './sort';
import { plainRowRenderer } from './renderers/plain_row_renderer';
import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
import { ColumnHeaders } from './column_headers';
import { Events } from './events';
import { useLicense } from '../../../../common/hooks/use_license';
import { selectTimelineById } from '../../../store/selectors';
export interface Props {
activePage: number;
browserFields: BrowserFields;
data: TimelineItem[];
id: string;
isEventViewer?: boolean;
sort: Sort[];
refetch: inputsModel.Refetch;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
leadingControlColumns: ControlColumnProps[];
trailingControlColumns: ControlColumnProps[];
tabType: TimelineTabs;
totalPages: number;
onRuleChange?: () => void;
onToggleShowNotes?: (eventId?: string) => void;
}
/**
* The Body component is used everywhere timeline is used within the security application. It is the highest level component
* that is shared across all implementations of the timeline.
*/
export const StatefulBody = React.memo<Props>(
({
activePage,
browserFields,
data,
id,
isEventViewer = false,
onRuleChange,
refetch,
renderCellValue,
rowRenderers,
sort,
tabType,
totalPages,
leadingControlColumns = [],
trailingControlColumns = [],
onToggleShowNotes,
}) => {
const dispatch = useDispatch();
const containerRef = useRef<HTMLDivElement | null>(null);
const {
columns,
eventIdToNoteIds,
excludedRowRendererIds,
isSelectAllChecked,
loadingEventIds,
pinnedEventIds,
selectedEventIds,
show,
queryFields,
selectAll,
} = useSelector((state: State) => selectTimelineById(state, id));
const columnHeaders = useMemo(
() => getColumnHeaders(columns, browserFields),
[browserFields, columns]
);
const isEnterprisePlus = useLicense().isEnterprise();
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
dispatch(
timelineActions.setSelected({
id,
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
isSelected,
isSelectAllChecked:
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
})
);
},
[data, dispatch, id, queryFields, selectedEventIds]
);
const onSelectAll: OnSelectAll = useCallback(
({ isSelected }: { isSelected: boolean }) =>
isSelected
? dispatch(
timelineActions.setSelected({
id,
eventIds: getEventIdToDataMapping(
data,
data.map((event) => event._id),
queryFields
),
isSelected,
isSelectAllChecked: isSelected,
})
)
: dispatch(timelineActions.clearSelected({ id })),
[data, dispatch, id, queryFields]
);
// Sync to selectAll so parent components can select all events
useEffect(() => {
if (selectAll && !isSelectAllChecked) {
onSelectAll({ isSelected: true });
}
}, [isSelectAllChecked, onSelectAll, selectAll]);
const enabledRowRenderers = useMemo(() => {
if (excludedRowRendererIds && excludedRowRendererIds.length === RowRendererCount)
return [plainRowRenderer];
if (!excludedRowRendererIds) return rowRenderers;
return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id));
}, [excludedRowRendererIds, rowRenderers]);
const actionsColumnWidth = useMemo(
() => getActionsColumnWidth(ACTION_BUTTON_COUNT),
[ACTION_BUTTON_COUNT]
);
const columnWidths = useMemo(
() =>
columnHeaders.reduce(
(totalWidth, header) => totalWidth + (header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH),
0
),
[columnHeaders]
);
const leadingActionColumnsWidth = useMemo(() => {
return leadingControlColumns
? leadingControlColumns.reduce(
(totalWidth, header) =>
header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth,
0
)
: 0;
}, [actionsColumnWidth, leadingControlColumns]);
const trailingActionColumnsWidth = useMemo(() => {
return trailingControlColumns
? trailingControlColumns.reduce(
(totalWidth, header) =>
header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth,
0
)
: 0;
}, [actionsColumnWidth, trailingControlColumns]);
const totalWidth = useMemo(() => {
return columnWidths + leadingActionColumnsWidth + trailingActionColumnsWidth;
}, [columnWidths, leadingActionColumnsWidth, trailingActionColumnsWidth]);
const [lastFocusedAriaColindex] = useState(FIRST_ARIA_INDEX);
const columnCount = useMemo(() => {
return columnHeaders.length + trailingControlColumns.length + leadingControlColumns.length;
}, [columnHeaders, trailingControlColumns, leadingControlColumns]);
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
onKeyDownFocusHandler({
colindexAttribute: ARIA_COLINDEX_ATTRIBUTE,
containerElement: containerRef.current,
event: e,
maxAriaColindex: columnHeaders.length + 1,
maxAriaRowindex: data.length + 1,
onColumnFocused: noop,
rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE,
});
},
[columnHeaders.length, containerRef, data.length]
);
return (
<>
<TimelineBody data-test-subj="timeline-body" ref={containerRef}>
<EventsTable
$activePage={activePage}
$columnCount={columnCount}
data-test-subj={`${tabType}-events-table`}
columnWidths={totalWidth}
onKeyDown={onKeyDown}
$rowCount={data.length}
$totalPages={totalPages}
>
<ColumnHeaders
actionsColumnWidth={actionsColumnWidth}
browserFields={browserFields}
columnHeaders={columnHeaders}
isEventViewer={isEventViewer}
isSelectAllChecked={isSelectAllChecked}
onSelectAll={onSelectAll}
show={show}
showEventsSelect={false}
showSelectAllCheckbox={false}
sort={sort}
tabType={tabType}
timelineId={id}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
/>
<Events
containerRef={containerRef}
actionsColumnWidth={actionsColumnWidth}
columnHeaders={columnHeaders}
data={data}
eventIdToNoteIds={eventIdToNoteIds}
id={id}
isEventViewer={isEventViewer}
lastFocusedAriaColindex={lastFocusedAriaColindex}
loadingEventIds={loadingEventIds}
onRowSelected={onRowSelected}
pinnedEventIds={pinnedEventIds}
refetch={refetch}
renderCellValue={renderCellValue}
rowRenderers={enabledRowRenderers}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
showCheckboxes={false}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
tabType={tabType}
onToggleShowNotes={onToggleShowNotes}
/>
</EventsTable>
</TimelineBody>
<TimelineBodyGlobalStyle />
</>
);
}
);
StatefulBody.displayName = 'StatefulBody';

View file

@ -1,147 +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 moment from 'moment';
import type { MomentUnit } from './date_ranges';
import { getDateRange, getDates } from './date_ranges';
describe('dateRanges', () => {
describe('#getDates', () => {
test('given a unit of "year", it returns the four quarters of the year', () => {
const unit: MomentUnit = 'year';
const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700');
const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700');
expect(getDates({ unit, end, current })).toEqual(
[
'2018-01-01T07:00:00.000Z',
'2018-04-01T07:00:00.000Z',
'2018-07-01T07:00:00.000Z',
'2018-10-01T07:00:00.000Z',
].map((d) => new Date(d))
);
});
test('given a unit of "month", it returns all the weeks of the month', () => {
const unit: MomentUnit = 'month';
const end = moment.utc('Wed, 31 Oct 2018 23:59:59 -0600');
const current = moment.utc('Mon, 01 Oct 2018 00:00:00 -0600');
expect(getDates({ unit, end, current })).toEqual(
[
'2018-10-01T06:00:00.000Z',
'2018-10-08T06:00:00.000Z',
'2018-10-15T06:00:00.000Z',
'2018-10-22T06:00:00.000Z',
'2018-10-29T06:00:00.000Z',
].map((d) => new Date(d))
);
});
test('given a unit of "week", it returns all the days of the week', () => {
const unit: MomentUnit = 'week';
const end = moment.utc('Sat, 27 Oct 2018 23:59:59 -0600');
const current = moment.utc('Sun, 21 Oct 2018 00:00:00 -0600');
expect(getDates({ unit, end, current })).toEqual(
[
'2018-10-21T06:00:00.000Z',
'2018-10-22T06:00:00.000Z',
'2018-10-23T06:00:00.000Z',
'2018-10-24T06:00:00.000Z',
'2018-10-25T06:00:00.000Z',
'2018-10-26T06:00:00.000Z',
'2018-10-27T06:00:00.000Z',
].map((d) => new Date(d))
);
});
test('given a unit of "day", it returns all the hours of the day', () => {
const unit: MomentUnit = 'day';
const end = moment.utc('Tue, 23 Oct 2018 23:59:59 -0600');
const current = moment.utc('Tue, 23 Oct 2018 00:00:00 -0600');
expect(getDates({ unit, end, current })).toEqual(
[
'2018-10-23T06:00:00.000Z',
'2018-10-23T07:00:00.000Z',
'2018-10-23T08:00:00.000Z',
'2018-10-23T09:00:00.000Z',
'2018-10-23T10:00:00.000Z',
'2018-10-23T11:00:00.000Z',
'2018-10-23T12:00:00.000Z',
'2018-10-23T13:00:00.000Z',
'2018-10-23T14:00:00.000Z',
'2018-10-23T15:00:00.000Z',
'2018-10-23T16:00:00.000Z',
'2018-10-23T17:00:00.000Z',
'2018-10-23T18:00:00.000Z',
'2018-10-23T19:00:00.000Z',
'2018-10-23T20:00:00.000Z',
'2018-10-23T21:00:00.000Z',
'2018-10-23T22:00:00.000Z',
'2018-10-23T23:00:00.000Z',
'2018-10-24T00:00:00.000Z',
'2018-10-24T01:00:00.000Z',
'2018-10-24T02:00:00.000Z',
'2018-10-24T03:00:00.000Z',
'2018-10-24T04:00:00.000Z',
'2018-10-24T05:00:00.000Z',
].map((d) => new Date(d))
);
});
});
describe('#getDateRange', () => {
let dateSpy: jest.SpyInstance<number, []>;
beforeEach(() => {
dateSpy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2018, 10, 23)).valueOf());
});
afterEach(() => {
dateSpy.mockReset();
});
test('given a unit of "day", it returns all the hours of the day', () => {
const unit: MomentUnit = 'day';
const dates = getDateRange(unit);
expect(dates).toEqual(
[
'2018-11-23T00:00:00.000Z',
'2018-11-23T01:00:00.000Z',
'2018-11-23T02:00:00.000Z',
'2018-11-23T03:00:00.000Z',
'2018-11-23T04:00:00.000Z',
'2018-11-23T05:00:00.000Z',
'2018-11-23T06:00:00.000Z',
'2018-11-23T07:00:00.000Z',
'2018-11-23T08:00:00.000Z',
'2018-11-23T09:00:00.000Z',
'2018-11-23T10:00:00.000Z',
'2018-11-23T11:00:00.000Z',
'2018-11-23T12:00:00.000Z',
'2018-11-23T13:00:00.000Z',
'2018-11-23T14:00:00.000Z',
'2018-11-23T15:00:00.000Z',
'2018-11-23T16:00:00.000Z',
'2018-11-23T17:00:00.000Z',
'2018-11-23T18:00:00.000Z',
'2018-11-23T19:00:00.000Z',
'2018-11-23T20:00:00.000Z',
'2018-11-23T21:00:00.000Z',
'2018-11-23T22:00:00.000Z',
'2018-11-23T23:00:00.000Z',
].map((d) => new Date(d))
);
});
});
});

View file

@ -1,76 +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 moment from 'moment';
export type MomentUnit = 'year' | 'month' | 'week' | 'day';
export type MomentIncrement = 'quarters' | 'months' | 'weeks' | 'days' | 'hours';
export type MomentUnitToIncrement = { [key in MomentUnit]: MomentIncrement };
const unitsToIncrements: MomentUnitToIncrement = {
day: 'hours',
month: 'weeks',
week: 'days',
year: 'quarters',
};
interface GetDatesParams {
unit: MomentUnit;
end: moment.Moment;
current: moment.Moment;
}
/**
* A pure function that given a unit (e.g. `'year' | 'month' | 'week'...`) and
* a date range, returns a range of `Date`s with a granularity appropriate
* to the unit.
*
* @example
* test('given a unit of "year", it returns the four quarters of the year', () => {
* const unit: MomentUnit = 'year';
* const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700');
* const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700');
*
* expect(getDates({ unit, end, current })).toEqual(
* [
* '2018-01-01T07:00:00.000Z',
* '2018-04-01T06:00:00.000Z',
* '2018-07-01T06:00:00.000Z',
* '2018-10-01T06:00:00.000Z'
* ].map(d => new Date(d))
* );
* });
*/
export const getDates = ({ unit, end, current }: GetDatesParams): Date[] =>
current <= end
? [
current.toDate(),
...getDates({
current: current.clone().add(1, unitsToIncrements[unit]),
end,
unit,
}),
]
: [];
/**
* An impure function (it performs IO to get the current `Date`) that,
* given a unit (e.g. `'year' | 'month' | 'week'...`), it
* returns range of `Date`s with a granularity appropriate to the unit.
*/
export function getDateRange(unit: MomentUnit): Date[] {
const current = moment().utc().startOf(unit);
const end = moment().utc().endOf(unit);
return getDates({
current,
end, // TODO: this should be relative to `unit`
unit,
});
}

View file

@ -6,7 +6,7 @@
*/
import { mockTimelineData } from '../../../../../common/mock';
import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { defaultUdtHeaders } from '../column_headers/default_headers';
import { getFormattedFields } from './formatted_field_udt';
import type { DataTableRecord } from '@kbn/discover-utils/types';

View file

@ -13,13 +13,13 @@ import type { TimelineNonEcsData } from '../../../../../../common/search_strateg
import { mockTimelineData } from '../../../../../common/mock';
import { TestProviders } from '../../../../../common/mock/test_providers';
import { getEmptyValue } from '../../../../../common/components/empty_value';
import { defaultHeaders } from '../column_headers/default_headers';
import { columnRenderers } from '.';
import { getColumnRenderer } from './get_column_renderer';
import { getValues, findItem, deleteItemIdx } from './helpers';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
import { TimelineId } from '../../../../../../common/types/timeline';
import { defaultUdtHeaders } from '../column_headers/default_headers';
jest.mock('../../../../../common/lib/kibana');
@ -47,7 +47,7 @@ describe('get_column_renderer', () => {
columnName,
eventId: _id,
values: getValues(columnName, nonSuricata),
field: defaultHeaders[1],
field: defaultUdtHeaders[1],
scopeId: TimelineId.test,
});
@ -62,7 +62,7 @@ describe('get_column_renderer', () => {
columnName,
eventId: _id,
values: getValues(columnName, nonSuricata),
field: defaultHeaders[1],
field: defaultUdtHeaders[1],
scopeId: TimelineId.test,
});
const wrapper = mount(
@ -82,7 +82,7 @@ describe('get_column_renderer', () => {
columnName,
eventId: _id,
values: getValues(columnName, nonSuricata),
field: defaultHeaders[7],
field: defaultUdtHeaders[7],
scopeId: TimelineId.test,
});
const wrapper = mount(
@ -100,7 +100,7 @@ describe('get_column_renderer', () => {
columnName,
eventId: _id,
values: getValues(columnName, nonSuricata),
field: defaultHeaders[7],
field: defaultUdtHeaders[7],
scopeId: TimelineId.test,
});
const wrapper = mount(

View file

@ -8,7 +8,6 @@
import React from 'react';
import { mockTimelineData, TestProviders } from '../../../../../common/mock';
import { defaultColumnHeaderType } from '../column_headers/default_headers';
import { REASON_FIELD_NAME } from './constants';
import { reasonColumnRenderer } from './reason_column_renderer';
import { plainColumnRenderer } from './plain_column_renderer';
@ -19,6 +18,7 @@ import { RowRendererIdEnum } from '../../../../../../common/api/timeline';
import { render } from '@testing-library/react';
import { cloneDeep } from 'lodash';
import { TableId } from '@kbn/securitysolution-data-table';
import { defaultColumnHeaderType } from '../column_headers/default_headers';
jest.mock('./plain_column_renderer');
jest.mock('../../../../../common/components/link_to', () => {

View file

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortIndicator rendering renders correctly against snapshot 1`] = `
<EuiToolTip
content="Sorted descending"
data-test-subj="sort-indicator-tooltip"
delay="regular"
display="inlineBlock"
position="top"
>
<EuiIcon
data-test-subj="sortIndicator"
type="sortDown"
/>
<SortNumber
sortNumber={-1}
/>
</EuiToolTip>
`;

View file

@ -1,11 +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 type { SortColumnTimeline } from '../../../../../../common/types/timeline';
/** Specifies which column the timeline is sorted on */
export type Sort = SortColumnTimeline;

View file

@ -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 { mount, shallow } from 'enzyme';
import React from 'react';
import { Direction } from '../../../../../../common/search_strategy';
import * as i18n from '../translations';
import { getDirection, SortIndicator } from './sort_indicator';
describe('SortIndicator', () => {
describe('rendering', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
expect(wrapper).toMatchSnapshot();
});
test('it renders the expected sort indicator when direction is ascending', () => {
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
'sortUp'
);
});
test('it renders the expected sort indicator when direction is descending', () => {
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
'sortDown'
);
});
test('it renders the expected sort indicator when direction is `none`', () => {
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
'empty'
);
});
});
describe('getDirection', () => {
test('it returns the expected symbol when the direction is ascending', () => {
expect(getDirection(Direction.asc)).toEqual('sortUp');
});
test('it returns the expected symbol when the direction is descending', () => {
expect(getDirection(Direction.desc)).toEqual('sortDown');
});
test('it returns the expected symbol (undefined) when the direction is neither ascending, nor descending', () => {
expect(getDirection('none')).toEqual(undefined);
});
});
describe('sort indicator tooltip', () => {
test('it returns the expected tooltip when the direction is ascending', () => {
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
expect(
wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content
).toEqual(i18n.SORTED_ASCENDING);
});
test('it returns the expected tooltip when the direction is descending', () => {
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
expect(
wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content
).toEqual(i18n.SORTED_DESCENDING);
});
test('it does NOT render a tooltip when sort direction is `none`', () => {
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false);
});
});
});

View file

@ -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 { EuiIcon, EuiToolTip } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
import { SortNumber } from './sort_number';
import { Direction } from '../../../../../../common/search_strategy';
import type { SortDirection } from '../../../../../../common/types/timeline';
enum SortDirectionIndicatorEnum {
SORT_UP = 'sortUp',
SORT_DOWN = 'sortDown',
}
export type SortDirectionIndicator = undefined | SortDirectionIndicatorEnum;
/** Returns the symbol that corresponds to the specified `SortDirection` */
export const getDirection = (sortDirection: SortDirection): SortDirectionIndicator => {
switch (sortDirection) {
case Direction.asc:
return SortDirectionIndicatorEnum.SORT_UP;
case Direction.desc:
return SortDirectionIndicatorEnum.SORT_DOWN;
case 'none':
return undefined;
default:
throw new Error('Unhandled sort direction');
}
};
interface Props {
sortDirection: SortDirection;
sortNumber: number;
}
/** Renders a sort indicator */
export const SortIndicator = React.memo<Props>(({ sortDirection, sortNumber }) => {
const direction = getDirection(sortDirection);
if (direction != null) {
return (
<EuiToolTip
content={
direction === SortDirectionIndicatorEnum.SORT_UP
? i18n.SORTED_ASCENDING
: i18n.SORTED_DESCENDING
}
data-test-subj="sort-indicator-tooltip"
>
<>
<EuiIcon data-test-subj="sortIndicator" type={direction} />
<SortNumber sortNumber={sortNumber} />
</>
</EuiToolTip>
);
} else {
return <EuiIcon data-test-subj="sortIndicator" type={'empty'} />;
}
});
SortIndicator.displayName = 'SortIndicator';

View file

@ -1,27 +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, EuiNotificationBadge } from '@elastic/eui';
import React from 'react';
interface Props {
sortNumber: number;
}
export const SortNumber = React.memo<Props>(({ sortNumber }) => {
if (sortNumber >= 0) {
return (
<EuiNotificationBadge color="subdued" data-test-subj="sortNumber">
{sortNumber + 1}
</EuiNotificationBadge>
);
} else {
return <EuiIcon data-test-subj="sortEmptyNumber" type={'empty'} />;
}
});
SortNumber.displayName = 'SortNumber';

View file

@ -9,7 +9,7 @@ import { TimelineTabs } from '../../../../../common/types';
import { DataLoadingState } from '@kbn/unified-data-table';
import React from 'react';
import { UnifiedTimeline } from '../unified_components';
import { defaultUdtHeaders } from '../unified_components/default_headers';
import { defaultUdtHeaders } from './column_headers/default_headers';
import type { UnifiedTimelineBodyProps } from './unified_timeline_body';
import { UnifiedTimelineBody } from './unified_timeline_body';
import { render, screen } from '@testing-library/react';

View file

@ -10,7 +10,7 @@ import React, { useEffect, useState, useMemo } from 'react';
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
import { StyledTableFlexGroup, StyledUnifiedTableFlexItem } from '../unified_components/styles';
import { UnifiedTimeline } from '../unified_components';
import { defaultUdtHeaders } from '../unified_components/default_headers';
import { defaultUdtHeaders } from './column_headers/default_headers';
import type { PaginationInputPaginated, TimelineItem } from '../../../../../common/search_strategy';
export interface UnifiedTimelineBodyProps extends ComponentProps<typeof UnifiedTimeline> {

View file

@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useGetMappedNonEcsValue } from '../body/data_driven_columns';
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
import { columnRenderers } from '../body/renderers';
import { getColumnRenderer } from '../body/renderers/get_column_renderer';
import type { CellValueElementProps } from '.';

View file

@ -17,6 +17,7 @@ import {
buildIsOneOfQueryMatch,
buildIsQueryMatch,
handleIsOperator,
isFullScreen,
isPrimitiveArray,
showGlobalFilters,
} from './helpers';
@ -392,3 +393,42 @@ describe('isStringOrNumberArray', () => {
});
});
});
describe('isFullScreen', () => {
describe('globalFullScreen is false', () => {
it('should return false if isActiveTimelines is false', () => {
const result = isFullScreen({
globalFullScreen: false,
isActiveTimelines: false,
timelineFullScreen: true,
});
expect(result).toBe(false);
});
it('should return false if timelineFullScreen is false', () => {
const result = isFullScreen({
globalFullScreen: false,
isActiveTimelines: true,
timelineFullScreen: false,
});
expect(result).toBe(false);
});
});
describe('globalFullScreen is true', () => {
it('should return true if isActiveTimelines is true and timelineFullScreen is true', () => {
const result = isFullScreen({
globalFullScreen: true,
isActiveTimelines: true,
timelineFullScreen: true,
});
expect(result).toBe(true);
});
it('should return true if isActiveTimelines is false', () => {
const result = isFullScreen({
globalFullScreen: true,
isActiveTimelines: false,
timelineFullScreen: false,
});
expect(result).toBe(true);
});
});
});

View file

@ -282,3 +282,14 @@ export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area';
export const getNonDropAreaFilters = (filters: Filter[] = []) =>
filters.filter((f: Filter) => f.meta.controlledBy !== TIMELINE_FILTER_DROP_AREA);
export const isFullScreen = ({
globalFullScreen,
isActiveTimelines,
timelineFullScreen,
}: {
globalFullScreen: boolean;
isActiveTimelines: boolean;
timelineFullScreen: boolean;
}) =>
(isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen);

View file

@ -29,7 +29,7 @@ import { useTimelineFullScreen } from '../../../common/containers/use_full_scree
import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen';
import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict';
import { sourcererSelectors } from '../../../common/store';
import { defaultUdtHeaders } from './unified_components/default_headers';
import { defaultUdtHeaders } from './body/column_headers/default_headers';
const TimelineTemplateBadge = styled.div`
background: ${({ theme }) => theme.eui.euiColorVis3_behindText};

View file

@ -10,11 +10,6 @@ import { rgba } from 'polished';
import styled, { createGlobalStyle } from 'styled-components';
import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import type { TimelineEventsType } from '../../../../common/types/timeline';
import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers';
import { EVENTS_TABLE_ARIA_LABEL } from './translations';
/**
* TIMELINE BODY
*/
@ -73,79 +68,6 @@ TimelineBody.displayName = 'TimelineBody';
export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable';
interface EventsTableProps {
$activePage: number;
$columnCount: number;
columnWidths: number;
$rowCount: number;
$totalPages: number;
}
export const EventsTable = styled.div.attrs<EventsTableProps>(
({ className = '', $columnCount, columnWidths, $activePage, $rowCount, $totalPages }) => ({
'aria-label': EVENTS_TABLE_ARIA_LABEL({ activePage: $activePage + 1, totalPages: $totalPages }),
'aria-colcount': `${$columnCount}`,
'aria-rowcount': `${$rowCount + 1}`,
className: `siemEventsTable ${className}`,
role: 'grid',
style: {
minWidth: `${columnWidths}px`,
},
tabindex: '-1',
})
)<EventsTableProps>`
padding: 3px;
`;
/* EVENTS HEAD */
export const EventsThead = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thead ${className}`,
role: 'rowgroup',
}))`
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThick} solid
${({ theme }) => theme.eui.euiColorLightShade};
position: sticky;
top: 0;
z-index: ${({ theme }) => theme.eui.euiZLevel1};
`;
export const EventsTrHeader = styled.div.attrs(({ className }) => ({
'aria-rowindex': '1',
className: `siemEventsTable__trHeader ${className}`,
role: 'row',
}))`
display: flex;
`;
export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`,
className: `siemEventsTable__thGroupActions ${className}`,
role: 'columnheader',
tabIndex: '0',
}))<{ actionsColumnWidth: number; isEventViewer: boolean }>`
display: flex;
flex: 0 0
${({ actionsColumnWidth, isEventViewer }) =>
`${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`};
min-width: 0;
padding-left: ${({ isEventViewer }) =>
!isEventViewer ? '4px;' : '0;'}; // match timeline event border
`;
export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thGroupData ${className}`,
}))<{ isDragging?: boolean }>`
display: flex;
> div:hover .siemEventsHeading__handle {
display: ${({ isDragging }) => (isDragging ? 'none' : 'block')};
opacity: 1;
visibility: visible;
}
`;
export const EventsTh = styled.div.attrs<{ role: string }>(
({ className = '', role = 'columnheader' }) => ({
className: `siemEventsTable__th ${className}`,
@ -197,76 +119,6 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
}
`;
/* EVENTS BODY */
export const EventsTbody = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__tbody ${className}`,
role: 'rowgroup',
}))`
overflow-x: hidden;
`;
export const EventsTrGroup = styled.div.attrs(
({ className = '', $ariaRowindex }: { className?: string; $ariaRowindex: number }) => ({
'aria-rowindex': `${$ariaRowindex}`,
className: `siemEventsTable__trGroup ${className}`,
role: 'row',
})
)<{
className?: string;
eventType: Omit<TimelineEventsType, 'all'>;
isEvenEqlSequence: boolean;
isBuildingBlockType: boolean;
isExpanded: boolean;
showLeftBorder: boolean;
}>`
border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid
${({ theme }) => theme.eui.euiColorLightShade};
${({ theme, eventType, isEvenEqlSequence, showLeftBorder }) =>
showLeftBorder
? `border-left: 4px solid
${
eventType === 'raw'
? theme.eui.euiColorLightShade
: eventType === 'eql' && isEvenEqlSequence
? theme.eui.euiColorPrimary
: eventType === 'eql' && !isEvenEqlSequence
? theme.eui.euiColorAccent
: theme.eui.euiColorWarning
}`
: ''};
${({ isBuildingBlockType }) =>
isBuildingBlockType
? 'background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);'
: ''};
${({ eventType, isEvenEqlSequence }) =>
eventType === 'eql'
? isEvenEqlSequence
? 'background: repeating-linear-gradient(127deg, rgba(0, 107, 180, 0.2), rgba(0, 107, 180, 0.2) 1px, rgba(0, 107, 180, 0.05) 2px, rgba(0, 107, 180, 0.05) 10px);'
: 'background: repeating-linear-gradient(127deg, rgba(221, 10, 115, 0.2), rgba(221, 10, 115, 0.2) 1px, rgba(221, 10, 115, 0.05) 2px, rgba(221, 10, 115, 0.05) 10px);'
: ''};
&:hover {
background-color: ${({ theme }) => theme.eui.euiTableHoverColor};
}
${({ isExpanded, theme }) =>
isExpanded &&
`
background: ${theme.eui.euiTableSelectedColor};
&:hover {
${theme.eui.euiTableHoverSelectedColor}
}
`}
`;
export const EventsTrData = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__trData ${className}`,
}))`
display: flex;
`;
const TIMELINE_EVENT_DETAILS_OFFSET = 40;
interface WidthProp {
@ -295,57 +147,6 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({
}
`;
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`,
className: `siemEventsTable__tdGroupActions ${className}`,
role: 'gridcell',
}))<{ width: number }>`
align-items: center;
display: flex;
flex: 0 0 ${({ width }) => `${width}px`};
min-width: 0;
`;
export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__tdGroupData ${className}`,
}))`
display: flex;
`;
interface EventsTdProps {
$ariaColumnIndex?: number;
width?: number;
}
export const EVENTS_TD_CLASS_NAME = 'siemEventsTable__td';
export const EventsTd = styled.div.attrs<EventsTdProps>(
({ className = '', $ariaColumnIndex, width }) => {
const common = {
className: `siemEventsTable__td ${className}`,
role: 'gridcell',
style: {
flexBasis: width ? `${width}px` : 'auto',
},
};
return $ariaColumnIndex != null
? {
...common,
'aria-colindex': `${$ariaColumnIndex}`,
}
: common;
}
)<EventsTdProps>`
align-items: center;
display: flex;
flex-shrink: 0;
min-width: 0;
.siemEventsTable__tdGroupActions &:first-child:last-child {
flex: 1;
}
`;
export const EventsTdContent = styled.div.attrs(({ className }) => ({
className: `siemEventsTable__tdContent ${className != null ? className : ''}`,
}))<{ textAlign?: string; width?: number }>`
@ -363,89 +164,9 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({
}
`;
/**
* EVENTS HEADING
*/
export const EventsHeading = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsHeading ${className}`,
}))<{ isLoading: boolean }>`
align-items: center;
display: flex;
&:hover {
cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')};
}
`;
export const EventsHeadingTitleButton = styled.button.attrs(({ className = '' }) => ({
className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`,
type: 'button',
}))`
align-items: center;
display: flex;
font-weight: inherit;
min-width: 0;
&:hover,
&:focus {
color: ${({ theme }) => theme.eui.euiColorPrimary};
text-decoration: underline;
}
&:hover {
cursor: pointer;
}
& > * + * {
margin-left: ${({ theme }) => theme.eui.euiSizeXS};
}
`;
export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({
className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`,
}))`
min-width: 0;
`;
export const EventsHeadingExtra = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsHeading__extra ${className}` as string,
}))`
margin-left: auto;
margin-right: 2px;
&.siemEventsHeading__extra--close {
opacity: 0;
transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease;
visibility: hidden;
.siemEventsTable__th:hover & {
opacity: 1;
visibility: visible;
}
}
`;
export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsHeading__handle ${className}`,
}))`
background-color: ${({ theme }) => theme.eui.euiBorderColor};
height: 100%;
opacity: 0;
transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease;
visibility: hidden;
width: ${({ theme }) => theme.eui.euiBorderWidthThick};
&:hover {
background-color: ${({ theme }) => theme.eui.euiColorPrimary};
cursor: col-resize;
}
`;
/**
* EVENTS LOADING
*/
export const EventsLoading = styled(EuiLoadingSpinner)`
margin: 0 2px;
vertical-align: middle;

View file

@ -35,9 +35,6 @@ jest.mock('../../../../containers/details', () => ({
jest.mock('../../../fields_browser', () => ({
useFieldBrowserOptions: jest.fn(),
}));
jest.mock('../../body/events', () => ({
Events: () => <></>,
}));
jest.mock('../../../../../sourcerer/containers');
jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({

View file

@ -14,7 +14,7 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'
import { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
import { TestProviders } from '../../../../../common/mock/test_providers';
import { defaultRowRenderers } from '../../body/renderers';
import type { Sort } from '../../body/sort';
import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline';
import { TimelineId } from '../../../../../../common/types/timeline';
import { useTimelineEvents } from '../../../../containers';
import { useTimelineEventsDetails } from '../../../../containers/details';
@ -38,9 +38,6 @@ jest.mock('../../../../containers/details', () => ({
jest.mock('../../../fields_browser', () => ({
useFieldBrowserOptions: jest.fn(),
}));
jest.mock('../../body/events', () => ({
Events: () => <></>,
}));
jest.mock('../../../../../sourcerer/containers');

View file

@ -21,7 +21,6 @@ import { useKibana } from '../../../../../common/lib/kibana';
import { timelineSelectors } from '../../../../store';
import type { Direction } from '../../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../../containers';
import { defaultHeaders } from '../../body/column_headers/default_headers';
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import { timelineDefaults } from '../../../../store/defaults';
@ -37,6 +36,7 @@ import { useTimelineControlColumn } from '../shared/use_timeline_control_columns
import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left';
import { useNotesInFlyout } from '../../properties/use_notes_in_flyout';
import { NotesFlyout } from '../../properties/notes_flyout';
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
interface PinnedFilter {
bool: {
@ -111,7 +111,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
}, [pinnedEventIds]);
const timelineQueryFields = useMemo(() => {
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const columnsHeader = isEmpty(columns) ? defaultUdtHeaders : columns;
const columnFields = columnsHeader.map((c) => c.id);
return [...columnFields, ...requiredFieldsForActions];

View file

@ -30,8 +30,10 @@ import { useDispatch } from 'react-redux';
import type { ExperimentalFeatures } from '../../../../../../common';
import { allowedExperimentalValues } from '../../../../../../common';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
import {
defaultUdtHeaders,
defaultColumnHeaderType,
} from '../../body/column_headers/default_headers';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
import * as timelineActions from '../../../../store/actions';
@ -52,10 +54,6 @@ jest.mock('../../../fields_browser', () => ({
useFieldBrowserOptions: jest.fn(),
}));
jest.mock('../../body/events', () => ({
Events: () => <></>,
}));
jest.mock('../../../../../sourcerer/containers');
jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({
useSignalHelpers: () => ({ signalIndexNeedsInit: false }),

View file

@ -23,7 +23,6 @@ import { useKibana } from '../../../../../common/lib/kibana';
import * as i18n from './translations';
import { TimelineTabs } from '../../../../../../common/types/timeline';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import { isFullScreen } from '../../body/column_headers';
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../../common/constants';
import { FULL_SCREEN } from '../../body/column_headers/translations';
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
@ -35,6 +34,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile
import { timelineActions, timelineSelectors } from '../../../../store';
import { timelineDefaults } from '../../../../store/defaults';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { isFullScreen } from '../../helpers';
const FullScreenButtonIcon = styled(EuiButtonIcon)`
margin: 4px 0 4px 0;

View file

@ -5,14 +5,7 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiBadge,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlyoutHeader, EuiBadge } from '@elastic/eui';
import styled from 'styled-components';
export const TabHeaderContainer = styled.div`
@ -33,46 +26,12 @@ export const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)`
}
`;
export const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
flex: 1;
.euiFlyoutBody__overflow {
overflow: hidden;
mask-image: none;
}
.euiFlyoutBody__overflowContent {
padding: 0;
height: 100%;
display: flex;
}
`;
export const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)`
background: none;
&.euiFlyoutFooter {
${({ theme }) => `padding: ${theme.eui.euiSizeS} 0;`}
}
`;
export const FullWidthFlexGroup = styled(EuiFlexGroup)`
margin: 0;
width: 100%;
overflow: hidden;
`;
export const ScrollableFlexItem = styled(EuiFlexItem)`
${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`}
overflow: hidden;
`;
export const SourcererFlex = styled(EuiFlexItem)`
align-items: flex-end;
`;
SourcererFlex.displayName = 'SourcererFlex';
export const VerticalRule = styled.div`
width: 2px;
height: 100%;

View file

@ -8,7 +8,7 @@
import { TestProviders } from '../../../../../common/mock';
import { renderHook } from '@testing-library/react-hooks';
import { useTimelineColumns } from './use_timeline_columns';
import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns';
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({

View file

@ -9,7 +9,7 @@ import { useMemo } from 'react';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../../../sourcerer/containers';
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
import { defaultUdtHeaders } from '../../unified_components/default_headers';
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
import type { ColumnHeaderOptions } from '../../../../../../common/types';
import { memoizedGetTimelineColumnHeaders } from './utils';

View file

@ -7,7 +7,7 @@
import type { BrowserFields, ColumnHeaderOptions } from '@kbn/timelines-plugin/common';
import memoizeOne from 'memoize-one';
import type { ControlColumnProps } from '../../../../../../common/types';
import type { Sort } from '../../body/sort';
import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline';
import type { TimelineItem } from '../../../../../../common/search_strategy';
import type { inputsModel } from '../../../../../common/store';
import { getColumnHeaders } from '../../body/column_headers/helpers';

View file

@ -12,7 +12,7 @@ import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body';
import { mockTimelineData, TestProviders } from '../../../../../common/mock';
import type { TimelineItem } from '@kbn/timelines-plugin/common';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { defaultUdtHeaders } from '../default_headers';
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
import type { EuiDataGridColumn } from '@elastic/eui';
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';

View file

@ -8,7 +8,6 @@
import { createMockStore, mockTimelineData, TestProviders } from '../../../../../common/mock';
import React from 'react';
import { TimelineDataTable } from '.';
import { defaultUdtHeaders } from '../default_headers';
import { TimelineId, TimelineTabs } from '../../../../../../common/types';
import { DataLoadingState } from '@kbn/unified-data-table';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
@ -18,6 +17,7 @@ import { getColumnHeaders } from '../../body/column_headers/helpers';
import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks';
import { timelineActions } from '../../../../store';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
jest.mock('../../../../../sourcerer/containers');

View file

@ -1,53 +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 type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
} from '../body/constants';
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
export const defaultUdtHeaders: ColumnHeaderOptions[] = [
{
columnHeaderType: defaultColumnHeaderType,
id: '@timestamp',
initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
esTypes: ['date'],
type: 'date',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'message',
initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'event.category',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'event.action',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'host.name',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'source.ip',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'destination.ip',
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'user.name',
},
];

View file

@ -32,7 +32,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex
import { TimelineTabs } from '@kbn/securitysolution-data-table';
import { DataLoadingState } from '@kbn/unified-data-table';
import { getColumnHeaders } from '../body/column_headers/helpers';
import { defaultUdtHeaders } from './default_headers';
import { defaultUdtHeaders } from '../body/column_headers/default_headers';
import type { ColumnHeaderType } from '../../../../../common/types';
jest.mock('../../../containers', () => ({
@ -45,10 +45,6 @@ jest.mock('../../fields_browser', () => ({
useFieldBrowserOptions: jest.fn(),
}));
jest.mock('../body/events', () => ({
Events: () => <></>,
}));
jest.mock('../../../../sourcerer/containers');
jest.mock('../../../../sourcerer/containers/use_signal_helpers', () => ({
useSignalHelpers: () => ({ signalIndexNeedsInit: false }),

View file

@ -31,7 +31,6 @@ import { withDataView } from '../../../../common/components/with_data_view';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
import type { TimelineItem } from '../../../../../common/search_strategy';
import { useKibana } from '../../../../common/lib/kibana';
import { defaultHeaders } from '../body/column_headers/default_headers';
import type {
ColumnHeaderOptions,
OnChangePage,
@ -47,7 +46,7 @@ import { TimelineResizableLayout } from './resizable_layout';
import TimelineDataTable from './data_table';
import { timelineActions } from '../../../store';
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
import { defaultUdtHeaders } from './default_headers';
import { defaultUdtHeaders } from '../body/column_headers/default_headers';
import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({
@ -291,7 +290,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
(columnId: string) => {
dispatch(
timelineActions.upsertColumn({
column: getColumnHeader(columnId, defaultHeaders),
column: getColumnHeader(columnId, defaultUdtHeaders),
id: timelineId,
index: 1,
})

View file

@ -18,7 +18,7 @@ import { appActions } from '../../common/store/app';
import { SourcererScopeName } from '../../sourcerer/store/model';
import { InputsModelId } from '../../common/store/inputs/constants';
import { TestProviders, mockGlobalState } from '../../common/mock';
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context');
jest.mock('../../common/containers/use_global_time', () => {

View file

@ -19,7 +19,7 @@ import { SourcererScopeName } from '../../sourcerer/store/model';
import { appActions } from '../../common/store/app';
import type { TimeRange } from '../../common/store/inputs/model';
import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
import { timelineDefaults } from '../store/defaults';
export interface UseCreateTimelineParams {

View file

@ -12,10 +12,9 @@ import {
RowRendererIdEnum,
} from '../../../common/api/timeline';
import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers';
import { normalizeTimeRange } from '../../common/utils/normalize_time_range';
import type { SubsetTimelineModel, TimelineModel } from './model';
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false
const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false);
@ -109,7 +108,7 @@ export const timelineDefaults: SubsetTimelineModel &
};
export const getTimelineManageDefaults = (id: string) => ({
defaultColumns: defaultHeaders,
defaultColumns: defaultUdtHeaders,
documentType: '',
selectAll: false,
id,

View file

@ -18,7 +18,10 @@ import type {
DataProvidersAnd,
} from '../components/timeline/data_providers/data_provider';
import { IS_OPERATOR } from '../components/timeline/data_providers/data_provider';
import { defaultColumnHeaderType } from '../components/timeline/body/column_headers/default_headers';
import {
defaultUdtHeaders,
defaultColumnHeaderType,
} from '../components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
RESIZED_COLUMN_MIN_WITH,
@ -50,7 +53,6 @@ import type { TimelineModel } from './model';
import { timelineDefaults } from './defaults';
import type { TimelineById } from './types';
import { Direction } from '../../../common/search_strategy';
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
jest.mock('../../common/utils/normalize_time_range');
jest.mock('../../common/utils/default_date_settings', () => {

View file

@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
import type { Filter } from '@kbn/es-query';
import type { SessionViewConfig } from '../../../common/types';
import type { TimelineNonEcsData } from '../../../common/search_strategy';
import type { Sort } from '../components/timeline/body/sort';
import type {
DataProvider,
QueryOperator,
@ -23,15 +22,16 @@ import {
TimelineStatusEnum,
TimelineTypeEnum,
} from '../../../common/api/timeline';
import { TimelineId } from '../../../common/types/timeline';
import type {
ColumnHeaderOptions,
TimelineEventsType,
SerializedFilterQuery,
TimelinePersistInput,
SortColumnTimeline,
SortColumnTimeline as Sort,
} from '../../../common/types/timeline';
import type { RowRendererId, TimelineType } from '../../../common/api/timeline';
import { TimelineId } from '../../../common/types/timeline';
import { normalizeTimeRange } from '../../common/utils/normalize_time_range';
import { getTimelineManageDefaults, timelineDefaults } from './defaults';
import type { KqlMode, TimelineModel } from './model';

View file

@ -40202,8 +40202,6 @@
"xpack.securitySolution.timeline.descriptionTooltip": "Description",
"xpack.securitySolution.timeline.destination": "Destination",
"xpack.securitySolution.timeline.EqlQueryBarLabel": "Requête EQL",
"xpack.securitySolution.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.securitySolution.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.securitySolution.timeline.eventRenderersSwitch.title": "Outils de rendu d'événement",
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "L'activation des outils de rendu d'événement peut avoir un impact sur les performances de la table.",
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "Épingler la sélection",
@ -40259,10 +40257,6 @@
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "Cliquer pour synchroniser la plage temporelle des requêtes avec la plage temporelle de la page actuelle.",
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "Modèle sans titre",
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "Chronologie sans titre",
"xpack.securitySolution.timeline.rangePicker.oneDay": "1 jour",
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 mois",
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 semaine",
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 an",
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "Retirer des favoris",
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "Modifications non enregistrées",
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "Non enregistré",
@ -40312,7 +40306,6 @@
"xpack.securitySolution.timeline.userDetails.managed.description": "Les métadonnées de toutes les intégrations de référentiel de ressource sont autorisées dans votre environnement.",
"xpack.securitySolution.timeline.userDetails.updatedTime": "Mis à jour le {time}",
"xpack.securitySolution.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.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}",
"xpack.securitySolution.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie",
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "Impossible d'interroger les données de toutes les chronologies",
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "Importer",

View file

@ -39946,8 +39946,6 @@
"xpack.securitySolution.timeline.descriptionTooltip": "説明",
"xpack.securitySolution.timeline.destination": "送信先",
"xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL クエリ",
"xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "行{row}のイベントにはイベントレンダラーがあります。Shiftと下矢印を押すとフォーカスします。",
"xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "行{row}のイベントには{notesCount, plural, other {{notesCount}個のメモ}}があります。Shiftと右矢印を押すとメモをフォーカスします。",
"xpack.securitySolution.timeline.eventRenderersSwitch.title": "イベントレンダラー",
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "イベントレンダリングを有効化すると、テーブルパフォーマンスに影響する可能性があります。",
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "選択項目にピン付け",
@ -40003,10 +40001,6 @@
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "クリックすると、クエリの時間範囲と現在のページの時間範囲を同期します。",
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "無題のテンプレート",
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "無題のタイムライン",
"xpack.securitySolution.timeline.rangePicker.oneDay": "1日",
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 か月",
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 週間",
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 年",
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "お気に入りから削除",
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "保存されていない変更",
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存",
@ -40056,7 +40050,6 @@
"xpack.securitySolution.timeline.userDetails.managed.description": "環境で有効になっているアセットリポジトリ統合からのメタデータ。",
"xpack.securitySolution.timeline.userDetails.updatedTime": "更新日時{time}",
"xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。",
"xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "表セルの行 {row}、列 {column} にいます",
"xpack.securitySolution.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました",
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "すべてのタイムラインデータをクエリできませんでした",
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "インポート",

View file

@ -39991,8 +39991,6 @@
"xpack.securitySolution.timeline.descriptionTooltip": "描述",
"xpack.securitySolution.timeline.destination": "目标",
"xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL 查询",
"xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "位于行 {row} 的事件具有事件呈现程序。按 shift + 向下箭头键以对其聚焦。",
"xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "位于行 {row} 的事件有{notesCount, plural, =1 {备注} other { {notesCount} 个备注}}。按 shift + 右箭头键以聚焦备注。",
"xpack.securitySolution.timeline.eventRenderersSwitch.title": "事件呈现器",
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "启用事件呈现器可能会影响表性能。",
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "固定所选",
@ -40048,10 +40046,6 @@
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "单击以将查询时间范围与当前页面的时间范围进行同步。",
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "未命名模板",
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "未命名时间线",
"xpack.securitySolution.timeline.rangePicker.oneDay": "1 天",
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 个月",
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 周",
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 年",
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "从收藏夹中移除",
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "未保存的更改",
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存",
@ -40101,7 +40095,6 @@
"xpack.securitySolution.timeline.userDetails.managed.description": "在您的环境中启用的任何资产存储库集成中的元数据。",
"xpack.securitySolution.timeline.userDetails.updatedTime": "已更新 {time}",
"xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。",
"xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}",
"xpack.securitySolution.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误",
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "无法查询所有时间线数据",
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "导入",