[RAC] [TGrid] Implements cell actions in the TGrid (#107771)

## Summary

This PR implements cell actions in the `TGrid`, rendering them via `EuiDataGrid`, per the `Before` and `After` screenshots below:

### Before

Users previously hovered over a draggable field to view and trigger cell actions:

<img width="1348" alt="legacy_cell_actions" src="https://user-images.githubusercontent.com/4459398/128351498-49b4d224-6c51-4293-b14f-46bbb58f7cb3.png">

_Above: legacy `TGrid` cell action rendering_

### After

Cell actions are now rendered via `EuiDataGrid` cell actions:

<img width="997" alt="euidatagrid_cell_actions" src="https://user-images.githubusercontent.com/4459398/128358847-c5540ea4-8ba1-4b35-ab6b-3b3e39ae54ce.png">

_Above: new `TGrid` cell action rendering via `EuiDataGrid`_

## Technical Details

Every instance of the `TGrid` on a page can specify its own set of cell actions via `defaultCellActions` when calling the `timelines.getTGrid()` function to create an instance.

For example, the Observability Alerts `TGrid` is initialized in with a default set of actions in `x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx`, as shown in the code below:

```ts
      {timelines.getTGrid<'standalone'>({
        type: 'standalone',
        columns,
        deletedEventIds: [],
        defaultCellActions: getDefaultCellActions({ enableFilterActions: false }), // <-- defaultCellActions
        // ...
    </>
```

The type of the `defaultCellActions` is:

```ts
defaultCellActions?: TGridCellAction[];
```

and the definition of `TGridCellAction` is in `x-pack/plugins/timelines/common/types/timeline/columns/index.tsx`:

```ts
/**
 * A `TGridCellAction` function accepts `data`, where each row of data is
 * represented as a `TimelineNonEcsData[]`. For example, `data[0]` would
 * contain a `TimelineNonEcsData[]` with the first row of data.
 *
 * A `TGridCellAction` returns a function that has access to all the
 * `EuiDataGridColumnCellActionProps`, _plus_ access to `data`,
 *  which enables code like the following example to be written:
 *
 * Example:
 * ```
 * ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
 *   const value = getMappedNonEcsValue({
 *     data: data[rowIndex], // access a specific row's values
 *     fieldName: columnId,
 *   });
 *
 *   return (
 *     <Component onClick={() => alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart">
 *       {'Love it'}
 *      </Component>
 *   );
 * };
 * ```
 */
export type TGridCellAction = ({
  browserFields,
  data,
}: {
  browserFields: BrowserFields;
  /** each row of data is represented as one TimelineNonEcsData[] */
  data: TimelineNonEcsData[][];
}) => (props: EuiDataGridColumnCellActionProps) => ReactNode;
```

For example, the following `TGridCellAction[]` defines the `Copy to clipboard` action for the Observability Alerts table in `x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx`:

```ts
/** actions common to all cells (e.g. copy to clipboard) */
const commonCellActions: TGridCellAction[] = [
  ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
    const { timelines } = useKibanaServices();

    const value = getMappedNonEcsValue({
      data: data[rowIndex],
      fieldName: columnId,
    });

    return (
      <>
        {timelines.getHoverActions().getCopyButton({
          Component,
          field: columnId,
          isHoverAction: false,
          ownFocus: false,
          showTooltip: false,
          value,
        })}
      </>
    );
  },
];
```

Note that an _implementation_ of the copy to clipboard cell action, including the button, is available for both the Observability and Security solutions to use via `timelines.getHoverActions().getCopyButton()`, (and both solutions use it in this PR), but there's no requirement to use that specific implementation of the copy action.

### Security Solution cell actions

All previously-available hover actions in the Security Solution are now available as cell actions, i.e.:

- Filter for value
- Filter out value
- Add to timeline investigation
- Show Top `<field>` (only enabled for some data types)
- Copy to clipboard

### Observability cell actions

In this PR:

- Only the `Copy to clipboard` cell action is enabled by default in the Observability Alerts table
- The `Filter for value` and `Filter out value` cell actions may be enabled in the `Observability` solution by changing a single line of code, (setting `enableFilterActions` to true), on the following line in `x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx`:

```js
defaultCellActions: getDefaultCellActions({ enableFilterActions: false }), // <-- set this to `true` to enable the filter actions
```

`enableFilterActions` is set to `false` in this PR because the Observability Alerts page's search bar, defined in `x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx`:

```ts
  return (
    <SearchBar
      indexPatterns={dynamicIndexPattern}
      placeholder={i18n.translate('xpack.observability.alerts.searchBarPlaceholder', {
        defaultMessage: 'kibana.alert.evaluation.threshold > 75',
      })}
      query={{ query: query ?? '', language: queryLanguage }}
      // ...
    />
````

must be integrated with a `filterManager` to display the filters. A `filterManager` instance may be obtained in the Observability solution via the following boilerplate:

```ts
  const {
    services: {
      data: {
        query: { filterManager },
      },
    },
  } = useKibana<ObservabilityPublicPluginsStart>();
```

## Desk testing

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

cc @mdefazio
This commit is contained in:
Andrew Goldstein 2021-08-05 12:46:07 -06:00 committed by GitHub
parent 764388e713
commit 5f409bc339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 556 additions and 82 deletions

View file

@ -5,8 +5,11 @@
* 2.0.
*/
import { EuiDataGridColumn } from '@elastic/eui';
import { ReactNode } from 'react';
import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { IFieldSubType } from '../../../../../../../src/plugins/data/common';
import { BrowserFields } from '../../../search_strategy/index_fields';
import { TimelineNonEcsData } from '../../../search_strategy/timeline';
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
@ -14,6 +17,40 @@ export type ColumnHeaderType = 'not-filtered' | 'text-filter';
/** Uniquely identifies a column */
export type ColumnId = string;
/**
* A `TGridCellAction` function accepts `data`, where each row of data is
* represented as a `TimelineNonEcsData[]`. For example, `data[0]` would
* contain a `TimelineNonEcsData[]` with the first row of data.
*
* A `TGridCellAction` returns a function that has access to all the
* `EuiDataGridColumnCellActionProps`, _plus_ access to `data`,
* which enables code like the following example to be written:
*
* Example:
* ```
* ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
* const value = getMappedNonEcsValue({
* data: data[rowIndex], // access a specific row's values
* fieldName: columnId,
* });
*
* return (
* <Component onClick={() => alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart">
* {'Love it'}
* </Component>
* );
* };
* ```
*/
export type TGridCellAction = ({
browserFields,
data,
}: {
browserFields: BrowserFields;
/** each row of data is represented as one TimelineNonEcsData[] */
data: TimelineNonEcsData[][];
}) => (props: EuiDataGridColumnCellActionProps) => ReactNode;
/** The specification of a column header */
export type ColumnHeaderOptions = Pick<
EuiDataGridColumn,
@ -26,6 +63,7 @@ export type ColumnHeaderOptions = Pick<
| 'isSortable'
> & {
aggregatable?: boolean;
tGridCellActions?: TGridCellAction[];
category?: string;
columnHeaderType: ColumnHeaderType;
description?: string;

View file

@ -25,3 +25,9 @@ export const COPY_TO_THE_CLIPBOARD = i18n.translate(
defaultMessage: 'Copy to the clipboard',
}
);
export const SUCCESS_TOAST_TITLE = (field: string) =>
i18n.translate('xpack.timelines.clipboard.copy.successToastTitle', {
values: { field },
defaultMessage: 'Copied field {field} to the clipboard',
});

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import React, { useCallback, useEffect } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo } from 'react';
import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { DraggableId } from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
@ -44,12 +44,15 @@ const useGetHandleStartDragToTimeline = ({
};
export interface AddToTimelineButtonProps extends HoverActionComponentProps {
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
draggableId?: DraggableId;
dataProvider?: DataProvider[] | DataProvider;
}
const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
({
Component,
closePopOver,
dataProvider,
defaultFocusedButtonRef,
@ -96,6 +99,33 @@ const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
}
}, [handleStartDragToTimeline, keyboardEvent, ownFocus]);
const button = useMemo(
() =>
Component ? (
<Component
aria-label={i18n.ADD_TO_TIMELINE}
buttonRef={defaultFocusedButtonRef}
data-test-subj="add-to-timeline"
iconType="timeline"
onClick={handleStartDragToTimeline}
title={i18n.ADD_TO_TIMELINE}
>
{i18n.ADD_TO_TIMELINE}
</Component>
) : (
<EuiButtonIcon
aria-label={i18n.ADD_TO_TIMELINE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="add-to-timeline"
iconSize="s"
iconType="timeline"
onClick={handleStartDragToTimeline}
/>
),
[Component, defaultFocusedButtonRef, handleStartDragToTimeline]
);
return showTooltip ? (
<EuiToolTip
content={
@ -110,26 +140,10 @@ const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
/>
}
>
<EuiButtonIcon
aria-label={i18n.ADD_TO_TIMELINE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="add-to-timeline"
iconSize="s"
iconType="timeline"
onClick={handleStartDragToTimeline}
/>
{button}
</EuiToolTip>
) : (
<EuiButtonIcon
aria-label={i18n.ADD_TO_TIMELINE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="add-to-timeline"
iconSize="s"
iconType="timeline"
onClick={handleStartDragToTimeline}
/>
button
);
}
);

View file

@ -5,12 +5,18 @@
* 2.0.
*/
import React, { useEffect, useRef } from 'react';
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
import copy from 'copy-to-clipboard';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { stopPropagationAndPreventDefault } from '../../../../common';
import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard';
import { HoverActionComponentProps } from './types';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../clipboard';
import { useAppToasts } from '../../../hooks/use_app_toasts';
import { COPY_TO_CLIPBOARD } from '../../t_grid/body/translations';
import { SUCCESS_TOAST_TITLE } from '../../clipboard/translations';
export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', {
defaultMessage: 'Field',
@ -19,11 +25,14 @@ export const FIELD = i18n.translate('xpack.timelines.hoverActions.fieldLabel', {
export const COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT = 'c';
export interface CopyProps extends HoverActionComponentProps {
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
isHoverAction?: boolean;
}
const CopyButton: React.FC<CopyProps> = React.memo(
({ closePopOver, field, isHoverAction, keyboardEvent, ownFocus, value }) => {
({ Component, closePopOver, field, isHoverAction, keyboardEvent, ownFocus, value }) => {
const { addSuccess } = useAppToasts();
const panelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!ownFocus) {
@ -42,13 +51,34 @@ const CopyButton: React.FC<CopyProps> = React.memo(
}
}
}, [closePopOver, keyboardEvent, ownFocus]);
return (
const text = useMemo(() => `${field}${value != null ? `: "${value}"` : ''}`, [field, value]);
const onClick = useCallback(() => {
const isSuccess = copy(text, { debug: true });
if (isSuccess) {
addSuccess(SUCCESS_TOAST_TITLE(field), { toastLifeTimeMs: 800 });
}
}, [addSuccess, field, text]);
return Component ? (
<Component
aria-label={COPY_TO_CLIPBOARD}
data-test-subj="copy-to-clipboard"
iconType="copyClipboard"
onClick={onClick}
title={COPY_TO_CLIPBOARD}
>
{COPY_TO_CLIPBOARD}
</Component>
) : (
<div ref={panelRef}>
<WithCopyToClipboard
data-test-subj="copy-to-clipboard"
isHoverAction={isHoverAction}
keyboardShortcut={ownFocus ? COPY_TO_CLIPBOARD_KEYBOARD_SHORTCUT : ''}
text={`${field}${value != null ? `: "${value}"` : ''}`}
text={text}
titleSummary={FIELD}
/>
</div>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
@ -23,6 +23,7 @@ export type FilterForValueProps = HoverActionComponentProps & FilterValueFnArgs;
const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
({
Component,
closePopOver,
defaultFocusedButtonRef,
field,
@ -63,6 +64,33 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
}
}, [filterForValueFn, keyboardEvent, ownFocus]);
const button = useMemo(
() =>
Component ? (
<Component
aria-label={FILTER_FOR_VALUE}
buttonRef={defaultFocusedButtonRef}
data-test-subj="filter-for-value"
iconType="plusInCircle"
onClick={filterForValueFn}
title={FILTER_FOR_VALUE}
>
{FILTER_FOR_VALUE}
</Component>
) : (
<EuiButtonIcon
aria-label={FILTER_FOR_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-for-value"
iconSize="s"
iconType="plusInCircle"
onClick={filterForValueFn}
/>
),
[Component, defaultFocusedButtonRef, filterForValueFn]
);
return showTooltip ? (
<EuiToolTip
content={
@ -77,26 +105,10 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
/>
}
>
<EuiButtonIcon
aria-label={FILTER_FOR_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-for-value"
iconSize="s"
iconType="plusInCircle"
onClick={filterForValueFn}
/>
{button}
</EuiToolTip>
) : (
<EuiButtonIcon
aria-label={FILTER_FOR_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-for-value"
iconSize="s"
iconType="plusInCircle"
onClick={filterForValueFn}
/>
button
);
}
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
@ -22,6 +22,7 @@ export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o';
const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnArgs> = React.memo(
({
Component,
closePopOver,
defaultFocusedButtonRef,
field,
@ -64,6 +65,33 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
}
}, [filterOutValueFn, keyboardEvent, ownFocus]);
const button = useMemo(
() =>
Component ? (
<Component
aria-label={FILTER_OUT_VALUE}
buttonRef={defaultFocusedButtonRef}
data-test-subj="filter-out-value"
iconType="minusInCircle"
onClick={filterOutValueFn}
title={FILTER_OUT_VALUE}
>
{FILTER_OUT_VALUE}
</Component>
) : (
<EuiButtonIcon
aria-label={FILTER_OUT_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-out-value"
iconSize="s"
iconType="minusInCircle"
onClick={filterOutValueFn}
/>
),
[Component, defaultFocusedButtonRef, filterOutValueFn]
);
return showTooltip ? (
<EuiToolTip
content={
@ -78,26 +106,10 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
/>
}
>
<EuiButtonIcon
aria-label={FILTER_OUT_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-out-value"
iconSize="s"
iconType="minusInCircle"
onClick={filterOutValueFn}
/>
{button}
</EuiToolTip>
) : (
<EuiButtonIcon
aria-label={FILTER_OUT_VALUE}
buttonRef={defaultFocusedButtonRef}
className="timelines__hoverActionButton"
data-test-subj="filter-out-value"
iconSize="s"
iconType="minusInCircle"
onClick={filterOutValueFn}
/>
button
);
}
);

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { EuiButtonIconPropsForButton } from '@elastic/eui';
import { EuiButtonEmpty, EuiButtonIcon, EuiButtonIconPropsForButton } from '@elastic/eui';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
export interface FilterValueFnArgs {
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
field: string;
value: string[] | string | null | undefined;
filterManager: FilterManager | undefined;

View file

@ -7,6 +7,7 @@
import {
EuiDataGrid,
EuiDataGridColumn,
EuiDataGridCellValueElementProps,
EuiDataGridControlColumn,
EuiDataGridStyle,
@ -27,6 +28,7 @@ import React, {
import { connect, ConnectedProps, useDispatch } from 'react-redux';
import {
TGridCellAction,
TimelineId,
TimelineTabs,
BulkActionsProp,
@ -66,6 +68,7 @@ interface OwnProps {
additionalControls?: React.ReactNode;
browserFields: BrowserFields;
data: TimelineItem[];
defaultCellActions?: TGridCellAction[];
id: string;
isEventViewer?: boolean;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
@ -211,6 +214,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
browserFields,
columnHeaders,
data,
defaultCellActions,
excludedRowRendererIds,
id,
isEventViewer = false,
@ -461,6 +465,24 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
sort,
]);
const columnsWithCellActions: EuiDataGridColumn[] = useMemo(
() =>
columnHeaders.map((header) => {
const buildAction = (tGridCellAction: TGridCellAction) =>
tGridCellAction({
data: data.map((row) => row.data),
browserFields,
});
return {
...header,
cellActions:
header.tGridCellActions?.map(buildAction) ?? defaultCellActions?.map(buildAction),
};
}),
[browserFields, columnHeaders, data, defaultCellActions]
);
const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({
columnId,
rowIndex,
@ -494,7 +516,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
<EuiDataGrid
data-test-subj="body-data-grid"
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
columns={columnHeaders}
columns={columnsWithCellActions}
columnVisibility={{ visibleColumns, setVisibleColumns }}
gridStyle={gridStyle}
leadingControlColumns={leadingTGridControlColumns}

View file

@ -15,7 +15,7 @@ import { Direction } from '../../../../common/search_strategy';
import type { DocValueFields } from '../../../../common/search_strategy';
import type { CoreStart } from '../../../../../../../src/core/public';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { TGridCellAction, TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import type {
CellValueElementProps,
ColumnHeaderOptions,
@ -104,6 +104,7 @@ export interface TGridIntegratedProps {
browserFields: BrowserFields;
columns: ColumnHeaderOptions[];
dataProviders: DataProvider[];
defaultCellActions?: TGridCellAction[];
deletedEventIds: Readonly<string[]>;
docValueFields: DocValueFields[];
end: string;
@ -138,6 +139,7 @@ export interface TGridIntegratedProps {
const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
browserFields,
columns,
defaultCellActions,
dataProviders,
deletedEventIds,
docValueFields,
@ -309,6 +311,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
activePage={pageInfo.activePage}
browserFields={browserFields}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={id}
isEventViewer={true}
loadPage={loadPage}

View file

@ -13,7 +13,7 @@ import { useDispatch } from 'react-redux';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Direction } from '../../../../common/search_strategy';
import type { CoreStart } from '../../../../../../../src/core/public';
import { TimelineTabs } from '../../../../common/types/timeline';
import { TGridCellAction, TimelineTabs } from '../../../../common/types/timeline';
import type {
CellValueElementProps,
ColumnHeaderOptions,
@ -98,6 +98,7 @@ const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>`
export interface TGridStandaloneProps {
columns: ColumnHeaderOptions[];
defaultCellActions?: TGridCellAction[];
deletedEventIds: Readonly<string[]>;
end: string;
loadingText: React.ReactNode;
@ -127,6 +128,7 @@ const basicUnit = (n: number) => i18n.UNIT(n);
const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
columns,
defaultCellActions,
deletedEventIds,
end,
loadingText,
@ -322,6 +324,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
activePage={pageInfo.activePage}
browserFields={browserFields}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={STANDALONE_ID}
isEventViewer={true}
loadPage={loadPage}