From ee0f9ebb4f44ea314b7abceec20e6821625d8028 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 15 Nov 2021 10:35:25 -0700 Subject: [PATCH 1/4] [Security Solution] Adjusts the width of the `Actions` column and action icon buttons (#118454) ## [Security Solution] Adjusts the width of the `Actions` column and action icon buttons This PR adjusts the width of the `Actions` column, and normalizes the action icon button sizes throughout the Security Solution, per https://github.com/elastic/kibana/issues/115726 ### Before / after screenshots This section provides before / after screenshots for the following views: - Alerts - Alerts > Event rendered - Rules > Details - Rules > Details > Event rendered - Host > Events - Host > External alerts - Network > External alerts - Timeline > Query tab - Timeline > Correlation tab - Timeline > Pinned tab - Observability > alerts (no change) #### Alerts (before) ![01-security_alerts_before](https://user-images.githubusercontent.com/4459398/141429498-a6040f8b-5bfb-468e-aa1a-993caa7f179c.png) #### Alerts (after) ![01a-security_alerts_after](https://user-images.githubusercontent.com/4459398/141429618-8ad313e1-fabc-424e-9e7d-c24240861c1d.png) #### Alerts > Event rendered (before) ![02-security_alerts_event_rendered_before](https://user-images.githubusercontent.com/4459398/141430881-2bfeb57a-9881-47f1-99e4-cc7eadcfff69.png) #### Alerts > Event rendered (after) ![02a-security_alerts_event_rendered_after](https://user-images.githubusercontent.com/4459398/141430976-88f8099a-81b1-4f1c-99a2-26f86218f909.png) #### Rules > Details (before) ![03-security_rules_details_before](https://user-images.githubusercontent.com/4459398/141431149-a308f171-a170-4ce9-9616-77e5c08dc406.png) #### Rules > Details (after) ![03a-security_rules_details_after](https://user-images.githubusercontent.com/4459398/141431221-06701540-97bb-400a-97bf-f2d22cd65caf.png) #### Rules > Details > Event rendered (before) ![04-security_rule_details_event_rendered_before](https://user-images.githubusercontent.com/4459398/141431394-12b29689-41c8-44b6-b69f-7796f99c5424.png) #### Rules > Details > Event rendered (after) ![04a-security_rule_details_event_rendered_after](https://user-images.githubusercontent.com/4459398/141431477-049804c0-1455-4216-a241-a44df5c9d398.png) #### Host > Events (before) ![05-host_events_before](https://user-images.githubusercontent.com/4459398/141431858-31116980-47f7-4779-af26-3b3785638137.png) #### Host > Events (after) ![05a-host_events_after](https://user-images.githubusercontent.com/4459398/141431956-664f86b9-2ad7-4281-bf82-8278fa23c755.png) #### Host > External alerts (before) ![06-host_external_alerts_before](https://user-images.githubusercontent.com/4459398/141432103-8cc9c10e-4d2d-42ec-a62c-a1e5867bf2d8.png) #### Host > External alerts (after) ![06a-host_external_alerts_after](https://user-images.githubusercontent.com/4459398/141432185-4d7e4007-dea9-47f3-af4b-1719f338a5ba.png) #### Network > External alerts (before) ![07-network_external_alerts_before](https://user-images.githubusercontent.com/4459398/141432331-2bb5a714-f733-4c97-91dc-73ff76633daa.png) #### Network > External alerts (after) ![07a-network_external_alerts_after](https://user-images.githubusercontent.com/4459398/141432428-b7b20450-db87-44ab-8014-cf4d6032dfe3.png) #### Timeline > Query tab (before) ![08-timeline_query_tab_before](https://user-images.githubusercontent.com/4459398/141432638-e484813b-275d-4eff-aa38-1705f913ce59.png) #### Timeline > Query tab (after) ![08a-timeline_query_tab_after](https://user-images.githubusercontent.com/4459398/141434461-1d36bba5-8fd1-484a-bacd-733aede95815.png) #### Timeline > Correlation tab (before) ![09-timeline_correlation_tab_before](https://user-images.githubusercontent.com/4459398/141434637-33f05447-e3d3-4eac-b38a-3612945e8379.png) #### Timeline > Correlation tab (after) ![09a-timeline_correlation_tab_after](https://user-images.githubusercontent.com/4459398/141434751-250fd26b-25fc-48cc-8a06-dbb17e53dce7.png) #### Timeline > Pinned tab (before) ![10-timeline_pinned_tab_before](https://user-images.githubusercontent.com/4459398/141434893-3f2b3d17-7e4b-4e0c-9096-ab1ee57f096f.png) #### Timeline > Pinned tab (after) ![10a-timeline_pinned_tab_after](https://user-images.githubusercontent.com/4459398/141435431-26eac065-bce4-4a25-99fd-095d447fb6f3.png) #### Observability > alerts (before) ![11-observability_alerts_before](https://user-images.githubusercontent.com/4459398/141435607-da059e9c-af03-4a21-bb1b-e47d44d61dde.png) #### Observability > alerts (after / no change) ![11a-observability_alerts_after_no_change](https://user-images.githubusercontent.com/4459398/141435696-52bcc5e1-6823-4b6a-b2da-32e3f8733dc8.png) ### Additional details - Per [this comment](https://github.com/elastic/kibana/issues/115726#issuecomment-962077067) from @monina-n , the size of all action buttons have been normalized match the size off the `...` overflow button (`28 x 32` at the time of this writing) via the `EuiButtonIcon` `size` prop: ``` size="s" ``` - The horizontal alignment of the `Analyze event` icon was updated by the EUI team in the following PR: https://github.com/elastic/eui/pull/5365 # Conflicts: # x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx # x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx # x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx # x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx --- .../components/alerts_viewer/alerts_table.tsx | 5 + .../components/events_viewer/index.test.tsx | 3 + .../common/components/events_viewer/index.tsx | 122 ++++++------------ .../components/alerts_table/index.tsx | 5 + .../timeline_actions/alert_context_menu.tsx | 4 +- .../navigation/events_query_tab_body.tsx | 8 +- .../body/actions/action_icon_item.tsx | 5 +- .../timeline/body/actions/header_actions.tsx | 10 +- .../timeline/body/actions/index.tsx | 10 +- .../body/actions/pin_event_action.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../body/column_headers/helpers.test.ts | 31 +---- .../timeline/body/column_headers/helpers.ts | 26 +--- .../body/column_headers/index.test.tsx | 37 +++--- .../components/timeline/body/constants.ts | 14 -- .../body/control_columns/index.test.tsx | 31 +++++ .../timeline/body/control_columns/index.tsx | 21 +-- .../body/data_driven_columns/index.test.tsx | 6 +- .../body/events/event_column_view.test.tsx | 12 +- .../components/timeline/body/index.test.tsx | 6 +- .../components/timeline/body/index.tsx | 21 +-- .../cell_rendering/default_cell_renderer.tsx | 2 +- .../timeline/eql_tab_content/index.tsx | 22 +++- .../timelines/components/timeline/helpers.tsx | 2 - .../components/timeline/pin/index.tsx | 7 +- .../timeline/pinned_tab_content/index.tsx | 16 ++- .../timeline/properties/helpers.tsx | 1 + .../timeline/query_tab_content/index.tsx | 16 ++- .../common/types/timeline/actions/index.ts | 2 - .../components/actions/action_icon_item.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../body/column_headers/helpers.test.tsx | 58 +++++---- .../t_grid/body/column_headers/helpers.tsx | 42 +++--- .../t_grid/body/column_headers/index.test.tsx | 19 +-- .../components/t_grid/body/constants.ts | 28 ++-- .../body/events/event_column_view.test.tsx | 5 +- .../public/components/t_grid/body/index.tsx | 23 +--- .../public/components/t_grid/helpers.tsx | 2 - x-pack/plugins/timelines/public/index.ts | 2 + 39 files changed, 315 insertions(+), 323 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index fc440197e83491..fff5b465956de7 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -21,6 +21,7 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import type { EntityType } from '../../../../../timelines/common'; +import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; export interface OwnProps { end: string; @@ -79,6 +80,7 @@ const AlertsTableComponent: React.FC = ({ const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; + const ACTION_BUTTON_COUNT = 3; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); @@ -104,6 +106,8 @@ const AlertsTableComponent: React.FC = ({ ); }, [dispatch, filterManager, tGridEnabled, timelineId]); + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + return ( = ({ end={endDate} entityType={entityType} id={timelineId} + leadingControlColumns={leadingControlColumns} renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 0943d3c0f075e8..9dd8bf59893c03 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -21,6 +21,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; @@ -38,6 +39,7 @@ mockUseResizeObserver.mockImplementation(() => ({})); const from = '2019-08-27T22:10:56.794Z'; const to = '2019-08-26T22:10:56.791Z'; +const ACTION_BUTTON_COUNT = 4; const testProps = { defaultCellActions, @@ -46,6 +48,7 @@ const testProps = { entityType: EntityType.ALERTS, indexNames: [], id: TimelineId.test, + leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT), renderCellValue: DefaultCellRenderer, rowRenderers: defaultRowRenderers, scopeId: SourcererScopeName.default, 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 1e61e69180f917..854f8af02375ca 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 @@ -10,7 +10,6 @@ import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; @@ -28,18 +27,9 @@ import { TGridCellAction } from '../../../../../timelines/common/types'; import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { useKibana } from '../../lib/kibana'; -import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; -import { EventsViewer } from './events_viewer'; -import * as i18n from './translations'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; -const leadingControlColumns: ControlColumnProps[] = [ - { - ...defaultControlColumn, - headerCellRender: () => <>{i18n.ACTIONS}, - }, -]; const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; @@ -54,6 +44,7 @@ export interface OwnProps { end: string; entityType: EntityType; id: TimelineId; + leadingControlColumns: ControlColumnProps[]; scopeId: SourcererScopeName; start: string; showTotalCount?: boolean; @@ -93,6 +84,7 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, + leadingControlColumns, pageFilters, currentFilter, onRuleChange, @@ -121,8 +113,6 @@ const StatefulEventsViewerComponent: React.FC = ({ loading: isLoadingIndexPattern, } = useSourcererScope(scopeId); const { globalFullScreen } = useGlobalFullScreen(); - // TODO: Once we are past experimental phase this code should be removed - const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( 'tGridEventRenderedViewEnabled' ); @@ -175,73 +165,46 @@ const StatefulEventsViewerComponent: React.FC = ({ <> - {tGridEnabled ? ( - timelinesUi.getTGrid<'embedded'>({ - additionalFilters, - browserFields, - bulkActions, - columns, - dataProviders, - defaultCellActions, - deletedEventIds, - docValueFields, - end, - entityType, - filters: globalFilters, - filterStatus: currentFilter, - globalFullScreen, - graphEventId, - graphOverlay, - hasAlertsCrud, - id, - indexNames: selectedPatterns, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - leadingControlColumns, - onRuleChange, - query, - renderCellValue, - rowRenderers, - setQuery, - sort, - start, - tGridEventRenderedViewEnabled, - trailingControlColumns, - type: 'embedded', - unit, - }) - ) : ( - - )} + {timelinesUi.getTGrid<'embedded'>({ + additionalFilters, + browserFields, + bulkActions, + columns, + dataProviders, + defaultCellActions, + deletedEventIds, + docValueFields, + end, + entityType, + filters: globalFilters, + filterStatus: currentFilter, + globalFullScreen, + graphEventId, + graphOverlay, + hasAlertsCrud, + id, + indexNames: selectedPatterns, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + leadingControlColumns, + onRuleChange, + query, + renderCellValue, + rowRenderers, + runtimeMappings, + setQuery, + sort, + start, + tGridEventRenderedViewEnabled, + trailingControlColumns, + type: 'embedded', + unit, + createFieldComponent, + })} = ({ const { addWarning } = useAppToasts(); // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + const ACTION_BUTTON_COUNT = 4; const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -369,6 +371,8 @@ export const AlertsTableComponent: React.FC = ({ ); }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return null; } @@ -383,6 +387,7 @@ export const AlertsTableComponent: React.FC = ({ entityType="events" hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} id={timelineId} + leadingControlColumns={leadingControlColumns} onRuleChange={onRuleChange} pageFilters={defaultFiltersMemo} renderCellValue={RenderCellValue} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index a9b6eabecff864..ed9ccb8a360334 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -15,7 +15,7 @@ import { get } from 'lodash/fp'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../timelines/public'; import { Ecs } from '../../../../../common/ecs'; import { AddExceptionModal, @@ -185,7 +185,7 @@ const AlertContextMenuComponent: React.FC 0 && (
- + = ({ }) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); - + const ACTION_BUTTON_COUNT = 3; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { @@ -96,6 +97,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ }; }, [deleteQuery]); + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + return ( <> {!globalFullScreen && ( @@ -115,6 +118,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ end={endDate} entityType="events" id={TimelineId.hostsPageEvents} + leadingControlColumns={leadingControlColumns} pageFilters={pageFilters} renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx index 2e2e912f5abfab..7012cf89896f95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx @@ -9,7 +9,7 @@ import React, { MouseEvent } from 'react'; import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui'; import { EventsTdContent } from '../../styles'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public'; interface ActionIconItemProps { ariaLabel?: string; @@ -24,7 +24,7 @@ interface ActionIconItemProps { } const ActionIconItemComponent: React.FC = ({ - width = DEFAULT_ICON_BUTTON_WIDTH, + width = DEFAULT_ACTION_BUTTON_WIDTH, dataTestSubj, content, ariaLabel, @@ -46,6 +46,7 @@ const ActionIconItemComponent: React.FC = ({ iconType={iconType} isDisabled={isDisabled} onClick={onClick} + size="s" /> )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index de190c7df5e3fe..5e49db28ef00d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -28,7 +28,7 @@ import { useGlobalFullScreen, useTimelineFullScreen, } from '../../../../../common/containers/use_full_screen'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public'; import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; import { EventsTh, EventsThContent } from '../../styles'; import { EventsSelect } from '../column_headers/events_select'; @@ -166,7 +166,7 @@ const HeaderActionsComponent: React.FC = ({ {showSelectAllCheckbox && ( - + = ({ - + = ({ {tabType !== TimelineTabs.eql && ( - + {ColumnSorting} @@ -228,7 +228,7 @@ const HeaderActionsComponent: React.FC = ({ {showEventsSelect && ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index c4dae739cb251e..69bc406bb2076e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -19,7 +19,7 @@ import { AddEventNoteAction } from './add_note_icon_item'; import { PinEventAction } from './pin_event_action'; import { EventsTdContent } from '../../styles'; import * as i18n from '../translations'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { setActiveTabTimeline, @@ -136,7 +136,7 @@ const ActionsComponent: React.FC = ({ {showCheckboxes && !tGridEnabled && (
- + {loadingEventIds.includes(eventId) ? ( ) : ( @@ -152,13 +152,14 @@ const ActionsComponent: React.FC = ({
)}
- + @@ -205,7 +206,7 @@ const ActionsComponent: React.FC = ({ /> {isDisabled === false ? (
- + = ({ data-test-subj="view-in-analyzer" iconType="analyzeEvent" onClick={handleClick} + size="s" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx index f08b34d1bdde65..9527ad4e3ee447 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { EventsTdContent } from '../../styles'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public'; import { eventHasNotes, getPinTooltip } from '../helpers'; import { Pin } from '../../pin'; import { TimelineType } from '../../../../../../common/types/timeline'; @@ -41,7 +41,7 @@ const PinEventActionComponent: React.FC = ({ return (
- + { @@ -28,28 +23,6 @@ describe('helpers', () => { }); }); - describe('getActionsColumnWidth', () => { - test('returns the default actions column width when isEventViewer is false', () => { - expect(getActionsColumnWidth(false)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH); - }); - - test('returns the minimum actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { - expect(getActionsColumnWidth(false, true)).toEqual( - MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH - ); - }); - - test('returns the minimum actions column width when isEventViewer is true', () => { - expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH); - }); - - test('returns the minimum actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { - expect(getActionsColumnWidth(true, true)).toEqual( - MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH - ); - }); - }); - describe('getColumnHeaders', () => { test('should return a full object of ColumnHeader from the default header', () => { const expectedData = [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index 760c132cd18240..60118b1e55e589 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -9,14 +9,7 @@ import { get } from 'lodash/fp'; import { ColumnHeaderOptions } from '../../../../../../common'; import { BrowserFields } from '../../../../../common/containers/source'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, - SHOW_CHECK_BOXES_COLUMN_WIDTH, - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, - DEFAULT_ACTIONS_COLUMN_WIDTH, - MINIMUM_ACTIONS_COLUMN_WIDTH, -} from '../constants'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; /** Enriches the column headers with field details from the specified browserFields */ export const getColumnHeaders = ( @@ -40,20 +33,3 @@ export const getColumnHeaders = ( export const getColumnWidthFromType = (type: string): number => type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; - -/** Returns the (fixed) width of the Actions column */ -export const getActionsColumnWidth = ( - isEventViewer: boolean, - showCheckboxes = false, - additionalActionWidth = 0 -): number => { - const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0; - const actionsColumnWidth = - checkboxesWidth + - (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + - additionalActionWidth; - - return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth - ? actionsColumnWidth - : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth; -}; 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 index 378f7fce250fe7..59bdcf808ca422 100644 --- 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 @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import '../../../../../common/mock/match_media'; -import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import { getActionsColumnWidth } from '../../../../../../../timelines/public'; import { defaultHeaders } from './default_headers'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Sort } from '../sort'; @@ -21,8 +21,9 @@ import { cloneDeep } from 'lodash/fp'; import { timelineActions } from '../../../../store/timeline'; import { TimelineTabs } from '../../../../../../common/types/timeline'; import { Direction } from '../../../../../../common/search_strategy'; -import { defaultControlColumn } from '../control_columns'; +import { getDefaultControlColumn } from '../control_columns'; import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns'; +import { HeaderActions } from '../actions/header_actions'; jest.mock('../../../../../common/lib/kibana'); @@ -39,6 +40,12 @@ const 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, + })); describe('rendering', () => { const sort: Sort[] = [ @@ -53,7 +60,7 @@ describe('ColumnHeaders', () => { const wrapper = shallow( { sort={sort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -75,7 +82,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { sort={sort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -98,7 +105,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { sort={sort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -159,7 +166,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { sort={mockSort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -203,7 +210,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { sort={mockSort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -242,7 +249,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { sort={mockSort} tabType={TimelineTabs.query} timelineId={timelineId} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} /> @@ -280,7 +287,7 @@ describe('ColumnHeaders', () => { const wrapper = mount( { + describe('getDefaultControlColumn', () => { + const ACTION_BUTTON_COUNT = 5; + + test('it returns the expected defaults', () => { + expect(getDefaultControlColumn(ACTION_BUTTON_COUNT)).toMatchInlineSnapshot(` + Array [ + Object { + "headerCellRender": [Function], + "id": "default-timeline-control-column", + "rowCellRender": Object { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": [Function], + }, + "width": 152, + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx index 2cdc8d5f4e284e..d1457200bca8a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx @@ -5,15 +5,18 @@ * 2.0. */ +import React from 'react'; + import { ControlColumnProps } from '../../../../../../common/types/timeline'; import { Actions } from '../actions'; -import { HeaderActions } from '../actions/header_actions'; - -const DEFAULT_CONTROL_COLUMN_WIDTH = 140; +import { getActionsColumnWidth } from '../../../../../../../timelines/public'; +import * as i18n from '../../../../../common/components/events_viewer/translations'; -export const defaultControlColumn: ControlColumnProps = { - id: 'default-timeline-control-column', - width: DEFAULT_CONTROL_COLUMN_WIDTH, - headerCellRender: HeaderActions, - rowCellRender: Actions, -}; +export const getDefaultControlColumn = (actionButtonCount: number): ControlColumnProps[] => [ + { + headerCellRender: () => <>{i18n.ACTIONS}, + id: 'default-timeline-control-column', + rowCellRender: Actions, + width: getActionsColumnWidth(actionButtonCount), + }, +]; 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 index 789cd5211f121b..bff9aa14602373 100644 --- 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 @@ -13,12 +13,14 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer' import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; -import { defaultControlColumn } from '../control_columns'; +import { getDefaultControlColumn } from '../control_columns'; import { DataDrivenColumns } 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( @@ -45,7 +47,7 @@ describe('Columns', () => { toggleShowNotes={jest.fn()} refetch={jest.fn()} eventIdToNoteIds={{}} - leadingControlColumns={[defaultControlColumn]} + leadingControlColumns={leadingControlColumns} trailingControlColumns={[]} setEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} 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 index b0ccbec6276dbd..ff0b0c7f163cb6 100644 --- 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 @@ -9,7 +9,6 @@ import { mount } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../../common/mock'; -import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; @@ -17,9 +16,10 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer' import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { defaultControlColumn } from '../control_columns'; +import { getDefaultControlColumn } from '../control_columns'; import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns'; import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; +import { getActionsColumnWidth } from '../../../../../../../timelines/public'; jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; @@ -57,11 +57,13 @@ jest.mock( describe('EventColumnView', () => { useIsExperimentalFeatureEnabledMock.mockReturnValue(false); (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default); + const ACTION_BUTTON_COUNT = 4; + const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT); const props = { ariaRowindex: 2, id: 'event-id', - actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT), associateNote: jest.fn(), columnHeaders: [], columnRenderers: [], @@ -91,7 +93,7 @@ describe('EventColumnView', () => { toggleShowNotes: jest.fn(), updateNote: jest.fn(), isEventPinned: false, - leadingControlColumns: [defaultControlColumn], + leadingControlColumns, trailingControlColumns: [], setEventsLoading: jest.fn(), setEventsDeleted: jest.fn(), @@ -145,7 +147,7 @@ describe('EventColumnView', () => { , { wrappingComponent: TestProviders, 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 index 9509ae0eb78389..a7bff6be2f643e 100644 --- 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 @@ -19,7 +19,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; -import { defaultControlColumn } from './control_columns'; +import { getDefaultControlColumn } from './control_columns'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; @@ -119,6 +119,8 @@ describe('Body', () => { (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); }); + const ACTION_BUTTON_COUNT = 4; + const props: StatefulBodyProps = { activePage: 0, browserFields: mockBrowserFields, @@ -140,7 +142,7 @@ describe('Body', () => { showCheckboxes: false, tabType: TimelineTabs.query, totalPages: 1, - leadingControlColumns: [defaultControlColumn], + leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT), trailingControlColumns: [], }; 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 index 3ee0ef8804e892..2c0ae1daec2354 100644 --- 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 @@ -16,6 +16,7 @@ import { ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, + getActionsColumnWidth, } from '../../../../../../timelines/public'; import { CellValueElementProps } from '../cell_rendering'; import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; @@ -34,14 +35,13 @@ import { TimelineModel } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; -import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; +import { getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; import { 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 { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; interface OwnProps { @@ -61,15 +61,11 @@ interface OwnProps { onRuleChange?: () => void; } -const NUM_OF_ICON_IN_TIMELINE_ROW = 2; - export const hasAdditionalActions = (id: TimelineId): boolean => [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( id ); -const EXTRA_WIDTH = 4; // px - export type StatefulBodyProps = OwnProps & PropsFromRedux; /** @@ -108,6 +104,7 @@ export const BodyComponent = React.memo( const { queryFields, selectAll } = useDeepEqualSelector((state) => getManageTimeline(state, id) ); + const ACTION_BUTTON_COUNT = 5; const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { @@ -158,17 +155,7 @@ export const BodyComponent = React.memo( return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); }, [excludedRowRendererIds, rowRenderers]); - const actionsColumnWidth = useMemo( - () => - getActionsColumnWidth( - isEventViewer, - showCheckboxes, - hasAdditionalActions(id as TimelineId) - ? DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH - : 0 - ), - [isEventViewer, showCheckboxes, id] - ); + const actionsColumnWidth = useMemo(() => getActionsColumnWidth(ACTION_BUTTON_COUNT), []); const columnWidths = useMemo( () => 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 6aec7ae19734cf..1bfd0ed6b87c44 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 @@ -18,7 +18,7 @@ import { StyledContent, } from '../../../../common/lib/cell_actions/expanded_cell_value_actions'; -const FIELDS_WITHOUT_CELL_ACTIONS = ['@timestamp', 'signal.rule.risk_score', 'signal.reason']; +const FIELDS_WITHOUT_CELL_ACTIONS = ['signal.rule.risk_score', 'signal.reason']; const hasCellActions = (columnId?: string) => { return columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 737c6b99cea756..892dc5365b1993 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -14,7 +14,7 @@ import { EuiBadge, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; @@ -55,7 +55,8 @@ import { useTimelineFullScreen } from '../../../../common/containers/use_full_sc import { activeTimeline } from '../../../containers/active_timeline_context'; import { DetailsPanel } from '../../side_panel'; import { EqlQueryBarTimeline } from '../query_bar/eql'; -import { defaultControlColumn } from '../body/control_columns'; +import { HeaderActions } from '../body/actions/header_actions'; +import { getDefaultControlColumn } from '../body/control_columns'; import { Sort } from '../body/sort'; const TimelineHeaderContainer = styled.div` @@ -151,6 +152,8 @@ export type Props = OwnProps & PropsFromRedux; const NO_SORTING: Sort[] = []; +const trailingControlColumns: ControlColumnProps[] = []; // stable reference + export const EqlTabContentComponent: React.FC = ({ activeTab, columns, @@ -179,7 +182,12 @@ export const EqlTabContentComponent: React.FC = ({ docValueFields, loading: loadingSourcerer, selectedPatterns, +<<<<<<< HEAD } = useSourcererScope(SourcererScopeName.timeline); +======= + } = useSourcererDataView(SourcererScopeName.timeline); + const ACTION_BUTTON_COUNT = 5; +>>>>>>> e4814b91cac ([Security Solution] Adjusts the width of the `Actions` column and action icon buttons (#118454)) const isBlankTimeline: boolean = isEmpty(eqlQuery); @@ -242,8 +250,14 @@ export const EqlTabContentComponent: React.FC = ({ ); }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; + const leadingControlColumns = useMemo( + () => + getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ + ...x, + headerCellRender: HeaderActions, + })), + [] + ); return ( <> 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 2c85f1547dbeb0..94f62f94f35b91 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 @@ -224,8 +224,6 @@ export const combineQueries = ({ */ export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; -export const DEFAULT_ICON_BUTTON_WIDTH = 24; - export const resolverIsShowing = (graphEventId: string | undefined): boolean => graphEventId != null && graphEventId !== ''; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx index 8a2b2170b72d12..63fc17d7cc1d2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonIcon, IconSize } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React from 'react'; @@ -20,14 +20,13 @@ export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' : interface Props { ariaLabel?: string; allowUnpinning: boolean; - iconSize?: IconSize; timelineType?: TimelineTypeLiteral; onClick?: () => void; pinned: boolean; } export const Pin = React.memo( - ({ ariaLabel, allowUnpinning, iconSize = 'm', onClick = noop, pinned, timelineType }) => { + ({ ariaLabel, allowUnpinning, onClick = noop, pinned, timelineType }) => { const isTemplate = timelineType === TimelineType.template; const defaultAriaLabel = isTemplate ? i18n.DISABLE_PIN : pinned ? i18n.PINNED : i18n.UNPINNED; const pinAriaLabel = ariaLabel != null ? ariaLabel : defaultAriaLabel; @@ -36,10 +35,10 @@ export const Pin = React.memo( ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 2051f95b75ae87..98003c8b226528 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -14,6 +14,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { HeaderActions } from '../body/actions/header_actions'; import { CellValueElementProps } from '../cell_rendering'; import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; @@ -39,7 +40,7 @@ import { import { DetailsPanel } from '../../side_panel'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; -import { defaultControlColumn } from '../body/control_columns'; +import { getDefaultControlColumn } from '../body/control_columns'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` overflow-y: hidden; @@ -103,6 +104,8 @@ interface PinnedFilter { export type Props = OwnProps & PropsFromRedux; +const trailingControlColumns: ControlColumnProps[] = []; // stable reference + export const PinnedTabContentComponent: React.FC = ({ columns, timelineId, @@ -121,6 +124,7 @@ export const PinnedTabContentComponent: React.FC = ({ loading: loadingSourcerer, } = useSourcererScope(SourcererScopeName.timeline); const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen(); + const ACTION_BUTTON_COUNT = 5; const existingIndexNamesSelector = useMemo( () => sourcererSelectors.getAllExistingIndexNamesSelector(), @@ -202,8 +206,14 @@ export const PinnedTabContentComponent: React.FC = ({ onEventClosed({ tabType: TimelineTabs.pinned, timelineId }); }, [timelineId, onEventClosed]); - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; + const leadingControlColumns = useMemo( + () => + getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ + ...x, + headerCellRender: HeaderActions, + })), + [] + ); return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 12525c9e4db75a..7e790ab820039e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -116,6 +116,7 @@ const SmallNotesButton = React.memo( data-test-subj="timeline-notes-button-small" iconType="editorComment" onClick={toggleShowNotes} + size="s" isDisabled={isTemplate} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 2082e7f5b69bb1..a7fd335f8c1d68 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -59,7 +59,8 @@ import { useTimelineFullScreen } from '../../../../common/containers/use_full_sc import { activeTimeline } from '../../../containers/active_timeline_context'; import { DetailsPanel } from '../../side_panel'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; -import { defaultControlColumn } from '../body/control_columns'; +import { HeaderActions } from '../body/actions/header_actions'; +import { getDefaultControlColumn } from '../body/control_columns'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; const TimelineHeaderContainer = styled.div` @@ -156,6 +157,8 @@ const EMPTY_EVENTS: TimelineItem[] = []; export type Props = OwnProps & PropsFromRedux; +const trailingControlColumns: ControlColumnProps[] = []; // stable reference + export const QueryTabContentComponent: React.FC = ({ activeTab, columns, @@ -194,6 +197,7 @@ export const QueryTabContentComponent: React.FC = ({ } = useSourcererScope(SourcererScopeName.timeline); const { uiSettings } = useKibana().services; + const ACTION_BUTTON_COUNT = 5; const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); const { filterManager: activeFilterManager } = useDeepEqualSelector((state) => @@ -313,8 +317,14 @@ export const QueryTabContentComponent: React.FC = ({ return (combinedQueries && combinedQueries.kqlError != null) || false; }, [combinedQueries]); - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; + const leadingControlColumns = useMemo( + () => + getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({ + ...x, + headerCellRender: HeaderActions, + })), + [] + ); return ( <> diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index e85f2eaa12d726..19a10ab2a22260 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -104,8 +104,6 @@ interface AdditionalControlColumnProps { // Override these type definitions to support either a generic custom component or the one used in security_solution today. headerCellRender: HeaderCellRender; rowCellRender: RowCellRender; - // If not provided, calculated dynamically - width?: number; } export type ControlColumnProps = Omit< diff --git a/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx b/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx index f0acdc588ddeb9..d979354e5548b4 100644 --- a/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx +++ b/x-pack/plugins/timelines/public/components/actions/action_icon_item.tsx @@ -9,7 +9,7 @@ import React, { MouseEvent } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { EventsTdContent } from '../t_grid/styles'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../t_grid/helpers'; +import { DEFAULT_ACTION_BUTTON_WIDTH } from '../t_grid/body/constants'; interface ActionIconItemProps { ariaLabel?: string; @@ -23,7 +23,7 @@ interface ActionIconItemProps { } const ActionIconItemComponent: React.FC = ({ - width = DEFAULT_ICON_BUTTON_WIDTH, + width = DEFAULT_ACTION_BUTTON_WIDTH, dataTestSubj, content, ariaLabel, diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap index e6e56818bcc846..56242d420d546c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` { }); }); - describe('getActionsColumnWidth', () => { - test('returns the default actions column width when isEventViewer is false', () => { - expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH); - }); - - test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { - expect(getActionsColumnWidth(false, true)).toEqual( - DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH - ); - }); - - test('returns the events viewer actions column width when isEventViewer is true', () => { - expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); - }); - - test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { - expect(getActionsColumnWidth(true, true)).toEqual( - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH - ); - }); - }); - describe('getSchema', () => { const expected: Record = { date: 'datetime', @@ -343,4 +320,35 @@ describe('helpers', () => { ).toEqual(expected); }); }); + + describe('getActionsColumnWidth', () => { + // ideally the following implementation detail wouldn't be part of these tests, + // but without it, the test would be brittle when `euiDataGridCellPaddingM` changes: + const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; + + test('it returns the expected width', () => { + const ACTION_BUTTON_COUNT = 5; + const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + expectedContentWidth + expectedPadding + ); + }); + + test('it returns the minimum width when the button count is zero', () => { + const ACTION_BUTTON_COUNT = 0; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding + ); + }); + + test('it returns the minimum width when the button count is negative', () => { + const ACTION_BUTTON_COUNT = -1; + + expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual( + DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding + ); + }); + }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx index 66ec3ec1c399fe..aa2fc3f964c3c7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiDataGridColumnActions } from '@elastic/eui'; import { get, keyBy } from 'lodash/fp'; import React from 'react'; @@ -15,12 +16,9 @@ import type { } from '../../../../../common/search_strategy/index_fields'; import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; import { + DEFAULT_ACTION_BUTTON_WIDTH, DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, - SHOW_CHECK_BOXES_COLUMN_WIDTH, - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, - DEFAULT_ACTIONS_COLUMN_WIDTH, - MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; import { allowSorting } from '../helpers'; @@ -127,19 +125,25 @@ export const getColumnHeaders = ( export const getColumnWidthFromType = (type: string): number => type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; -/** Returns the (fixed) width of the Actions column */ -export const getActionsColumnWidth = ( - isEventViewer: boolean, - showCheckboxes = false, - additionalActionWidth = 0 -): number => { - const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0; - const actionsColumnWidth = - checkboxesWidth + - (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + - additionalActionWidth; - - return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth - ? actionsColumnWidth - : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth; +/** + * Returns the width of the Actions column based on the number of buttons being + * displayed + * + * NOTE: This function is necessary because `width` is a required property of + * the `EuiDataGridControlColumn` interface, so it must be calculated before + * content is rendered. (The width of a `EuiDataGridControlColumn` does not + * automatically size itself to fit all the content.) + */ +export const getActionsColumnWidth = (actionButtonCount: number): number => { + const contentWidth = + actionButtonCount > 0 + ? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH + : DEFAULT_ACTION_BUTTON_WIDTH; + + // `EuiDataGridRowCell` applies additional `padding-left` and + // `padding-right`, which must be added to the content width to prevent the + // content from being partially hidden due to the space occupied by padding: + const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px` + + return contentWidth + leftRightCellPadding; }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx index 1466b06f8ed25b..084cd31d76e610 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx @@ -8,7 +8,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import { getActionsColumnWidth } from './helpers'; + import { defaultHeaders } from './default_headers'; import { Sort } from '../sort'; @@ -51,6 +52,8 @@ const timelineId = 'test'; describe('ColumnHeaders', () => { const mount = useMountAppended(); + const ACTION_BUTTON_COUNT = 4; + const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT); describe('rendering', () => { const sort: Sort[] = [ @@ -65,7 +68,7 @@ describe('ColumnHeaders', () => { const wrapper = shallow( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( ({ })); describe('EventColumnView', () => { + const ACTION_BUTTON_COUNT = 4; const props = { ariaRowindex: 2, id: 'event-id', - actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT), associateNote: jest.fn(), columnHeaders: [], columnRenderers: [], diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 724a881fda318a..c3b1d184afffe7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -50,7 +50,7 @@ import { import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; -import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; +import { getColumnHeaders } from './column_headers/helpers'; import { addBuildingBlockStyle, getEventIdToDataMapping, @@ -58,7 +58,6 @@ import { mapSortingColumns, } from './helpers'; -import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import type { OnRowSelected, OnSelectAll } from '../types'; import type { Refetch } from '../../../store/t_grid/inputs'; @@ -119,19 +118,14 @@ interface OwnProps { } const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n); -const NUM_OF_ICON_IN_TIMELINE_ROW = 2; export const hasAdditionalActions = (id: TimelineId): boolean => [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( id ); -const EXTRA_WIDTH = 4; // px - const ES_LIMIT_COUNT = 9999; -const MIN_ACTION_COLUMN_WIDTH = 96; // px - const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; const EmptyHeaderCellRender: ComponentType = () => null; @@ -150,7 +144,6 @@ const FIELDS_WITHOUT_CELL_ACTIONS = ['@timestamp', 'signal.rule.risk_score', 'si const hasCellActions = (columnId?: string) => columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0; const transformControlColumns = ({ - actionColumnsWidth, columnHeaders, controlColumns, data, @@ -172,7 +165,6 @@ const transformControlColumns = ({ setEventsDeleted, hasAlertsCrudPermissions, }: { - actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; data: TimelineItem[]; @@ -209,7 +201,7 @@ const transformControlColumns = ({ <> {HeaderActions && ( ); }, - width: width ?? actionColumnsWidth, + width, }) ); @@ -611,13 +603,6 @@ export const BodyComponent = React.memo( controlColumns, data, isEventViewer, - actionColumnsWidth: hasAdditionalActions(id as TimelineId) - ? getActionsColumnWidth( - isEventViewer, - showCheckboxes, - DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH - ) - : controlColumns.reduce((acc, c) => acc + (c.width ?? MIN_ACTION_COLUMN_WIDTH), 0), loadingEventIds, onRowSelected, onRuleChange, diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index 33b76da0aa98c9..b2807c2acc811b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -242,8 +242,6 @@ export const getCombinedFilterQuery = ({ */ export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; -export const DEFAULT_ICON_BUTTON_WIDTH = 24; - export const resolverIsShowing = (graphEventId: string | undefined): boolean => graphEventId != null && graphEventId !== ''; diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 95ef7247269ea4..0612571eefaa34 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -59,6 +59,8 @@ export { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId, } from './components/drag_and_drop/helpers'; +export { getActionsColumnWidth } from './components/t_grid/body/column_headers/helpers'; +export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants'; export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser'; export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items'; // This exports static code and TypeScript types, From 307518b67483804979847dc51157ead37670aee2 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Mon, 15 Nov 2021 11:55:55 -0700 Subject: [PATCH 2/4] - removed files deleted via https://github.com/elastic/kibana/pull/118454 --- .../events_viewer/events_viewer.test.tsx | 484 ------------------ .../events_viewer/events_viewer.tsx | 395 -------------- 2 files changed, 879 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx deleted file mode 100644 index 7b7a1ead5d702e..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { waitFor, act } from '@testing-library/react'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import '../../mock/match_media'; -import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock'; - -import { mockEventViewerResponse, mockEventViewerResponseWithEvents } from './mock'; -import { StatefulEventsViewer } from '.'; -import { EventsViewer } from './events_viewer'; -import { defaultHeaders } from './default_headers'; -import { useSourcererScope } from '../../containers/sourcerer'; -import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock'; -import { eventsDefaultModel } from './default_model'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { inputsModel } from '../../store/inputs'; -import { TimelineId, SortDirection } from '../../../../common/types/timeline'; -import { KqlMode } from '../../../timelines/store/timeline/model'; -import { EntityType } from '../../../../../timelines/common'; -import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; -import { SourcererScopeName } from '../../store/sourcerer/model'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { useTimelineEvents } from '../../../timelines/containers'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; -import { mockTimelines } from '../../mock/mock_timelines_plugin'; - -jest.mock('../../lib/kibana', () => ({ - useKibana: () => ({ - services: { - application: { - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - uiSettings: { - get: jest.fn(), - }, - savedObjects: { - client: {}, - }, - timelines: { ...mockTimelines }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - }), - useGetUserCasesPermissions: jest.fn(), - useDateFormat: jest.fn(), - useTimeZone: jest.fn(), -})); - -jest.mock('../../hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; - -jest.mock('../../../timelines/components/graph_overlay', () => ({ - GraphOverlay: jest.fn(() =>
), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - useDataGridColumnSorting: jest.fn(), - }; -}); -jest.mock('../../../timelines/containers', () => ({ - useTimelineEvents: jest.fn(), -})); - -jest.mock('../../components/url_state/normalize_time_range.ts'); - -const mockUseSourcererScope: jest.Mock = useSourcererScope as jest.Mock; -jest.mock('../../containers/sourcerer'); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockUseTimelineEvents: jest.Mock = useTimelineEvents as jest.Mock; -jest.mock('../../../timelines/containers'); - -const from = '2019-08-26T22:10:56.791Z'; -const to = '2019-08-27T22:10:56.794Z'; - -const defaultMocks = { - browserFields: mockBrowserFields, - docValueFields: mockDocValueFields, - indexPattern: mockIndexPattern, - loading: false, - selectedPatterns: mockIndexNames, -}; - -const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => ( -
-); - -const eventsViewerDefaultProps = { - browserFields: {}, - columns: [], - dataProviders: [], - deletedEventIds: [], - docValueFields: [], - end: to, - entityType: EntityType.ALERTS, - filters: [], - id: TimelineId.detectionsPage, - indexNames: mockIndexNames, - indexPattern: mockIndexPattern, - isLive: false, - isLoadingIndexPattern: false, - itemsPerPage: 10, - itemsPerPageOptions: [], - kqlMode: 'filter' as KqlMode, - query: { - query: '', - language: 'kql', - }, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - start: from, - sort: [ - { - columnId: 'foo', - columnType: 'number', - sortDirection: 'asc' as SortDirection, - }, - ], - scopeId: SourcererScopeName.timeline, - utilityBar, -}; - -describe('EventsViewer', () => { - const mount = useMountAppended(); - - let testProps = { - defaultCellActions, - defaultModel: eventsDefaultModel, - end: to, - entityType: EntityType.ALERTS, - id: TimelineId.test, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - start: from, - scopeId: SourcererScopeName.timeline, - }; - beforeEach(() => { - mockUseTimelineEvents.mockReset(); - }); - beforeAll(() => { - mockUseSourcererScope.mockImplementation(() => defaultMocks); - }); - - describe('event details', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); - }); - - test('call the right reduce action to show event details', () => { - const wrapper = mount( - - - - ); - - act(() => { - wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); - }); - - waitFor(() => { - expect(mockDispatch).toBeCalledTimes(2); - expect(mockDispatch.mock.calls[1][0]).toEqual({ - payload: { - panelView: 'eventDetail', - params: { - eventId: 'yb8TkHYBRgU82_bJu_rY', - indexName: 'auditbeat-7.10.1-2020.12.18-000001', - }, - tabType: 'query', - timelineId: TimelineId.test, - }, - type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL', - }); - }); - }); - }); - - describe('rendering', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the "Showing..." subtitle with the expected event count by default', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( - 'Showing: 12 events' - ); - }); - - test('should not render the "Showing..." subtitle with the expected event count if showTotalCount is set to false ', () => { - const disableSubTitle = { - ...eventsViewerDefaultProps, - showTotalCount: false, - }; - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual(''); - }); - - test('it renders the Fields Browser as a settings gear', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="field-browser"]`).first().exists()).toBe(true); - }); - - test('it renders the footer containing the pagination', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true); - }); - - defaultHeaders.forEach((header) => { - test(`it renders the ${header.id} default EventsViewer column header`, () => { - testProps = { - ...testProps, - // Update with a new id, to force columns back to default. - id: TimelineId.alternateTest, - }; - const wrapper = mount( - - - - ); - - defaultHeaders.forEach((h) => { - expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( - true - ); - }); - }); - }); - }); - - describe('loading', () => { - beforeAll(() => { - mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true })); - }); - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it does NOT render fetch index pattern is loading', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - - test('it does NOT render when start is empty', () => { - testProps = { - ...testProps, - start: '', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - - test('it does NOT render when end is empty', () => { - testProps = { - ...testProps, - end: '', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - }); - - describe('headerFilterGroup', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the provided headerFilterGroup', () => { - const wrapper = mount( - - - } - /> - - ); - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); - - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); - - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); - - test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).toHaveStyleRule('visibility', 'hidden'); - }); - - test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - } - /> - - ); - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); - }); - - describe('utilityBar', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); - - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); - - test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false); - }); - }); - - describe('header inspect button', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); - - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); - - test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx deleted file mode 100644 index fd644d1380ddbc..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ /dev/null @@ -1,395 +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, EuiPanel } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { useDispatch } from 'react-redux'; -import { Direction } from '../../../../common/search_strategy'; -import { BrowserFields, DocValueFields } from '../../containers/source'; -import { useTimelineEvents } from '../../../timelines/containers'; -import { useKibana } from '../../lib/kibana'; -import { KqlMode } from '../../../timelines/store/timeline/model'; -import { HeaderSection } from '../header_section'; -import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { Sort } from '../../../timelines/components/timeline/body/sort'; -import { StatefulBody } from '../../../timelines/components/timeline/body'; -import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; -import { - calculateTotalPages, - combineQueries, - resolverIsShowing, -} from '../../../timelines/components/timeline/helpers'; -import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; -import { EventDetailsWidthProvider } from './event_details_width_context'; -import * as i18n from './translations'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../store'; -import { ExitFullScreen } from '../exit_full_screen'; -import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import { - ColumnHeaderOptions, - ControlColumnProps, - RowRenderer, - TimelineId, - TimelineTabs, -} from '../../../../common/types/timeline'; -import { GraphOverlay } from '../../../timelines/components/graph_overlay'; -import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; -import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; -import { TimelineContext } from '../../../../../timelines/public'; - -export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const UTILITY_BAR_HEIGHT = 19; // px -const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px - -const UtilityBar = styled.div` - height: ${UTILITY_BAR_HEIGHT}px; -`; - -const TitleText = styled.span` - margin-right: 12px; -`; - -const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` - display: flex; - flex-direction: column; - - ${({ $isFullScreen }) => - $isFullScreen && - ` - border: 0; - box-shadow: none; - padding-top: 0; - padding-bottom: 0; - `} -`; - -const TitleFlexGroup = styled(EuiFlexGroup)` - margin-top: 8px; -`; - -const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ - className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, -}))` - width: 100%; - overflow: hidden; - flex: 1; - display: flex; - flex-direction: column; -`; - -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - -const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; -`; - -/** - * Hides stateful headerFilterGroup implementations, but prevents the component - * from being unmounted, to preserve the state of the component - */ -const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => (show ? '' : 'visibility: hidden;')} -`; - -interface Props { - browserFields: BrowserFields; - columns: ColumnHeaderOptions[]; - dataProviders: DataProvider[]; - deletedEventIds: Readonly; - docValueFields: DocValueFields[]; - end: string; - filters: Filter[]; - headerFilterGroup?: React.ReactNode; - id: TimelineId; - indexNames: string[]; - indexPattern: IIndexPattern; - isLive: boolean; - isLoadingIndexPattern: boolean; - itemsPerPage: number; - itemsPerPageOptions: number[]; - kqlMode: KqlMode; - query: Query; - onRuleChange?: () => void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - start: string; - sort: Sort[]; - showTotalCount?: boolean; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; - // If truthy, the graph viewer (Resolver) is showing - graphEventId: string | undefined; -} - -const EventsViewerComponent: React.FC = ({ - browserFields, - columns, - dataProviders, - deletedEventIds, - docValueFields, - end, - filters, - headerFilterGroup, - id, - indexNames, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - onRuleChange, - query, - renderCellValue, - rowRenderers, - start, - sort, - showTotalCount = true, - utilityBar, - graphEventId, -}) => { - const dispatch = useDispatch(); - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const kibana = useKibana(); - const [isQueryLoading, setIsQueryLoading] = useState(false); - - useEffect(() => { - dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading })); - }, [dispatch, id, isQueryLoading]); - - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); - const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); - const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id)); - - const justTitle = useMemo(() => {title}, [title]); - - const titleWithExitFullScreen = useMemo( - () => ( - - {justTitle} - - - - - ), - [globalFullScreen, justTitle, setGlobalFullScreen] - ); - - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: query, - kqlMode, - isEventViewer: true, - }); - - const canQueryTimeline = useMemo( - () => - combinedQueries != null && - isLoadingIndexPattern != null && - !isLoadingIndexPattern && - !isEmpty(start) && - !isEmpty(end), - [isLoadingIndexPattern, combinedQueries, start, end] - ); - - const fields = useMemo( - () => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])], - [columnsHeader, queryFields] - ); - - const sortField = useMemo( - () => - sort.map(({ columnId, columnType, sortDirection }) => ({ - field: columnId, - type: columnType, - direction: sortDirection as Direction, - })), - [sort] - ); - - const [loading, { events, updatedAt, inspect, loadPage, pageInfo, refetch, totalCount = 0 }] = - useTimelineEvents({ - docValueFields, - fields, - filterQuery: combinedQueries?.filterQuery, - id, - indexNames, - limit: itemsPerPage, - sort: sortField, - startDate: start, - endDate: end, - skip: !canQueryTimeline || combinedQueries?.filterQuery === undefined, // When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped - }); - - const totalCountMinusDeleted = useMemo( - () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), - [deletedEventIds.length, totalCount] - ); - - const subtitle = useMemo( - () => - showTotalCount - ? `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${unit( - totalCountMinusDeleted - )}` - : null, - [showTotalCount, totalCountMinusDeleted, unit] - ); - - const nonDeletedEvents = useMemo( - () => events.filter((e) => !deletedEventIds.includes(e._id)), - [deletedEventIds, events] - ); - - const HeaderSectionContent = useMemo( - () => - headerFilterGroup && ( - - {headerFilterGroup} - - ), - [graphEventId, headerFilterGroup] - ); - - useEffect(() => { - setIsQueryLoading(loading); - }, [loading]); - - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; - const timelineContext = useMemo(() => ({ timelineId: id }), [id]); - return ( - - {canQueryTimeline ? ( - - <> - - {HeaderSectionContent} - - {utilityBar && !resolverIsShowing(graphEventId) && ( - {utilityBar?.(refetch, totalCountMinusDeleted)} - )} - - - - - {graphEventId && } - - - -