Skip to content

Commit

Permalink
[RAC] [TGrid] Bulk actions to EuiDataGrid toolbar (#107141)
Browse files Browse the repository at this point in the history
* tGrid EuiDataGrid toolbar replace utilityBar

* tgrid new prop in observability

* types and translations fixes

* bulkActions props and encapsulation

* update limits

* code cleaning

* load lazy and remove export from public

* add memoization to bulk_actions

* icon change and test fixed

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
semd and kibanamachine committed Aug 3, 2021
1 parent 1e1d669 commit b5e8db2
Show file tree
Hide file tree
Showing 25 changed files with 799 additions and 159 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
timelines: 330000
timelines: 327300
screenshotMode: 17856
visTypePie: 35583
expressionRevealImage: 25675
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
import type { TimelinesUIStart } from '../../../../timelines/public';
import type { TopAlert } from './';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import type { ActionProps, ColumnHeaderOptions, RowRenderer } from '../../../../timelines/common';
import type {
ActionProps,
AlertStatus,
ColumnHeaderOptions,
RowRenderer,
} from '../../../../timelines/common';

import { getRenderCellValue } from './render_cell_value';
import { usePluginContext } from '../../hooks/use_plugin_context';
Expand Down Expand Up @@ -213,6 +218,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
sortDirection: 'desc',
},
],
filterStatus: status as AlertStatus,
leadingControlColumns,
trailingControlColumns,
unit: (totalAlerts: number) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import type { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
Expand Down Expand Up @@ -54,6 +55,7 @@ export interface OwnProps {
showTotalCount?: boolean;
headerFilterGroup?: React.ReactNode;
pageFilters?: Filter[];
currentFilter?: Status;
onRuleChange?: () => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
Expand Down Expand Up @@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
itemsPerPageOptions,
kqlMode,
pageFilters,
currentFilter,
onRuleChange,
query,
renderCellValue,
Expand Down Expand Up @@ -160,6 +163,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
sort,
utilityBar,
graphEventId,
filterStatus: currentFilter,
leadingControlColumns,
trailingControlColumns,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
pageFilters={defaultFiltersMemo}
defaultModel={defaultTimelineModel}
end={to}
currentFilter={filterGroup}
headerFilterGroup={headerFilterGroup}
id={timelineId}
onRuleChange={onRuleChange}
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/timelines/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@
* 2.0.
*/

import { AlertStatus } from './types/timeline/actions';

export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';

export const FILTER_OPEN: AlertStatus = 'open';
export const FILTER_CLOSED: AlertStatus = 'closed';
export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress';
11 changes: 11 additions & 0 deletions x-pack/plugins/timelines/common/types/timeline/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,14 @@ export type ControlColumnProps = Omit<
keyof AdditionalControlColumnProps
> &
Partial<AdditionalControlColumnProps>;

export type OnAlertStatusActionSuccess = (status: AlertStatus) => void;
export type OnAlertStatusActionFailure = (status: AlertStatus, error: string) => void;
export interface BulkActionsObjectProp {
alertStatusActions?: boolean;
onAlertStatusActionSuccess?: OnAlertStatusActionSuccess;
onAlertStatusActionFailure?: OnAlertStatusActionFailure;
}
export type BulkActionsProp = boolean | BulkActionsObjectProp;

export type AlertStatus = 'open' | 'closed' | 'in-progress';
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ describe('Body', () => {
showCheckboxes: false,
tabType: TimelineTabs.query,
totalPages: 1,
totalItems: 1,
leadingControlColumns: [],
trailingControlColumns: [],
filterStatus: 'open',
refetch: jest.fn(),
};

describe('rendering', () => {
Expand Down
150 changes: 125 additions & 25 deletions x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,34 @@ import {
EuiDataGridControlColumn,
EuiDataGridStyle,
EuiDataGridToolBarVisibilityOptions,
EuiLoadingSpinner,
} from '@elastic/eui';
import { getOr } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
import React, {
ComponentType,
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { connect, ConnectedProps } from 'react-redux';

import { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import {
TimelineId,
TimelineTabs,
BulkActionsProp,
SortColumnTimeline,
} from '../../../../common/types/timeline';

import type {
CellValueElementProps,
ColumnHeaderOptions,
ControlColumnProps,
RowRenderer,
AlertStatus,
} from '../../../../common/types/timeline';
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';

Expand All @@ -32,32 +47,44 @@ import { getEventIdToDataMapping } from './helpers';
import { Sort } from './sort';

import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
import { OnRowSelected, OnSelectAll } from '../types';
import { StatefulFieldsBrowser, tGridActions } from '../../../';
import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import type { OnRowSelected, OnSelectAll } from '../types';
import type { Refetch } from '../../../store/t_grid/inputs';
import { StatefulFieldsBrowser } from '../../../';
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { RowAction } from './row_action';
import * as i18n from './translations';
import { AlertCount } from '../styles';
import { checkBoxControlColumn } from './control_columns';

const StatefulAlertStatusBulkActions = lazy(
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
);

interface OwnProps {
activePage: number;
additionalControls?: React.ReactNode;
browserFields: BrowserFields;
data: TimelineItem[];
id: string;
isEventViewer?: boolean;
leadingControlColumns: ControlColumnProps[];
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
sort: Sort[];
tabType: TimelineTabs;
trailingControlColumns: ControlColumnProps[];
leadingControlColumns?: ControlColumnProps[];
trailingControlColumns?: ControlColumnProps[];
totalPages: number;
totalItems: number;
bulkActions?: BulkActionsProp;
filterStatus?: AlertStatus;
unit?: (total: number) => React.ReactNode;
onRuleChange?: () => void;
refetch: Refetch;
}

const basicUnit = (n: number) => i18n.UNIT(n);
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;

export const hasAdditionalActions = (id: TimelineId): boolean =>
Expand Down Expand Up @@ -200,31 +227,42 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
sort,
tabType,
totalPages,
totalItems,
filterStatus,
bulkActions = true,
unit = basicUnit,
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
refetch,
}) => {
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
getManageTimeline(state, id)
);

const alertCountText = useMemo(() => `${totalItems.toLocaleString()} ${unit(totalItems)}`, [
totalItems,
unit,
]);

const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);

const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
setSelected!({
setSelected({
id,
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
isSelected,
isSelectAllChecked:
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
isSelectAllChecked: isSelected && selectedCount + 1 === data.length,
});
},
[setSelected, id, data, selectedEventIds, queryFields]
[setSelected, id, data, selectedCount, queryFields]
);

const onSelectPage: OnSelectAll = useCallback(
({ isSelected }: { isSelected: boolean }) =>
isSelected
? setSelected!({
? setSelected({
id,
eventIds: getEventIdToDataMapping(
data,
Expand All @@ -234,7 +272,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
isSelected,
isSelectAllChecked: isSelected,
})
: clearSelected!({ id }),
: clearSelected({ id }),
[setSelected, clearSelected, id, data, queryFields]
);

Expand All @@ -245,25 +283,87 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
}
}, [isSelectAllChecked, onSelectPage, selectAll]);

const onAlertStatusActionSuccess = useMemo(() => {
if (bulkActions && bulkActions !== true) {
return bulkActions.onAlertStatusActionSuccess;
}
}, [bulkActions]);

const onAlertStatusActionFailure = useMemo(() => {
if (bulkActions && bulkActions !== true) {
return bulkActions.onAlertStatusActionFailure;
}
}, [bulkActions]);

const showBulkActions = useMemo(() => {
if (selectedCount === 0 || !showCheckboxes) {
return false;
}
if (typeof bulkActions === 'boolean') {
return bulkActions;
}
return bulkActions.alertStatusActions ?? true;
}, [selectedCount, showCheckboxes, bulkActions]);

const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo(
() => ({
additionalControls: (
<>
{additionalControls ?? null}
{
<StatefulFieldsBrowser
data-test-subj="field-browser"
browserFields={browserFields}
timelineId={id}
columnHeaders={columnHeaders}
/>
}
<AlertCount>{alertCountText}</AlertCount>
{showBulkActions ? (
<>
<Suspense fallback={<EuiLoadingSpinner />}>
<StatefulAlertStatusBulkActions
data-test-subj="bulk-actions"
id={id}
totalItems={totalItems}
filterStatus={filterStatus}
onActionSuccess={onAlertStatusActionSuccess}
onActionFailure={onAlertStatusActionFailure}
refetch={refetch}
/>
</Suspense>
{additionalControls ?? null}
</>
) : (
<>
{additionalControls ?? null}
<StatefulFieldsBrowser
data-test-subj="field-browser"
browserFields={browserFields}
timelineId={id}
columnHeaders={columnHeaders}
/>
</>
)}
</>
),
showColumnSelector: { allowHide: false, allowReorder: true },
...(showBulkActions
? {
showColumnSelector: false,
showSortSelector: false,
showFullScreenSelector: false,
}
: {
showColumnSelector: { allowHide: true, allowReorder: true },
showSortSelector: true,
showFullScreenSelector: true,
}),
showStyleSelector: false,
}),
[additionalControls, browserFields, columnHeaders, id]
[
id,
alertCountText,
totalItems,
filterStatus,
browserFields,
columnHeaders,
additionalControls,
showBulkActions,
onAlertStatusActionSuccess,
onAlertStatusActionFailure,
refetch,
]
);

const [sortingColumns, setSortingColumns] = useState([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,9 @@ export const INVESTIGATE_IN_RESOLVER_DISABLED = i18n.translate(
defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings',
}
);

export const UNIT = (totalCount: number) =>
i18n.translate('xpack.timelines.timeline.body.unit', {
values: { totalCount },
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
});
Loading

0 comments on commit b5e8db2

Please sign in to comment.