[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:
parent
7af1ec246d
commit
402702d55b
|
@ -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 }), {});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) ? (
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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}',
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -279,7 +279,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
sort,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
showCheckboxes: false,
|
||||
showCheckboxes: true,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
|
|
|
@ -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": "このイベントを分析できません。フィールドマッピングの互換性がありません",
|
||||
|
|
|
@ -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": "无法分析此事件,因为其包含不兼容的字段映射",
|
||||
|
|
Loading…
Reference in a new issue