From 23e3037ba7d662f4eddc15eb2110986bb3ac72ef Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 4 Aug 2021 15:54:58 +0200 Subject: [PATCH] [Security solution] [RAC] Add checkbox control column to t-grid (#107144) (#107630) * Add checkbox control column to t-grid * Add unit tests * Update translations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/alerts/alerts_table_t_grid.tsx | 4 +- .../timeline/body/actions/index.test.tsx | 32 +++++ .../timeline/body/actions/index.tsx | 7 +- .../body/control_columns/checkbox.test.tsx | 76 ++++++++++ .../t_grid/body/control_columns/checkbox.tsx | 59 ++++++++ .../t_grid/body/control_columns/index.tsx | 16 +++ .../body/control_columns/translations.ts | 22 +++ .../public/components/t_grid/body/index.tsx | 130 ++++++++++++------ .../components/t_grid/body/translations.ts | 15 -- .../components/t_grid/standalone/index.tsx | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 299 insertions(+), 66 deletions(-) create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index bd6844915459c..77c2b5cbca0cf 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -120,7 +120,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const leadingControlColumns = [ { id: 'expand', - width: 20, + width: 40, headerCellRender: () => { return ( @@ -149,7 +149,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { }, { id: 'view_in_app', - width: 20, + width: 40, headerCellRender: () => null, rowCellRender: ({ data }: ActionProps) => { const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index c5d39dd80c7ca..66deeddaf03f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -93,4 +93,36 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); + + test('it does NOT render a checkbox for selecting the event when `tGridEnabled` is `true`', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); + }); }); 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 93039e6fd44e5..ab34ea37efeac 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 @@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elas import { noop } from 'lodash/fp'; import styled from 'styled-components'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { eventHasNotes, getEventType, @@ -52,13 +53,14 @@ const ActionsComponent: React.FC = ({ onEventDetailsPanelOpened, onRowSelected, refetch, - onRuleChange, showCheckboxes, + onRuleChange, showNotes, timelineId, toggleShowNotes, }) => { const dispatch = useDispatch(); + const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { timelines: timelinesUi } = useKibana().services; @@ -81,6 +83,7 @@ const ActionsComponent: React.FC = ({ }), [eventId, onRowSelected] ); + const handlePinClicked = useCallback( () => getPinOnClick({ @@ -113,7 +116,7 @@ const ActionsComponent: React.FC = ({ }, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]); return ( - {showCheckboxes && ( + {showCheckboxes && !tGridEnabled && (
{loadingEventIds.includes(eventId) ? ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx new file mode 100644 index 0000000000000..e263be11c0dcc --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { render, fireEvent } from '@testing-library/react'; +import { ActionProps, HeaderActionProps, TimelineTabs } from '../../../../../common'; +import { HeaderCheckBox, RowCheckBox } from './checkbox'; +import React from 'react'; + +describe('checkbox control column', () => { + describe('RowCheckBox', () => { + const defaultProps: ActionProps = { + ariaRowindex: 1, + columnId: 'test-columnId', + columnValues: 'test-columnValues', + checked: false, + onRowSelected: jest.fn(), + eventId: 'test-event-id', + loadingEventIds: [], + onEventDetailsPanelOpened: jest.fn(), + showCheckboxes: true, + data: [], + ecsData: { + _id: 'test-ecsData-id', + }, + index: 1, + rowIndex: 1, + showNotes: true, + timelineId: 'test-timelineId', + }; + test('displays loader when id is included on loadingEventIds', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('event-loader')).not.toBeNull(); + }); + + test('calls onRowSelected when checked', () => { + const onRowSelected = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('select-event')); + + expect(onRowSelected).toHaveBeenCalled(); + }); + }); + describe('HeaderCheckBox', () => { + const defaultProps: HeaderActionProps = { + width: 99999, + browserFields: {}, + columnHeaders: [], + isSelectAllChecked: true, + onSelectAll: jest.fn(), + showEventsSelect: true, + showSelectAllCheckbox: true, + sort: [], + tabType: TimelineTabs.query, + timelineId: 'test-timelineId', + }; + + test('calls onSelectAll when checked', () => { + const onSelectAll = jest.fn(); + const { getByTestId } = render( + + ); + fireEvent.click(getByTestId('select-all-events')); + + expect(onSelectAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx new file mode 100644 index 0000000000000..cc8ec06d18dbd --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { ActionProps, HeaderActionProps } from '../../../../../common'; +import * as i18n from './translations'; + +export const RowCheckBox = ({ + eventId, + onRowSelected, + checked, + ariaRowindex, + columnValues, + loadingEventIds, +}: ActionProps) => { + const handleSelectEvent = useCallback( + (event: React.ChangeEvent) => + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }), + [eventId, onRowSelected] + ); + + return loadingEventIds.includes(eventId) ? ( + + ) : ( + + ); +}; + +export const HeaderCheckBox = ({ onSelectAll, isSelectAllChecked }: HeaderActionProps) => { + const handleSelectPageChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx new file mode 100644 index 0000000000000..dbf7fc9b99cff --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx @@ -0,0 +1,16 @@ +/* + * 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 { ControlColumnProps } from '../../../../../common'; +import { HeaderCheckBox, RowCheckBox } from './checkbox'; + +export const checkBoxControlColumn: ControlColumnProps = { + id: 'checkbox-control-column', + width: 32, + headerCellRender: HeaderCheckBox, + rowCellRender: RowCheckBox, +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts new file mode 100644 index 0000000000000..9cc4bfd58357c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const CHECKBOX_FOR_ROW = ({ + ariaRowindex, + columnValues, + checked, +}: { + ariaRowindex: number; + columnValues: string; + checked: boolean; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { + values: { ariaRowindex, checked, columnValues }, + defaultMessage: + '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); 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 61deba0459da6..1efee943c6456 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 @@ -17,7 +17,8 @@ import memoizeOne from 'memoize-one'; import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; + import type { CellValueElementProps, ColumnHeaderOptions, @@ -38,6 +39,7 @@ import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; import * as i18n from './translations'; +import { checkBoxControlColumn } from './control_columns'; interface OwnProps { activePage: number; @@ -86,6 +88,10 @@ const transformControlColumns = ({ showCheckboxes, tabType, timelineId, + isSelectAllChecked, + onSelectPage, + browserFields, + sort, }: { actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; @@ -99,11 +105,38 @@ const transformControlColumns = ({ showCheckboxes: boolean; tabType: TimelineTabs; timelineId: string; + isSelectAllChecked: boolean; + browserFields: BrowserFields; + onSelectPage: OnSelectAll; + sort: SortColumnTimeline[]; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ id: `${columnId}`, - headerCellRender: headerCellRender as ComponentType, + // eslint-disable-next-line react/display-name + headerCellRender: () => { + const HeaderActions = headerCellRender; + return ( + <> + {HeaderActions && ( + + )} + + ); + }, + // eslint-disable-next-line react/display-name rowCellRender: ({ isDetails, @@ -134,7 +167,7 @@ const transformControlColumns = ({ width={width ?? MIN_ACTION_COLUMN_WIDTH} /> ), - width: actionColumnsWidth, + width: width ?? actionColumnsWidth, }) ); @@ -188,7 +221,7 @@ export const BodyComponent = React.memo( [setSelected, id, data, selectedEventIds, queryFields] ); - const onSelectAll: OnSelectAll = useCallback( + const onSelectPage: OnSelectAll = useCallback( ({ isSelected }: { isSelected: boolean }) => isSelected ? setSelected!({ @@ -208,9 +241,9 @@ export const BodyComponent = React.memo( // Sync to selectAll so parent components can select all events useEffect(() => { if (selectAll && !isSelectAllChecked) { - onSelectAll({ isSelected: true }); + onSelectPage({ isSelected: true }); } - }, [isSelectAllChecked, onSelectAll, selectAll]); + }, [isSelectAllChecked, onSelectPage, selectAll]); const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( () => ({ @@ -250,45 +283,54 @@ export const BodyComponent = React.memo( setVisibleColumns(columnHeaders.map(({ id: cid }) => cid)); }, [columnHeaders]); - const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo( - () => - [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, - data, - id, - isEventViewer, - leadingControlColumns, - loadingEventIds, - onRowSelected, - onRuleChange, - selectedEventIds, - showCheckboxes, - tabType, + const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo(() => { + return [ + showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : 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, + isSelectAllChecked, + sort, + browserFields, + onSelectPage, + }) + ); + }, [ + columnHeaders, + data, + id, + isEventViewer, + leadingControlColumns, + loadingEventIds, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + trailingControlColumns, + isSelectAllChecked, + browserFields, + onSelectPage, + sort, + ]); const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({ columnId, 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 c45a00a0516f4..e2d13fe49f2b6 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 @@ -120,21 +120,6 @@ export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( } ); -export const CHECKBOX_FOR_ROW = ({ - ariaRowindex, - columnValues, - checked, -}: { - ariaRowindex: number; - columnValues: string; - checked: boolean; -}) => - i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { - values: { ariaRowindex, checked, columnValues }, - defaultMessage: - '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', - }); - export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({ ariaRowindex, columnValues, diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index cabedd84d270d..94ae06dc9a558 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -279,7 +279,7 @@ const TGridStandaloneComponent: React.FC = ({ sort, itemsPerPage, itemsPerPageOptions, - showCheckboxes: false, + showCheckboxes: true, }) ); dispatch( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b1dcac9cadba..7f7932aa5aa3a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22695,7 +22695,6 @@ "xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です", "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のイベントのメモをタイムラインに追加", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントをケースに追加", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "行 {ariaRowindex}、列 {columnValues} のアラートまたはイベントのチェックボックスを{checked, select, false {オフ} true {オン}}", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "縮小", "xpack.securitySolution.timeline.body.actions.expandEventTooltip": "詳細を表示", "xpack.securitySolution.timeline.body.actions.investigateInResolverDisabledTooltip": "このイベントを分析できません。フィールドマッピングの互換性がありません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dfffafc0c57d2..5e876e4aad539 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23042,7 +23042,6 @@ "xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存", "xpack.securitySolution.timeline.body.actions.addNotesForRowAriaLabel": "将事件第 {ariaRowindex} 行的备注添加到时间线,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.actions.attachAlertToCaseForRowAriaLabel": "将第 {ariaRowindex} 行的告警或事件附加到案例,其中列为 {columnValues}", - "xpack.securitySolution.timeline.body.actions.checkboxForRowAriaLabel": "告警或事件第 {ariaRowindex} 行的{checked, select, false {已取消选中} true {已选中}}复选框,其中列为 {columnValues}", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "折叠", "xpack.securitySolution.timeline.body.actions.expandEventTooltip": "查看详情", "xpack.securitySolution.timeline.body.actions.investigateInResolverDisabledTooltip": "无法分析此事件,因为其包含不兼容的字段映射",