[Security solution] [RAC] Add checkbox control column to t-grid (#107144)

* Add checkbox control column to t-grid

* Add unit tests

* Update translations

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2021-08-03 09:31:14 +02:00 committed by GitHub
parent 7af1ec246d
commit 402702d55b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 299 additions and 66 deletions

View file

@ -120,7 +120,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
const leadingControlColumns = [
{
id: 'expand',
width: 20,
width: 40,
headerCellRender: () => {
return (
<EventsThContent>
@ -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 }), {});

View file

@ -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(
<TestProviders>
<Actions
ariaRowindex={2}
checked={false}
columnValues={'abc def'}
data={mockTimelineData[0].data}
ecsData={mockTimelineData[0].ecs}
eventIdToNoteIds={{}}
showNotes={false}
isEventPinned={false}
rowIndex={10}
toggleShowNotes={jest.fn()}
timelineId={'test'}
refetch={jest.fn()}
columnId={''}
index={2}
eventId="abc"
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
onRowSelected={jest.fn()}
showCheckboxes={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false);
});
});

View file

@ -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<ActionProps> = ({
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<ActionProps> = ({
}),
[eventId, onRowSelected]
);
const handlePinClicked = useCallback(
() =>
getPinOnClick({
@ -113,7 +116,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
}, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]);
return (
<ActionsContainer>
{showCheckboxes && (
{showCheckboxes && !tGridEnabled && (
<div key="select-event-container" data-test-subj="select-event-container">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
{loadingEventIds.includes(eventId) ? (

View file

@ -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(
<RowCheckBox {...defaultProps} loadingEventIds={[defaultProps.eventId]} />
);
expect(getByTestId('event-loader')).not.toBeNull();
});
test('calls onRowSelected when checked', () => {
const onRowSelected = jest.fn();
const { getByTestId } = render(
<RowCheckBox {...defaultProps} onRowSelected={onRowSelected} />
);
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(
<HeaderCheckBox {...defaultProps} onSelectAll={onSelectAll} />
);
fireEvent.click(getByTestId('select-all-events'));
expect(onSelectAll).toHaveBeenCalled();
});
});
});

View file

@ -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<HTMLInputElement>) =>
onRowSelected({
eventIds: [eventId],
isSelected: event.currentTarget.checked,
}),
[eventId, onRowSelected]
);
return loadingEventIds.includes(eventId) ? (
<EuiLoadingSpinner size="m" data-test-subj="event-loader" />
) : (
<EuiCheckbox
data-test-subj="select-event"
id={eventId}
checked={checked}
onChange={handleSelectEvent}
aria-label={i18n.CHECKBOX_FOR_ROW({ ariaRowindex, columnValues, checked })}
/>
);
};
export const HeaderCheckBox = ({ onSelectAll, isSelectAllChecked }: HeaderActionProps) => {
const handleSelectPageChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
onSelectAll({ isSelected: event.currentTarget.checked });
},
[onSelectAll]
);
return (
<EuiCheckbox
data-test-subj="select-all-events"
id="select-all-events"
checked={isSelectAllChecked}
onChange={handleSelectPageChange}
/>
);
};

View file

@ -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,
};

View file

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

View file

@ -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 && (
<HeaderActions
width={width ?? MIN_ACTION_COLUMN_WIDTH}
browserFields={browserFields}
columnHeaders={columnHeaders}
isEventViewer={isEventViewer}
isSelectAllChecked={isSelectAllChecked}
onSelectAll={onSelectPage}
showEventsSelect={false}
showSelectAllCheckbox={showCheckboxes}
sort={sort}
tabType={tabType}
timelineId={timelineId}
/>
)}
</>
);
},
// 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<StatefulBodyProps>(
[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<StatefulBodyProps>(
// 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<StatefulBodyProps>(
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,

View file

@ -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,

View file

@ -279,7 +279,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
sort,
itemsPerPage,
itemsPerPageOptions,
showCheckboxes: false,
showCheckboxes: true,
})
);
dispatch(

View file

@ -22527,7 +22527,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": "このイベントを分析できません。フィールドマッピングの互換性がありません",

View file

@ -23051,7 +23051,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": "无法分析此事件,因为其包含不兼容的字段映射",