diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index 5ec3e0c2d0e3d..aebf34f094027 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -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[]; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index e251370c7e4d3..fb13cf4a3ceed 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx index 1341cb72104d8..db88061d86013 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/header_actions.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts new file mode 100644 index 0000000000000..70e3078fd6301 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.test.ts @@ -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']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts new file mode 100644 index 0000000000000..e0711127e1e40 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/get_mapped_non_ecs_value.ts @@ -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]); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 3d284b89f0745..4eeb343134014 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -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(), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index a67eb08496dd0..d7df06616f221 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx index 557a1ff9038df..761ad573a5cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx index 75b888f84f334..62eddb4b6b49c 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 75c074c517758..4e298e50d2a26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx index c26b34dffcaf4..92f9b874f8743 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/actions/new_timeline_button.test.tsx @@ -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'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx index 8e08e4b957d64..0d1ed98f0bc99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/new_timeline/index.test.tsx @@ -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'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5da977c30a410..917f1d1bc29db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -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'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index b3f84a82d4ed0..e9c1d85b9049e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 6afd900185af7..8af06fe910f99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -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 { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 48209711babbf..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,513 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx deleted file mode 100644 index 025d12bc6ddc5..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ /dev/null @@ -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) => { - // To avoid a re-sorting when you delete a column - event.preventDefault(); - event.stopPropagation(); - onColumnRemoved(columnId); - }, - [columnId, onColumnRemoved] - ); - - return ( - - ); -}); - -CloseButton.displayName = 'CloseButton'; - -export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { - return ( - <> - {sort.some((i) => i.columnId === header.id) && isLoading ? ( - - - - ) : ( - - - - )} - - ); -}); - -Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx deleted file mode 100644 index 2048cefb9bf0f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ /dev/null @@ -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 = ({ - draggableIndex, - header, - timelineId, - isDragging, - onFilterChange, - sort, - tabType, -}) => { - const keyboardHandlerRef = useRef(null); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(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: , - }), - [] - ); - 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: , - name: i18n.HIDE_COLUMN, - onClick: () => { - dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id })); - handleClosePopOverTrigger(); - }, - }, - ...(tabType !== TimelineTabs.eql - ? [ - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_AZ, - onClick: () => { - onColumnSort(Direction.asc); - handleClosePopOverTrigger(); - }, - }, - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_ZA, - onClick: () => { - onColumnSort(Direction.desc); - handleClosePopOverTrigger(); - }, - }, - ] - : []), - ], - }, - ], - [ - dispatch, - handleClosePopOverTrigger, - header.aggregatable, - header.id, - onColumnSort, - tabType, - timelineId, - ] - ); - - const headerButton = useMemo( - () => ( -
- ), - [header, onFilterChange, sort, timelineId] - ); - - const DraggableContent = useCallback( - (dragProvided) => ( - - - - - - - - - - ), - [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 ( - -
- - {DraggableContent} - -
-
- ); -}; - -export const ColumnHeader = React.memo(ColumnHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts index 56c29462415bd..a249f2ef2a851 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts @@ -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, }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap deleted file mode 100644 index ed15a5f635e9f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Filter renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx deleted file mode 100644 index bd6e75efd94d9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx +++ /dev/null @@ -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(); - 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(); - - 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(); - - expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx deleted file mode 100644 index 3eb2cda8af242..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx +++ /dev/null @@ -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(({ header, onFilterChange = noop }) => { - switch (header.columnHeaderType) { - case 'text-filter': - return ( - - ); - case 'not-filtered': // fall through - default: - return null; - } -}); - -Filter.displayName = 'Filter'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 0a621f0218337..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Header renders correctly against snapshot 1`] = ` - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx deleted file mode 100644 index 24b75c88d7963..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ /dev/null @@ -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 = ({ - children, - header, - isLoading, - isResizing, - onClick, - showSortingCapability, - sort, -}) => ( - - {header.aggregatable && showSortingCapability ? ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - - - ) : ( - - - } - > - <> - {React.isValidElement(header.display) - ? header.display - : header.displayAsText ?? header.id} - - - - - )} - - {children} - -); - -export const HeaderContent = React.memo(HeaderContentComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts deleted file mode 100644 index e31ed05e55929..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ /dev/null @@ -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( - (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( - (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), - 'none' - ); - -export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => - sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx deleted file mode 100644 index abf452b834a1d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ /dev/null @@ -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( - - - - ); - expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders the header text', () => { - const wrapper = mount( - - - - ); - - 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( - - - - ); - - 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 = ( -
- {'The display property renders the column heading as a ReactNode'} -
- ); - const headerWithLabel = { ...columnHeader, display }; - const wrapper = mount( - - - - ); - - 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 = ( -
{'this text is rendered via display'}
- ); - const headerWithLabel = { ...columnHeader, display, displayAsText }; - const wrapper = mount( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - ); - - 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( - - - - ); - - 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( - - - - ); - - expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx deleted file mode 100644 index 08af0bf9cbdd1..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ /dev/null @@ -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 = ({ - 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( - (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 ( - <> - - - - - - - ); -}; - -export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx deleted file mode 100644 index cac232fd6ea34..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/selectors.tsx +++ /dev/null @@ -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); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 750f3956786a5..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderToolTipContent it renders the expected table content 1`] = ` - -

- - Category - : - - - base - -

-

- - Field - : - - - @timestamp - -

-

- - Type - : - - - - - date - - -

-

- - Description - : - - - 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. - -

-
-`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx deleted file mode 100644 index d2a134f37aba4..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx +++ /dev/null @@ -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(); - - expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual( - header.category - ); - }); - - test('it renders the name of the field', () => { - const wrapper = mount(); - - 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(); - - expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock'); - }); - - test('it renders the type of the field', () => { - const wrapper = mount(); - - 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(); - - 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(); - - 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(); - - expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); - }); - - test('it renders the expected table content', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx deleted file mode 100644 index c96f3473a0064..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ /dev/null @@ -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) && ( -

- - {i18n.CATEGORY} - {':'} - - {header.category} -

- )} -

- - {i18n.FIELD} - {':'} - - {header.id} -

-

- - {i18n.TYPE} - {':'} - - - - {header.esTypes?.map((esType) => ( - - {esType} - - ))} - -

- {!isEmpty(header.description) && ( -

- - {i18n.DESCRIPTION} - {':'} - - - {header.description} - -

- )} - -)); -HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts index 8a4f024daac83..816ba731ba4fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts @@ -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); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx deleted file mode 100644 index c20fcc45ea300..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ /dev/null @@ -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( - - - - ); - 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( - - - - ); - - expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true); - }); - - test('it renders every column header', () => { - const wrapper = mount( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - - 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( - - - - ); - 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) => ( - - - - ); - const wrapper = mount(); - expect(mockCloseEditor).not.toHaveBeenCalled(); - - wrapper.setProps({ ...defaultProps, show: false }); - expect(mockCloseEditor).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx deleted file mode 100644 index f343b9af8ed97..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ /dev/null @@ -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( - ({ 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(null); - const fieldEditorActionsRef = useRef(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 ( - - - - - - - - ); - }, - [columnHeaders, setDraggingIndex] - ); - - const ColumnHeaderList = useMemo( - () => - columnHeaders.map((header, draggableIndex) => ( - - )), - [columnHeaders, timelineId, draggingIndex, sort, tabType] - ); - - const DroppableContent = useCallback( - (dropProvided, snapshot) => ( - <> - - {ColumnHeaderList} - - - ), - [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 | React.ComponentType | undefined, index) => { - const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - leadingHeaderCells, - leadingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - fieldBrowserOptions, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - - const TrailingHeaderActions = useMemo(() => { - return trailingHeaderCells.map( - (Header: React.ComponentType | React.ComponentType | undefined, index) => { - const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - trailingHeaderCells, - trailingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - fieldBrowserOptions, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - return ( - - - {LeadingHeaderActions} - - {DroppableContent} - - {TrailingHeaderActions} - - - ); -}; - -export const ColumnHeaders = React.memo(ColumnHeadersComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx deleted file mode 100644 index 4d531e34fa3cd..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx +++ /dev/null @@ -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(); - - 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(); - - 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( - - ); - - wrapper.find('select').simulate('change', { target: { value: newSelection } }); - - expect(mockOnRangeSelected).toBeCalledWith(newSelection); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx deleted file mode 100644 index 2eca94729aeb7..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx +++ /dev/null @@ -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(({ selected, onRangeSelected }) => { - const onChange = (event: React.ChangeEvent): void => { - onRangeSelected(event.target.value); - }; - - return ( - - ({ - text: range, - }))} - onChange={onChange} - /> - - ); -}); - -RangePicker.displayName = 'RangePicker'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts deleted file mode 100644 index a174207b2b057..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts +++ /dev/null @@ -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]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts deleted file mode 100644 index 339c56f92490a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts +++ /dev/null @@ -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', -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fc4bd7bbd6148..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TextFilter rendering renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx deleted file mode 100644 index aae979c19902e..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx +++ /dev/null @@ -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(); - 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(); - - 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( - - ); - - 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(); - - 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(); - - 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( - - ); - - wrapper.find('input').simulate('change', { target: { value: newFilter } }); - expect(onFilterChange).toBeCalledWith({ - columnId, - filter: newFilter, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx deleted file mode 100644 index d22e2ca40ca40..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx +++ /dev/null @@ -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( - ({ - columnId, - minWidth, - filter = '', - onFilterChange = noop, - placeholder = DEFAULT_PLACEHOLDER, - }) => { - const onChange = (event: React.ChangeEvent): void => { - onFilterChange({ columnId, filter: event.target.value }); - }; - - return ( - - ); - } -); - -TextFilter.displayName = 'TextFilter'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fd4566ca440e8..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,1037 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Columns it renders the expected columns 1`] = ` - - - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx deleted file mode 100644 index 67f3c35a3ec13..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ /dev/null @@ -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( - - ); - - 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(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx deleted file mode 100644 index a1d65e69dba58..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ /dev/null @@ -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>; - isEventPinned: boolean; - isEventViewer?: boolean; - loadingEventIds: Readonly; - notesCount: number; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - refetch: inputsModel.Refetch; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - 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( - `.${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>; -}) => { - const displayWidth = width ? width : actionsColumnWidth; - return ( - - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}

-
- {Action && ( - - )} - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} - - {notesCount ? ( - -

{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}

-
- ) : null} -
-
- ); -}; - -const TgridTdCell = ({ - _id, - ariaRowindex, - index, - header, - data, - ecsData, - hasRowRenderers, - notesCount, - renderCellValue, - tabType, - timelineId, -}: CellProps) => { - const ariaColIndex = index + ARIA_COLUMN_INDEX_OFFSET; - return ( - - - <> - -

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: ariaColIndex })}

-
- - -
- {hasRowRenderers ? ( - -

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

-
- ) : null} - - {notesCount ? ( - -

{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}

-
- ) : null} -
- ); -}; - -export const DataDrivenColumns = React.memo( - ({ - 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 && ( - - ) - ); - }), - [ - 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) => ( - - )), - [ - _id, - ariaRowindex, - columnHeaders, - data, - ecsData, - hasRowRenderers, - notesCount, - renderCellValue, - tabType, - timelineId, - ] - ); - return ( - - {ColumnHeaders} - {TrailingActions} - - ); - } -); - -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]); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx deleted file mode 100644 index 32ca51ca62f78..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx +++ /dev/null @@ -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 = ({ 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
{value}
; -}; - -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( - - ); - - 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( - - ); - - 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 = () =>
; - - const wrapper = mount( - - ); - - 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( - - ); - - expect( - wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') - ).toEqual('background-color: green;'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx deleted file mode 100644 index 941f6499fe854..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx +++ /dev/null @@ -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>({}); - return ( -
- {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, - })} -
- ); -}; - -StatefulCellComponent.displayName = 'StatefulCellComponent'; - -export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts deleted file mode 100644 index 18bd7a600f7cd..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts +++ /dev/null @@ -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.', - }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx deleted file mode 100644 index 8b49c12eaf73c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ /dev/null @@ -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(() =>
), - }; -}); - -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(, { - 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(, { - 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(, { - 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( - , - { - 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(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx deleted file mode 100644 index e184e27d428ef..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ /dev/null @@ -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>; - isEventPinned: boolean; - isEventViewer?: boolean; - loadingEventIds: Readonly; - notesCount: number; - onEventDetailsPanelOpened: () => void; - onRowSelected: OnRowSelected; - refetch: inputsModel.Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - hasRowRenderers: boolean; - selectedEventIds: Readonly>; - 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( - ({ - 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 ( - - {Action && ( - - )} - - ); - }), - [ - 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 ( - - {LeadingActions} - - - ); - } -); - -EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx deleted file mode 100644 index 76c28f24b14d6..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ /dev/null @@ -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; - data: TimelineItem[]; - eventIdToNoteIds: Readonly>; - id: string; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - pinnedEventIds: Readonly>; - refetch: inputsModel.Refetch; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - onRuleChange?: () => void; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - onToggleShowNotes?: (eventId?: string) => void; -} - -const EventsComponent: React.FC = ({ - actionsColumnWidth, - columnHeaders, - containerRef, - data, - eventIdToNoteIds, - id, - isEventViewer = false, - lastFocusedAriaColindex, - loadingEventIds, - onRowSelected, - pinnedEventIds, - refetch, - onRuleChange, - renderCellValue, - rowRenderers, - selectedEventIds, - showCheckboxes, - tabType, - leadingControlColumns, - trailingControlColumns, - onToggleShowNotes, -}) => ( - - {data.map((event, i) => ( - - ))} - -); - -export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx deleted file mode 100644 index 05f7a9d8b8e2b..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ /dev/null @@ -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; - columnHeaders: ColumnHeaderOptions[]; - event: TimelineItem; - eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; - lastFocusedAriaColindex: number; - loadingEventIds: Readonly; - onRowSelected: OnRowSelected; - isEventPinned: boolean; - refetch: inputsModel.Refetch; - ariaRowindex: number; - onRuleChange?: () => void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - tabType?: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; - onToggleShowNotes?: (eventId?: string) => void; -} - -const emptyNotes: string[] = []; - -const EventsTrSupplementContainerWrapper = React.memo>( - ({ children }) => { - const width = useEventDetailsWidthContext(); - return {children}; - } -); - -EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper'; - -const StatefulEventComponent: React.FC = ({ - 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(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( - `.${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( - ({ eventIds, isLoading }) => { - dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); - }, - [dispatch, timelineId] - ); - - const setEventsDeleted = useCallback( - ({ eventIds, isDeleted }) => { - dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); - }, - [dispatch, timelineId] - ); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export const StatefulEvent = React.memo(StatefulEventComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx deleted file mode 100644 index 0c365ae42798d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ /dev/null @@ -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>>; -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 } - ) => { - 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>; - - 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(); - expect(wrapper.find('[data-test-subj="column-headers"]').first().exists()).toEqual(true); - }); - - test('it renders the scroll container', async () => { - const wrapper = await getWrapper(); - expect(wrapper.find('[data-test-subj="timeline-body"]').first().exists()).toEqual(true); - }); - - test('it renders events', async () => { - const wrapper = await getWrapper(); - 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(, { 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(); - - 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(); - - 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(); - - 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', - }, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx deleted file mode 100644 index ab60e061fcdf9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ /dev/null @@ -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( - ({ - activePage, - browserFields, - data, - id, - isEventViewer = false, - onRuleChange, - refetch, - renderCellValue, - rowRenderers, - sort, - tabType, - totalPages, - leadingControlColumns = [], - trailingControlColumns = [], - onToggleShowNotes, - }) => { - const dispatch = useDispatch(); - const containerRef = useRef(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 ( - <> - - - - - - - - - - ); - } -); - -StatefulBody.displayName = 'StatefulBody'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts deleted file mode 100644 index 36749de01333a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts +++ /dev/null @@ -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; - - 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)) - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts deleted file mode 100644 index c715b2004c69c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/mini_map/date_ranges.ts +++ /dev/null @@ -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, - }); -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx index d731b6e831f9d..c1fe2f3278dd5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_udt.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx index 551ba3c4ac570..ad75ef79aa049 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -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( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx index a5cd33efdd5c4..3912d7d9ef8ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap deleted file mode 100644 index 8a7b179da059f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts deleted file mode 100644 index 96503dcac3812..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts +++ /dev/null @@ -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; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx deleted file mode 100644 index 56f98a6795cd1..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx deleted file mode 100644 index 82c25f00c78ab..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ /dev/null @@ -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(({ sortDirection, sortNumber }) => { - const direction = getDirection(sortDirection); - - if (direction != null) { - return ( - - <> - - - - - ); - } else { - return ; - } -}); - -SortIndicator.displayName = 'SortIndicator'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx deleted file mode 100644 index 3fdd31eae5c47..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx +++ /dev/null @@ -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(({ sortNumber }) => { - if (sortNumber >= 0) { - return ( - - {sortNumber + 1} - - ); - } else { - return ; - } -}); - -SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx index 031604c6a3da6..41cdec6d6d4bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx index fe6b668ed6837..95feab8543617 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/unified_timeline_body.tsx @@ -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 { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index 9e5006267d32b..ec230139dc95e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -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 '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index f7dad276cb939..9f187f91dcdff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -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); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index 04f08f203ec7f..43c4648abb83a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -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); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index ea0edabdfe7bb..05d15f076f569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -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}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 97762de6bcb91..1591c7f8c791b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -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( - ({ 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', - }) -)` - 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; - 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( - ({ 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; - } -)` - 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; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx index 7c8949c1b6121..23fb44d04910f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx @@ -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', () => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx index 0df50a8cf47c3..55604cd958c23 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.test.tsx @@ -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'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 8f19e90c77a70..e4593f7eec959 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -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 = ({ }, [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]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index 70afec0d73135..f0a2c06bbffb4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -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 }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index eae2eec549dfe..c711208cb6806 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx index aacb38ea2e798..1cac4dc2536f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/layout.tsx @@ -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%; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts index 8bbda9a255a09..926082ff9ed41 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.test.ts @@ -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', () => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx index f42bf47c76423..006c6ba1eb679 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_columns.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts index 879e4b140a61a..543c7daaf8679 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/shared/utils.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx index cfdb2b0d2dbf9..cc8b24710e5af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/custom_timeline_data_grid_body.test.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx index 78ffadeb37ff8..649817d5f8ef2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx @@ -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'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx deleted file mode 100644 index a0cf9d6355b9d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/default_headers.tsx +++ /dev/null @@ -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', - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx index 9703efd8d5bb3..c660893ba379e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.test.tsx @@ -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 }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx index 7d89da9002ba8..112886f93ca32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/index.tsx @@ -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 = ({ (columnId: string) => { dispatch( timelineActions.upsertColumn({ - column: getColumnHeader(columnId, defaultHeaders), + column: getColumnHeader(columnId, defaultUdtHeaders), id: timelineId, index: 1, }) diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx index e8e2fe9dbbadf..a4c054371a316 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx index 527f372c1a447..2f80e969cab5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/hooks/use_create_timeline.tsx @@ -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 { diff --git a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts index dd9b811e144e8..e4fd97cd50534 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts @@ -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, diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts index 4503d1026d7c8..b0067d6d0d9f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts index b876465449740..a9566c22a814a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts @@ -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'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d7dfcc8ba4072..d3efeff9f821c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -40230,8 +40230,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", @@ -40287,10 +40285,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é", @@ -40340,7 +40334,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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 656e6f844f0c3..d026751779628 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39974,8 +39974,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": "選択項目にピン付け", @@ -40031,10 +40029,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": "未保存", @@ -40084,7 +40078,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": "インポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3341c98103cb..79c35dff0e021 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -40020,8 +40020,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": "固定所选", @@ -40077,10 +40075,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": "未保存", @@ -40130,7 +40124,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": "导入",