From d3107f5c6a44141879b96e859fc5937d843b3ff4 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 20 Jul 2021 00:25:37 -0600 Subject: [PATCH 1/8] This PR migrates TGrid's rendering to use `EuiDataGrid`, per the screenshots below: ![o11y_alerts](https://user-images.githubusercontent.com/4459398/126273572-7e02e3b1-c075-4b1a-9b77-03a6843d6b72.png) ![security_solution_alerts](https://user-images.githubusercontent.com/4459398/126279321-60d8c118-a97f-4c3f-b997-a966f7755d33.png) Related RAC Issue: https://github.com/elastic/security-team/issues/1299 To reduce the size of the `timelines` and `security_solution` plugins, legacy TGrid code and the dependency on `react-beautiful-dnd` will be removed in a follow-up PR. - Related issue: https://github.com/elastic/kibana/issues/105941 The legacy code and dependencies will be deleted when the following tasks are completed (in follow-up PRs): - Sorting: Map `redux` sort state to `EuiDataGrid`'s `sorting` prop - Actions: Migrate draggable hover actions to `EuiDataGrid` `cellActions` - related PR: - Integrate with the Field Browser for adding / removing columns - related PR: - Use `BrowserFields` to display field metadata when hovering over a column - related PR: - Migrate Security Solution's actions column config from a single column to multiple columns To desk test this PR, you must enable feature flags in the Observability and Security Solution: - To desk test the `Observability > Alerts` page, add the following settings to `config/kibana.dev.yml`: ``` xpack.observability.unsafe.cases.enabled: true xpack.observability.unsafe.alertingExperience.enabled: true xpack.ruleRegistry.write.enabled: true ``` - To desk test the TGrid in the following Security Solution, edit `x-pack/plugins/security_solution/common/experimental_features.ts` and in the `allowedExperimentalValues` section set: ```typescript tGridEnabled: true, ``` --- .../common/components/events_viewer/index.tsx | 13 +- .../components/events_viewer/translations.ts | 4 + .../components/alerts_table/translations.ts | 6 + .../render_cell_value.test.tsx | 1 + .../observablity_alerts/render_cell_value.tsx | 2 + .../render_cell_value.test.tsx | 1 + .../render_cell_value.tsx | 1 + .../render_cell_value.test.tsx | 1 + .../render_cell_value.tsx | 2 + .../network/components/port/index.test.tsx | 32 +- .../public/network/components/port/index.tsx | 25 +- .../source_destination/ip_with_port.tsx | 1 + .../source_destination_ip.tsx | 1 + .../components/duration/index.test.tsx | 1 + .../timelines/components/duration/index.tsx | 27 +- .../components/formatted_ip/index.tsx | 77 ++-- .../timeline/body/actions/header_actions.tsx | 25 +- .../timeline/body/actions/index.tsx | 11 +- .../data_driven_columns/stateful_cell.tsx | 1 + .../get_column_renderer.test.tsx.snap | 1 + .../plain_column_renderer.test.tsx.snap | 1 + .../body/renderers/agent_statuses.tsx | 20 +- .../body/renderers/bytes/index.test.tsx | 8 +- .../timeline/body/renderers/bytes/index.tsx | 27 +- .../body/renderers/column_renderer.ts | 2 + .../body/renderers/empty_column_renderer.tsx | 67 ++-- .../body/renderers/formatted_field.tsx | 65 +++- .../renderers/formatted_field_helpers.tsx | 111 ++++-- .../timeline/body/renderers/host_name.tsx | 38 +- .../body/renderers/plain_column_renderer.tsx | 3 + .../timeline/body/renderers/rule_status.tsx | 13 +- .../default_cell_renderer.test.tsx | 3 + .../cell_rendering/default_cell_renderer.tsx | 2 + .../components/timeline/footer/index.tsx | 8 +- .../common/types/timeline/cells/index.ts | 1 + .../public/components/last_updated/index.tsx | 11 +- .../data_driven_columns/stateful_cell.tsx | 1 + .../public/components/t_grid/body/index.tsx | 352 ++++++++++-------- .../t_grid/body/row_action/index.tsx | 145 ++++++++ .../components/t_grid/body/translations.ts | 4 + .../components/t_grid/footer/index.test.tsx | 9 - .../public/components/t_grid/footer/index.tsx | 80 ++-- .../components/t_grid/footer/translations.ts | 6 + .../components/t_grid/integrated/index.tsx | 28 +- .../t_grid/integrated/translations.ts | 6 + .../components/t_grid/standalone/index.tsx | 29 +- .../public/components/t_grid/styles.tsx | 15 +- .../public/store/t_grid/translations.ts | 2 +- 48 files changed, 863 insertions(+), 427 deletions(-) create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx 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 bfc14a0f0c6803..c4da4e8d4506a2 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 @@ -27,6 +27,16 @@ import { CellValueElementProps } from '../../../timelines/components/timeline/ce import { useKibana } from '../../lib/kibana'; import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { EventsViewer } from './events_viewer'; +import * as i18n from './translations'; + +const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; +const leadingControlColumns: ControlColumnProps[] = [ + { + ...defaultControlColumn, + // eslint-disable-next-line react/display-name + headerCellRender: () => <>{i18n.ACTIONS}, + }, +]; const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; @@ -115,8 +125,7 @@ const StatefulEventsViewerComponent: React.FC = ({ }, []); const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; + const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts index 133ba1d98e0921..7c79bce1d73433 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts @@ -27,3 +27,7 @@ export const UNIT = (totalCount: number) => values: { totalCount }, defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, }); + +export const ACTIONS = i18n.translate('xpack.securitySolution.eventsViewer.actionsColumnLabel', { + defaultMessage: 'Actions', +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index ec86b00e3ac817..c63b4b73ae3152 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -18,6 +18,12 @@ export const ALERTS_DOCUMENT_TYPE = i18n.translate( } ); +export const ALERTS_UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.detectionEngine.alerts.alertsUnit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`, + }); + export const OPEN_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx index 7db75d3a73d907..d99fecb6bdadff 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx @@ -41,6 +41,7 @@ describe('RenderCellValue', () => { eventId, header, isDetails: false, + isDraggable: true, isExpandable: false, isExpanded: false, linkValues, 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 bc8c4bd6bfe69a..4eb885d4c9aea1 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 @@ -33,6 +33,7 @@ export const RenderCellValue: React.FC< eventId, header, isDetails, + isDraggable, isExpandable, isExpanded, linkValues, @@ -71,6 +72,7 @@ export const RenderCellValue: React.FC< eventId={eventId} header={header} isDetails={isDetails} + isDraggable={isDraggable} isExpandable={isExpandable} isExpanded={isExpanded} linkValues={linkValues} diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx index a8f295df2540d8..ccd71404a22161 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx @@ -41,6 +41,7 @@ describe('RenderCellValue', () => { eventId, header, isDetails: false, + isDraggable: false, isExpandable: false, isExpanded: false, linkValues, 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 097cb54a7b0ef3..879712c85327ec 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 @@ -67,6 +67,7 @@ export const RenderCellValue: React.FC< eventId={eventId} header={header} isDetails={isDetails} + isDraggable={false} isExpandable={isExpandable} isExpanded={isExpanded} linkValues={linkValues} diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx index 965ee913a1daa0..a7def2a23ef1d8 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx @@ -41,6 +41,7 @@ describe('RenderCellValue', () => { eventId, header, isDetails: false, + isDraggable: false, isExpandable: false, isExpanded: false, linkValues, diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index e9bfdefa433c27..72914507bb6a66 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -22,6 +22,7 @@ export const RenderCellValue: React.FC< columnId, data, eventId, + isDraggable, header, isDetails, isExpandable, @@ -35,6 +36,7 @@ export const RenderCellValue: React.FC< columnId={columnId} data={data} eventId={eventId} + isDraggable={isDraggable} header={header} isDetails={isDetails} isExpandable={isExpandable} diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index dd7ad20d2384a3..fc48a022946d5b 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -31,7 +31,13 @@ describe('Port', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -39,7 +45,13 @@ describe('Port', () => { test('it renders the port', () => { const wrapper = mount( - + ); @@ -51,7 +63,13 @@ describe('Port', () => { test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { const wrapper = mount( - + ); @@ -65,7 +83,13 @@ describe('Port', () => { test('it renders only one external link icon', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.tsx index 8ee1616d4c77bc..df288c1abfb06c 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.tsx @@ -29,17 +29,22 @@ export const Port = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; value: string | undefined | null; -}>(({ contextId, eventId, fieldName, value }) => ( - +}>(({ contextId, eventId, fieldName, isDraggable, value }) => + isDraggable ? ( + + + + ) : ( - -)); + ) +); Port.displayName = 'Port'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx index 9a0a79a8902b64..17b55c4229fcc7 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/ip_with_port.tsx @@ -39,6 +39,7 @@ const PortWithSeparator = React.memo<{ data-test-subj="port" eventId={eventId} fieldName={portFieldName} + isDraggable={true} value={port} /> diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx index 288364d1eb0cb3..db9773789bf549 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx @@ -202,6 +202,7 @@ export const SourceDestinationIp = React.memo( data-test-subj="port" eventId={eventId} fieldName={`${type}.port`} + isDraggable={true} value={port} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx index ea8317346cd998..e868b3e4c21dd7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx @@ -26,6 +26,7 @@ describe('Duration', () => { contextId="test" eventId="abc" fieldName="event.duration" + isDraggable={true} value={`${ONE_MILLISECOND_AS_NANOSECONDS}`} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx index 9d2a6e1f70a5da..421ba5941eaefc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.tsx @@ -20,18 +20,23 @@ export const Duration = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - +}>(({ contextId, eventId, fieldName, isDraggable, value }) => + isDraggable ? ( + + + + ) : ( - -)); + ) +); Duration.displayName = 'Duration'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx index 8cdb263fe42bbc..235b3f0b9300ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx @@ -85,9 +85,10 @@ const NonDecoratedIpComponent: React.FC<{ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; truncate?: boolean; value: string | object | null | undefined; -}> = ({ contextId, eventId, fieldName, truncate, value }) => { +}> = ({ contextId, eventId, fieldName, isDraggable, truncate, value }) => { const key = useMemo( () => `non-decorated-ip-draggable-wrapper-${getUniqueId({ @@ -104,20 +105,30 @@ const NonDecoratedIpComponent: React.FC<{ [contextId, eventId, fieldName, value] ); + const content = useMemo( + () => + typeof value !== 'object' + ? getOrEmptyTagFromValue(value) + : getOrEmptyTagFromValue(tryStringify(value)), + [value] + ); + const render = useCallback( (dataProvider, _, snapshot) => snapshot.isDragging ? ( - ) : typeof value !== 'object' ? ( - getOrEmptyTagFromValue(value) ) : ( - getOrEmptyTagFromValue(tryStringify(value)) + content ), - [value] + [content] ); + if (!isDraggable) { + return content; + } + return ( = ({ contextId, eventId, fieldName, + isDraggable, truncate, }) => { const key = `address-links-draggable-wrapper-${getUniqueId({ @@ -189,6 +201,23 @@ const AddressLinksItemComponent: React.FC = ({ [eventContext, isInTimelineContext, address, fieldName, dispatch] ); + const content = useMemo( + () => ( + + + {address} + + + ), + [address, fieldName, formatUrl, isInTimelineContext, openNetworkDetailsSidePanel] + ); + const render = useCallback( (_props, _provided, snapshot) => snapshot.isDragging ? ( @@ -196,28 +225,15 @@ const AddressLinksItemComponent: React.FC = ({ ) : ( - - - {address} - - + content ), - [ - dataProviderProp, - fieldName, - address, - formatUrl, - isInTimelineContext, - openNetworkDetailsSidePanel, - ] + [dataProviderProp, content] ); + if (!isDraggable) { + return content; + } + return ( = ({ contextId, eventId, fieldName, + isDraggable, truncate, }) => { const uniqAddresses = useMemo(() => uniq(addresses), [addresses]); @@ -256,10 +274,11 @@ const AddressLinksComponent: React.FC = ({ contextId={contextId} eventId={eventId} fieldName={fieldName} + isDraggable={isDraggable} truncate={truncate} /> )), - [contextId, eventId, fieldName, truncate, uniqAddresses] + [contextId, eventId, fieldName, isDraggable, truncate, uniqAddresses] ); return <>{content}; @@ -271,6 +290,7 @@ const AddressLinks = React.memo( prevProps.contextId === nextProps.contextId && prevProps.eventId === nextProps.eventId && prevProps.fieldName === nextProps.fieldName && + prevProps.isDraggable === nextProps.isDraggable && prevProps.truncate === nextProps.truncate && deepEqual(prevProps.addresses, nextProps.addresses) ); @@ -279,9 +299,10 @@ const FormattedIpComponent: React.FC<{ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; truncate?: boolean; value: string | object | null | undefined; -}> = ({ contextId, eventId, fieldName, truncate, value }) => { +}> = ({ contextId, eventId, fieldName, isDraggable, truncate, value }) => { if (isString(value) && !isEmpty(value)) { try { const addresses = JSON.parse(value); @@ -292,6 +313,7 @@ const FormattedIpComponent: React.FC<{ contextId={contextId} eventId={eventId} fieldName={fieldName} + isDraggable={isDraggable} truncate={truncate} /> ); @@ -306,6 +328,7 @@ const FormattedIpComponent: React.FC<{ addresses={[value]} contextId={contextId} eventId={eventId} + isDraggable={isDraggable} fieldName={fieldName} truncate={truncate} /> @@ -316,6 +339,7 @@ const FormattedIpComponent: React.FC<{ contextId={contextId} eventId={eventId} fieldName={fieldName} + isDraggable={isDraggable} truncate={truncate} value={value} /> @@ -329,6 +353,7 @@ export const FormattedIp = React.memo( prevProps.contextId === nextProps.contextId && prevProps.eventId === nextProps.eventId && prevProps.fieldName === nextProps.fieldName && + prevProps.isDraggable === nextProps.isDraggable && prevProps.truncate === nextProps.truncate && deepEqual(prevProps.value, nextProps.value) ); 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 d5c1085d506b5d..fb483190577888 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 @@ -51,6 +51,11 @@ const SortingColumnsContainer = styled.div` } `; +const ActionsContainer = styled.div` + align-items: center; + display: flex; +`; + const HeaderActionsComponent: React.FC = ({ width, browserFields, @@ -111,35 +116,36 @@ const HeaderActionsComponent: React.FC = ({ const sortedColumns = useMemo( () => ({ onSort: onSortColumns, - columns: sort.map<{ id: string; direction: 'asc' | 'desc' }>( - ({ columnId, sortDirection }) => ({ + columns: + sort?.map<{ id: string; direction: 'asc' | 'desc' }>(({ columnId, sortDirection }) => ({ id: columnId, direction: sortDirection as 'asc' | 'desc', - }) - ), + })) ?? [], }), [onSortColumns, sort] ); const displayValues = useMemo( - () => columnHeaders.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.displayAsText ?? ch.id }), {}), + () => + columnHeaders?.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.displayAsText ?? ch.id }), {}) ?? + {}, [columnHeaders] ); const myColumns = useMemo( () => - columnHeaders.map(({ aggregatable, displayAsText, id, type }) => ({ + columnHeaders?.map(({ aggregatable, displayAsText, id, type }) => ({ id, isSortable: aggregatable, displayAsText, schema: type, - })), + })) ?? [], [columnHeaders] ); const ColumnSorting = useDataGridColumnSorting(myColumns, sortedColumns, {}, [], displayValues); return ( - <> + {showSelectAllCheckbox && ( @@ -207,10 +213,9 @@ const HeaderActionsComponent: React.FC = ({ )} - + ); }; - HeaderActionsComponent.displayName = 'HeaderActionsComponent'; export const HeaderActions = React.memo(HeaderActionsComponent); 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 29e00d169b4e4b..e317b3cc140acc 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 @@ -9,6 +9,8 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; +import styled from 'styled-components'; + import { eventHasNotes, getEventType, @@ -28,6 +30,11 @@ import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/ty import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; +const ActionsContainer = styled.div` + align-items: center; + display: flex; +`; + const ActionsComponent: React.FC = ({ ariaRowindex, width, @@ -93,7 +100,7 @@ const ActionsComponent: React.FC = ({ ); return ( - <> + {showCheckboxes && (
@@ -179,7 +186,7 @@ const ActionsComponent: React.FC = ({ onRuleChange={onRuleChange} /> - + ); }; 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 index 7931e0739aa68a..403756a763808b 100644 --- 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 @@ -47,6 +47,7 @@ const StatefulCellComponent = ({ eventId, data, header, + isDraggable: true, isExpandable: true, isExpanded: false, isDetails: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap index 4da4e12e0f7b3c..5c42306f563df8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap @@ -8,6 +8,7 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = ` fieldFormat="" fieldName="event.severity" fieldType="" + isDraggable={true} key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3-0" value="3" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap index 13912e6ad3da92..b9859fc5453b7f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap @@ -8,6 +8,7 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`] fieldFormat="" fieldName="event.category" fieldType="keyword" + isDraggable={true} key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access-0" value="Access" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx index dac10f46487841..417cf0ceee1846 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -18,11 +18,13 @@ export const AgentStatuses = React.memo( fieldName, contextId, eventId, + isDraggable, value, }: { fieldName: string; contextId: string; eventId: string; + isDraggable: boolean; value: string; }) => { const { @@ -36,14 +38,18 @@ export const AgentStatuses = React.memo( {agentStatus !== undefined ? ( - + {isDraggable ? ( + + + + ) : ( - + )} ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx index c7da6f758766e0..8930a813cde6f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx @@ -22,7 +22,13 @@ describe('Bytes', () => { test('it renders the expected formatted bytes', () => { const wrapper = mount( - + ); expect(wrapper.find(PreferenceFormattedBytes).first().text()).toEqual('1.2MB'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx index 25b58dac918dd8..e2418334dfc804 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.tsx @@ -20,18 +20,23 @@ export const Bytes = React.memo<{ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - +}>(({ contextId, eventId, fieldName, isDraggable, value }) => + isDraggable ? ( + + + + ) : ( - -)); + ) +); Bytes.displayName = 'Bytes'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts index 65bb67458ab2a8..fc13680b81be2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts @@ -15,6 +15,7 @@ export interface ColumnRenderer { columnName, eventId, field, + isDraggable, timelineId, truncate, values, @@ -23,6 +24,7 @@ export interface ColumnRenderer { columnName: string; eventId: string; field: ColumnHeaderOptions; + isDraggable?: boolean; timelineId: string; truncate?: boolean; values: string[] | null | undefined; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx index 37873df7f4e7be..8e2335a2f149b9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx @@ -31,43 +31,48 @@ export const emptyColumnRenderer: ColumnRenderer = { columnName, eventId, field, + isDraggable = true, timelineId, truncate, }: { columnName: string; eventId: string; field: ColumnHeaderOptions; + isDraggable?: boolean; timelineId: string; truncate?: boolean; - }) => ( - - snapshot.isDragging ? ( - - - - ) : ( - {getEmptyValue()} - ) - } - truncate={truncate} - /> - ), + }) => + isDraggable ? ( + + snapshot.isDragging ? ( + + + + ) : ( + {getEmptyValue()} + ) + } + truncate={truncate} + /> + ) : ( + {getEmptyValue()} + ), }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 1d04849b198ad8..aa6c7beb9139e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -50,6 +50,7 @@ const FormattedFieldValueComponent: React.FC<{ fieldFormat?: string; fieldName: string; fieldType?: string; + isDraggable?: boolean; truncate?: boolean; value: string | number | undefined | null; linkValue?: string | null | undefined; @@ -60,6 +61,7 @@ const FormattedFieldValueComponent: React.FC<{ fieldName, fieldType, isObjectArray = false, + isDraggable = true, truncate, value, linkValue, @@ -72,6 +74,7 @@ const FormattedFieldValueComponent: React.FC<{ eventId={eventId} contextId={contextId} fieldName={fieldName} + isDraggable={isDraggable} value={!isNumber(value) ? value : String(value)} truncate={truncate} /> @@ -79,7 +82,7 @@ const FormattedFieldValueComponent: React.FC<{ } else if (fieldType === GEO_FIELD_TYPE) { return <>{value}; } else if (fieldType === DATE_FIELD_TYPE) { - return ( + return isDraggable ? ( + ) : ( + ); } else if (PORT_NAMES.some((portName) => fieldName === portName)) { return ( - + ); } else if (fieldName === EVENT_DURATION_FIELD_NAME) { return ( - + ); } else if (fieldName === HOST_NAME_FIELD_NAME) { - return ; + return ( + + ); } else if (fieldFormat === BYTES_FORMAT) { return ( - + ); } else if (fieldName === SIGNAL_RULE_NAME_FIELD_NAME) { return ( @@ -109,16 +140,31 @@ const FormattedFieldValueComponent: React.FC<{ contextId={contextId} eventId={eventId} fieldName={fieldName} + isDraggable={isDraggable} linkValue={linkValue} truncate={truncate} value={value} /> ); } else if (fieldName === EVENT_MODULE_FIELD_NAME) { - return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value }); + return renderEventModule({ + contextId, + eventId, + fieldName, + isDraggable, + linkValue, + truncate, + value, + }); } else if (fieldName === SIGNAL_STATUS_FIELD_NAME) { return ( - + ); } else if (fieldName === AGENT_STATUS_FIELD_NAME) { return ( @@ -126,6 +172,7 @@ const FormattedFieldValueComponent: React.FC<{ contextId={contextId} eventId={eventId} fieldName={fieldName} + isDraggable={isDraggable} value={typeof value === 'string' ? value : ''} /> ); @@ -137,8 +184,8 @@ const FormattedFieldValueComponent: React.FC<{ INDICATOR_REFERENCE, ].includes(fieldName) ) { - return renderUrl({ contextId, eventId, fieldName, linkValue, truncate, value }); - } else if (columnNamesNotDraggable.includes(fieldName)) { + return renderUrl({ contextId, eventId, fieldName, linkValue, isDraggable, truncate, value }); + } else if (columnNamesNotDraggable.includes(fieldName) || !isDraggable) { return truncate && !isEmpty(value) ? ( = ({ contextId, eventId, fieldName, + isDraggable, linkValue, truncate, value, @@ -63,13 +65,8 @@ export const RenderRuleName: React.FC = ({ [navigateToApp, ruleId, search] ); - return isString(value) && ruleName.length > 0 && ruleId != null ? ( - + if (isString(value) && ruleName.length > 0 && ruleId != null) { + const link = ( = ({ > {content} - - ) : value != null ? ( - - {value} - - ) : ( - getEmptyTagValue() - ); + ); + + return isDraggable ? ( + + {link} + + ) : ( + link + ); + } else if (value != null) { + return isDraggable ? ( + + {value} + + ) : ( + <>{value} + ); + } + + return getEmptyTagValue(); }; const canYouAddEndpointLogo = (moduleName: string, endpointUrl: string | null | undefined) => @@ -105,6 +119,7 @@ export const renderEventModule = ({ contextId, eventId, fieldName, + isDraggable, linkValue, truncate, value, @@ -112,6 +127,7 @@ export const renderEventModule = ({ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; linkValue: string | null | undefined; truncate?: boolean; value: string | number | null | undefined; @@ -130,14 +146,18 @@ export const renderEventModule = ({ } > - - {content} - + {isDraggable ? ( + + {content} + + ) : ( + <>{content} + )} {endpointRefUrl != null && canYouAddEndpointLogo(moduleName, endpointRefUrl) && ( @@ -166,6 +186,7 @@ export const renderUrl = ({ contextId, eventId, fieldName, + isDraggable, linkValue, truncate, value, @@ -173,28 +194,38 @@ export const renderUrl = ({ contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; linkValue: string | null | undefined; truncate?: boolean; value: string | number | null | undefined; }) => { const urlName = `${value}`; - const content = truncate ? {value} : value; - - return isString(value) && urlName.length > 0 ? ( - + const formattedValue = truncate ? {value} : value; + const content = ( + <> {!isUrlInvalid(urlName) && ( - {content} + {formattedValue} )} - {isUrlInvalid(urlName) && <>{content}} - + {isUrlInvalid(urlName) && <>{formattedValue}} + + ); + + return isString(value) && urlName.length > 0 ? ( + isDraggable ? ( + + {content} + + ) : ( + content + ) ) : ( getEmptyTagValue() ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index e40ccec7bef342..abd4731ec4b668 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { isString } from 'lodash/fp'; import { LinkAnchor } from '../../../../../common/components/links'; @@ -27,10 +27,17 @@ interface Props { contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; value: string | number | undefined | null; } -const HostNameComponent: React.FC = ({ fieldName, contextId, eventId, value }) => { +const HostNameComponent: React.FC = ({ + fieldName, + contextId, + eventId, + isDraggable, + value, +}) => { const dispatch = useDispatch(); const eventContext = useContext(StatefulEventContext); const hostName = `${value}`; @@ -66,13 +73,8 @@ const HostNameComponent: React.FC = ({ fieldName, contextId, eventId, val [dispatch, eventContext, isInTimelineContext, hostName] ); - return isString(value) && hostName.length > 0 ? ( - + const content = useMemo( + () => ( = ({ fieldName, contextId, eventId, val > {hostName} - + ), + [formatUrl, hostName, isInTimelineContext, openHostDetailsSidePanel] + ); + + return isString(value) && hostName.length > 0 ? ( + isDraggable ? ( + + {content} + + ) : ( + content + ) ) : ( getEmptyTagValue() ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx index 77039ddc4a586c..8509e7be0d22bf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx @@ -26,6 +26,7 @@ export const plainColumnRenderer: ColumnRenderer = { columnName, eventId, field, + isDraggable = true, timelineId, truncate, values, @@ -34,6 +35,7 @@ export const plainColumnRenderer: ColumnRenderer = { columnName: string; eventId: string; field: ColumnHeaderOptions; + isDraggable?: boolean; timelineId: string; truncate?: boolean; values: string[] | undefined | null; @@ -48,6 +50,7 @@ export const plainColumnRenderer: ColumnRenderer = { fieldFormat={field.format || ''} fieldName={columnName} fieldType={field.type || ''} + isDraggable={isDraggable} value={parseValue(value)} truncate={truncate} linkValue={head(linkValues)} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index 08a16437ff5457..126bfae996ef7f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -26,12 +26,19 @@ interface Props { contextId: string; eventId: string; fieldName: string; + isDraggable: boolean; value: string | number | undefined | null; } -const RuleStatusComponent: React.FC = ({ contextId, eventId, fieldName, value }) => { +const RuleStatusComponent: React.FC = ({ + contextId, + eventId, + fieldName, + isDraggable, + value, +}) => { const color = useMemo(() => getOr('default', `${value}`, mapping), [value]); - return ( + return isDraggable ? ( = ({ contextId, eventId, fieldName, v > {value} + ) : ( + {value} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx index 06d8133a24f6e6..5282276f8bb51e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -55,6 +55,7 @@ describe('DefaultCellRenderer', () => { eventId={eventId} header={header} isDetails={isDetails} + isDraggable={true} isExpandable={isExpandable} isExpanded={isExpanded} linkValues={linkValues} @@ -84,6 +85,7 @@ describe('DefaultCellRenderer', () => { eventId={eventId} header={header} isDetails={isDetails} + isDraggable={true} isExpandable={isExpandable} isExpanded={isExpanded} linkValues={linkValues} @@ -100,6 +102,7 @@ describe('DefaultCellRenderer', () => { columnName: header.id, eventId, field: header, + isDraggable: true, linkValues, timelineId, truncate: true, 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 8d8f821107e7bc..d2652ed063fc7c 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,6 +18,7 @@ export const DefaultCellRenderer: React.FC = ({ data, eventId, header, + isDraggable, linkValues, setCellProps, timelineId, @@ -27,6 +28,7 @@ export const DefaultCellRenderer: React.FC = ({ columnName: header.id, eventId, field: header, + isDraggable, linkValues, timelineId, truncate: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 2a253087567a74..f68538703951af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -376,6 +376,10 @@ export const FooterComponent = ({ + + + + {isLive ? ( @@ -407,10 +411,6 @@ export const FooterComponent = ({ /> )} - - - - ); diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts index ad70d8bba82fd3..2a6e1b3e12bcf7 100644 --- a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts @@ -14,6 +14,7 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { data: TimelineNonEcsData[]; eventId: string; // _id header: ColumnHeaderOptions; + isDraggable: boolean; linkValues: string[] | undefined; timelineId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/timelines/public/components/last_updated/index.tsx b/x-pack/plugins/timelines/public/components/last_updated/index.tsx index 344cb36791dd55..f60b0e147b6895 100644 --- a/x-pack/plugins/timelines/public/components/last_updated/index.tsx +++ b/x-pack/plugins/timelines/public/components/last_updated/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import React, { useEffect, useMemo, useState } from 'react'; @@ -66,14 +66,9 @@ export const LastUpdatedAt = React.memo( return ( - - - } + content={} > - - + {updateText} diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx index 82d872d30c273d..9ee64e0e45be3a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx @@ -48,6 +48,7 @@ const StatefulCellComponent = ({ eventId, data, header, + isDraggable: true, isExpandable: true, isExpanded: false, isDetails: false, 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 51227c0e811f26..d0b2822fd5abef 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 @@ -5,20 +5,19 @@ * 2.0. */ -import { noop } from 'lodash/fp'; +import { + EuiDataGrid, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + EuiDataGridStyle, + EuiDataGridToolBarVisibilityOptions, +} from '@elastic/eui'; +import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; -import { - ARIA_COLINDEX_ATTRIBUTE, - ARIA_ROWINDEX_ATTRIBUTE, - FIRST_ARIA_INDEX, - onKeyDownFocusHandler, -} from '../../../../common'; -import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; -import { RowRendererId, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; // eslint-disable-next-line no-duplicate-imports import type { CellValueElementProps, @@ -26,35 +25,34 @@ import type { ControlColumnProps, RowRenderer, } from '../../../../common/types/timeline'; -import type { TimelineItem } from '../../../../common/search_strategy/timeline'; +import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; import { Sort } from './sort'; -import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; -import { ColumnHeaders } from './column_headers'; -import { Events } from './events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { OnRowSelected, OnSelectAll } from '../types'; import { tGridActions } from '../../../'; import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; -import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { RowAction } from './row_action'; +import * as i18n from './translations'; interface OwnProps { activePage: number; + additionalControls?: React.ReactNode; browserFields: BrowserFields; data: TimelineItem[]; id: string; isEventViewer?: boolean; - sort: Sort[]; + leadingControlColumns: ControlColumnProps[]; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; + sort: Sort[]; tabType: TimelineTabs; + trailingControlColumns: ControlColumnProps[]; totalPages: number; onRuleChange?: () => void; } @@ -68,6 +66,79 @@ export const hasAdditionalActions = (id: TimelineId): boolean => const EXTRA_WIDTH = 4; // px +const MIN_ACTION_COLUMN_WIDTH = 96; // px + +const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; + +const EmptyHeaderCellRender: ComponentType = () => null; + +const gridStyle: EuiDataGridStyle = { border: 'none', header: 'underline' }; + +const transformControlColumns = ({ + actionColumnsWidth, + columnHeaders, + controlColumns, + data, + isEventViewer = false, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, +}: { + actionColumnsWidth: number; + columnHeaders: ColumnHeaderOptions[]; + controlColumns: ControlColumnProps[]; + data: TimelineItem[]; + isEventViewer?: boolean; + loadingEventIds: string[]; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + selectedEventIds: Record; + showCheckboxes: boolean; + tabType: TimelineTabs; + timelineId: string; +}): EuiDataGridControlColumn[] => + controlColumns.map( + ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ + id: `${columnId}`, + headerCellRender: headerCellRender as ComponentType, + // eslint-disable-next-line react/display-name + rowCellRender: ({ + isDetails, + isExpandable, + isExpanded, + rowIndex, + setCellProps, + }: EuiDataGridCellValueElementProps) => ( + + ), + width: actionColumnsWidth, + }) + ); + export type StatefulBodyProps = OwnProps & PropsFromRedux; /** @@ -77,6 +148,7 @@ export type StatefulBodyProps = OwnProps & PropsFromRedux; export const BodyComponent = React.memo( ({ activePage, + additionalControls, browserFields, columnHeaders, data, @@ -95,10 +167,9 @@ export const BodyComponent = React.memo( sort, tabType, totalPages, - leadingControlColumns = [], - trailingControlColumns = [], + leadingControlColumns = EMPTY_CONTROL_COLUMNS, + trailingControlColumns = EMPTY_CONTROL_COLUMNS, }) => { - const containerRef = useRef(null); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const { queryFields, selectAll } = useDeepEqualSelector((state) => getManageTimeline(state, id) @@ -141,152 +212,117 @@ export const BodyComponent = React.memo( } }, [isSelectAllChecked, onSelectAll, selectAll]); - const enabledRowRenderers = useMemo(() => { - if ( - excludedRowRendererIds && - excludedRowRendererIds.length === Object.keys(RowRendererId).length - ) - return [plainRowRenderer]; - - if (!excludedRowRendererIds) return rowRenderers; + const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( + () => ({ + additionalControls, + showColumnSelector: { allowHide: false, allowReorder: true }, + showStyleSelector: false, + }), + [additionalControls] + ); - return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds, rowRenderers]); + const [sortingColumns, setSortingColumns] = useState([]); - 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 onSort = useCallback( + (columns) => { + setSortingColumns(columns); + }, + [setSortingColumns] ); - const columnWidths = useMemo( + const [visibleColumns, setVisibleColumns] = useState(() => + columnHeaders.map(({ id: cid }) => cid) + ); // initializes to the full set of columns + + useEffect(() => { + setVisibleColumns(columnHeaders.map(({ id: cid }) => cid)); + }, [columnHeaders]); + + const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo( () => - columnHeaders.reduce( - (totalWidth, header) => totalWidth + (header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH), - 0 + [leadingControlColumns, trailingControlColumns].map((controlColumns) => + transformControlColumns({ + columnHeaders, + 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, + selectedEventIds, + showCheckboxes, + tabType, + timelineId: id, + }) ), - [columnHeaders] + [ + columnHeaders, + data, + id, + isEventViewer, + leadingControlColumns, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + trailingControlColumns, + ] ); - 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] - ); + const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({ + columnId, + rowIndex, + setCellProps, + }) => { + const rowData = rowIndex < data.length ? data[rowIndex].data : null; + const header = columnHeaders.find((h) => h.id === columnId); + const eventId = rowIndex < data.length ? data[rowIndex]._id : null; + + if (rowData == null || header == null || eventId == null) { + return null; + } + + return renderCellValue({ + columnId: header.id, + eventId, + data: rowData, + header, + isDraggable: false, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), + rowIndex, + setCellProps, + timelineId: tabType != null ? `${id}-${tabType}` : id, + }); + }; + return ( - <> - - - - - - - - - + ); - }, - (prevProps, nextProps) => - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - deepEqual(prevProps.data, nextProps.data) && - deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.selectedEventIds, nextProps.selectedEventIds) && - deepEqual(prevProps.loadingEventIds, nextProps.loadingEventIds) && - prevProps.id === nextProps.id && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.renderCellValue === nextProps.renderCellValue && - prevProps.rowRenderers === nextProps.rowRenderers && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.tabType === nextProps.tabType + } ); BodyComponent.displayName = 'BodyComponent'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx new file mode 100644 index 00000000000000..408528cd0b898f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx @@ -0,0 +1,145 @@ +/* + * 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 { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { + ColumnHeaderOptions, + ControlColumnProps, + OnRowSelected, + TimelineExpandedDetailType, + TimelineTabs, +} from '../../../../../common/types/timeline'; +import { getMappedNonEcsValue } from '../data_driven_columns'; +import { tGridActions } from '../../../../store/t_grid'; + +type Props = EuiDataGridCellValueElementProps & { + columnHeaders: ColumnHeaderOptions[]; + controlColumn: ControlColumnProps; + data: TimelineItem[]; + index: number; + isEventViewer: boolean; + loadingEventIds: Readonly; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + selectedEventIds: Readonly>; + showCheckboxes: boolean; + tabType?: TimelineTabs; + timelineId: string; + width: number; +}; + +const RowActionComponent = ({ + columnHeaders, + controlColumn, + data, + index, + isEventViewer, + loadingEventIds, + onRowSelected, + onRuleChange, + rowIndex, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + width, +}: Props) => { + const dispatch = useDispatch(); + + const LeadingActions = useMemo(() => { + if (data.length === 0 || rowIndex >= data.length) { + return ; + } + + const { data: timelineNonEcsData, ecs: ecsData, _id: eventId, _index: indexName } = data[ + rowIndex + ]; + + const handleOnEventDetailPanelOpened = () => { + const updatedExpandedDetail: TimelineExpandedDetailType = { + panelView: 'eventDetail', + params: { + eventId, + indexName: indexName ?? '', + }, + }; + + dispatch( + tGridActions.toggleDetailPanel({ + ...updatedExpandedDetail, + tabType, + timelineId, + }) + ); + }; + + const columnValues = columnHeaders + .map( + (header) => + getMappedNonEcsValue({ + data: timelineNonEcsData, + fieldName: header.id, + }) ?? [] + ) + .join(' '); + + const Action = controlColumn.rowCellRender; + + return ( + <> + {Action && ( + + )} + + ); + }, [ + columnHeaders, + controlColumn.id, + controlColumn.rowCellRender, + data, + dispatch, + index, + isEventViewer, + loadingEventIds, + onRowSelected, + onRuleChange, + rowIndex, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + width, + ]); + + return <>{LeadingActions}; +}; + +export const RowAction = React.memo(RowActionComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts index 1a00a4eaf6bc6f..c45a00a0516f47 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts @@ -14,6 +14,10 @@ export const NOTES_TOOLTIP = i18n.translate( } ); +export const TGRID_BODY_ARIA_LABEL = i18n.translate('xpack.timelines.tgrid.body.ariaLabel', { + defaultMessage: 'Alerts', +}); + export const NOTES_DISABLE_TOOLTIP = i18n.translate( 'xpack.timelines.timeline.body.notes.disableEventTooltip', { diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx index fe57ab8d2d0f3d..c5f8cb4afd3187 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx @@ -14,7 +14,6 @@ import { FooterComponent, PagingControlComponent } from './index'; describe('Footer Timeline Component', () => { const loadMore = jest.fn(); - const updatedAt = 1546878704036; const serverSideEventCount = 15546; const itemsCount = 2; @@ -24,7 +23,6 @@ describe('Footer Timeline Component', () => { { { { { { { { width < 600; @@ -100,6 +97,7 @@ export const EventsCountComponent = ({ isOpen, items, itemsCount, + itemsPerPage, onClick, serverSideEventCount, }: { @@ -108,51 +106,40 @@ export const EventsCountComponent = ({ isOpen: boolean; items: React.ReactElement[]; itemsCount: number; + itemsPerPage: number; onClick: () => void; serverSideEventCount: number; footerText: string | React.ReactNode; }) => { - const totalCount = useMemo(() => (serverSideEventCount > 0 ? serverSideEventCount : 0), [ - serverSideEventCount, - ]); - return ( -
- - - {itemsCount} - - - {` ${i18n.OF} `} - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" + const button = useMemo( + () => ( + - - - - - - {totalCount} - {' '} - {documentType} - - -
+ {i18n.ROWS_PER_PAGE(itemsPerPage)} + + ), + [itemsPerPage, onClick] + ); + + return ( + + + ); }; @@ -211,7 +198,6 @@ export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; interface FooterProps { - updatedAt: number; activePage: number; height: number; id: string; @@ -227,7 +213,6 @@ interface FooterProps { /** Renders a loading indicator and paging controls */ export const FooterComponent = ({ activePage, - updatedAt, height, id, isLive, @@ -341,6 +326,7 @@ export const FooterComponent = ({ isOpen={isPopoverOpen} items={rowItems} itemsCount={itemsCount} + itemsPerPage={itemsPerPage} onClick={onButtonClick} serverSideEventCount={totalCount} /> @@ -378,10 +364,6 @@ export const FooterComponent = ({ /> )} - - - - ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts index e237ca39e10abc..c2417f34530652 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts @@ -27,6 +27,12 @@ export const LOADING = i18n.translate('xpack.timelines.footer.loadingLabel', { defaultMessage: 'Loading', }); +export const ROWS_PER_PAGE = (rowsPerPage: number) => + i18n.translate('xpack.timelines.footer.rowsPerPageLabel', { + values: { rowsPerPage }, + defaultMessage: `Rows per page: {rowsPerPage}`, + }); + export const TOTAL_COUNT_OF_EVENTS = i18n.translate('xpack.timelines.footer.totalCountOfEvents', { defaultMessage: 'events', }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index d52174b02f88eb..a6ded88fae96bf 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -41,7 +41,8 @@ import { useTimelineEvents } from '../../../container'; import { HeaderSection } from '../header_section'; import { StatefulBody } from '../body'; import { Footer, footerHeight } from '../footer'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../styles'; +import { LastUpdatedAt } from '../..'; +import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles'; import * as i18n from './translations'; import { ExitFullScreen } from '../../exit_full_screen'; import { Sort } from '../body/sort'; @@ -49,7 +50,7 @@ import { InspectButtonContainer } from '../../inspect'; 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 COMPACT_HEADER_HEIGHT = 36; // px const UtilityBar = styled.div` height: ${UTILITY_BAR_HEIGHT}px; @@ -176,7 +177,7 @@ const TGridIntegratedComponent: React.FC = ({ const [isQueryLoading, setIsQueryLoading] = useState(false); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); + const unit = useMemo(() => (n: number) => i18n.ALERTS_UNIT(n), []); const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id ?? '') ); @@ -257,13 +258,12 @@ const TGridIntegratedComponent: React.FC = ({ ); const subtitle = useMemo( - () => - `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${ - unit && unit(totalCountMinusDeleted) - }`, + () => `${totalCountMinusDeleted.toLocaleString()} ${unit && unit(totalCountMinusDeleted)}`, [totalCountMinusDeleted, unit] ); + const additionalControls = useMemo(() => {subtitle}, [subtitle]); + const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ deletedEventIds, events, @@ -295,8 +295,10 @@ const TGridIntegratedComponent: React.FC = ({ id={!resolverIsShowing(graphEventId) ? id : undefined} inspect={inspect} loading={loading} - height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT} - subtitle={utilityBar ? undefined : subtitle} + height={ + headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT + } + subtitle={utilityBar} title={globalFullScreen ? titleWithExitFullScreen : justTitle} > {HeaderSectionContent} @@ -308,10 +310,17 @@ const TGridIntegratedComponent: React.FC = ({ data-timeline-id={id} data-test-subj={`events-container-loading-${loading}`} > + + + + + + = ({