mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
764388e713
commit
5f409bc339
21 changed files with 556 additions and 82 deletions
|
@ -31,6 +31,7 @@ import type {
|
|||
import { getRenderCellValue } from './render_cell_value';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { decorateResponse } from './decorate_response';
|
||||
import { getDefaultCellActions } from './default_cell_actions';
|
||||
import { LazyAlertsFlyout } from '../..';
|
||||
|
||||
interface AlertsTableTGridProps {
|
||||
|
@ -192,6 +193,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
type: 'standalone',
|
||||
columns,
|
||||
deletedEventIds: [],
|
||||
defaultCellActions: getDefaultCellActions({ enableFilterActions: false }),
|
||||
end: rangeTo,
|
||||
filters: [],
|
||||
indexNames: [indexName],
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { ObservabilityPublicPluginsStart } from '../..';
|
||||
import { getMappedNonEcsValue } from './render_cell_value';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { TimelineNonEcsData } from '../../../../timelines/common/search_strategy';
|
||||
import { TGridCellAction } from '../../../../timelines/common/types/timeline';
|
||||
import { TimelinesUIStart } from '../../../../timelines/public';
|
||||
|
||||
/** a noop required by the filter in / out buttons */
|
||||
const onFilterAdded = () => {};
|
||||
|
||||
/** a hook to eliminate the verbose boilerplate required to use common services */
|
||||
const useKibanaServices = () => {
|
||||
const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services;
|
||||
const {
|
||||
services: {
|
||||
data: {
|
||||
query: { filterManager },
|
||||
},
|
||||
},
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
return { timelines, filterManager };
|
||||
};
|
||||
|
||||
/** actions for adding filters to the search bar */
|
||||
const filterCellActions: TGridCellAction[] = [
|
||||
({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getFilterForValueButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getFilterOutValueButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
/** 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,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
/** returns the default actions shown in `EuiDataGrid` cells */
|
||||
export const getDefaultCellActions = ({ enableFilterActions }: { enableFilterActions: boolean }) =>
|
||||
enableFilterActions ? [...filterCellActions, ...commonCellActions] : [...commonCellActions];
|
|
@ -23,7 +23,7 @@ import { TopAlert } from '.';
|
|||
import { decorateResponse } from './decorate_response';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
|
||||
const getMappedNonEcsValue = ({
|
||||
export const getMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { alertsDefaultModel } from './default_headers';
|
|||
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
|
||||
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
import * as i18n from './translations';
|
||||
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
|
@ -104,6 +105,7 @@ const AlertsTableComponent: React.FC<Props> = ({
|
|||
<StatefulEventsViewer
|
||||
pageFilters={alertsFilter}
|
||||
defaultModel={alertsDefaultModel}
|
||||
defaultCellActions={defaultCellActions}
|
||||
end={endDate}
|
||||
id={timelineId}
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { defaultRowRenderers } from '../../../timelines/components/timeline/body
|
|||
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
import { useTimelineEvents } from '../../../timelines/containers';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
|
@ -124,6 +125,7 @@ describe('EventsViewer', () => {
|
|||
const mount = useMountAppended();
|
||||
|
||||
let testProps = {
|
||||
defaultCellActions,
|
||||
defaultModel: eventsDefaultModel,
|
||||
end: to,
|
||||
id: TimelineId.test,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { SourcererScopeName } from '../../store/sourcerer/model';
|
|||
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
import { useTimelineEvents } from '../../../timelines/containers';
|
||||
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
|
||||
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
|
@ -38,6 +39,7 @@ const from = '2019-08-27T22:10:56.794Z';
|
|||
const to = '2019-08-26T22:10:56.791Z';
|
||||
|
||||
const testProps = {
|
||||
defaultCellActions,
|
||||
defaultModel: eventsDefaultModel,
|
||||
end: to,
|
||||
indexNames: [],
|
||||
|
|
|
@ -23,6 +23,7 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
|||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { useSourcererScope } from '../../containers/sourcerer';
|
||||
import { TGridCellAction } from '../../../../../timelines/common/types';
|
||||
import { DetailsPanel } from '../../../timelines/components/side_panel';
|
||||
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
@ -47,6 +48,7 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
|
|||
`;
|
||||
|
||||
export interface OwnProps {
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
defaultModel: SubsetTimelineModel;
|
||||
end: string;
|
||||
id: TimelineId;
|
||||
|
@ -73,6 +75,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
createTimeline,
|
||||
columns,
|
||||
dataProviders,
|
||||
defaultCellActions,
|
||||
deletedEventIds,
|
||||
deleteEventQuery,
|
||||
end,
|
||||
|
@ -140,6 +143,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
browserFields,
|
||||
columns,
|
||||
dataProviders: dataProviders!,
|
||||
defaultCellActions,
|
||||
deletedEventIds,
|
||||
docValueFields,
|
||||
end,
|
||||
|
@ -269,6 +273,7 @@ export const StatefulEventsViewer = connector(
|
|||
prevProps.scopeId === nextProps.scopeId &&
|
||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
||||
prevProps.defaultCellActions === nextProps.defaultCellActions &&
|
||||
deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
|
||||
prevProps.deletedEventIds === nextProps.deletedEventIds &&
|
||||
prevProps.end === nextProps.end &&
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiButtonIcon, EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { StatefulTopN } from '../../top_n';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
|
@ -23,17 +23,30 @@ const SHOW_TOP = (fieldName: string) =>
|
|||
});
|
||||
|
||||
interface Props {
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
field: string;
|
||||
onClick: () => void;
|
||||
onFilterAdded?: () => void;
|
||||
ownFocus: boolean;
|
||||
showTopN: boolean;
|
||||
showTooltip?: boolean;
|
||||
timelineId?: string | null;
|
||||
value?: string[] | string | null;
|
||||
}
|
||||
|
||||
export const ShowTopNButton: React.FC<Props> = React.memo(
|
||||
({ field, onClick, onFilterAdded, ownFocus, showTopN, timelineId, value }) => {
|
||||
({
|
||||
Component,
|
||||
field,
|
||||
onClick,
|
||||
onFilterAdded,
|
||||
ownFocus,
|
||||
showTooltip = true,
|
||||
showTopN,
|
||||
timelineId,
|
||||
value,
|
||||
}) => {
|
||||
const activeScope: SourcererScopeName =
|
||||
timelineId === TimelineId.active
|
||||
? SourcererScopeName.timeline
|
||||
|
@ -44,19 +57,32 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
? SourcererScopeName.detections
|
||||
: SourcererScopeName.default;
|
||||
const { browserFields, indexPattern } = useSourcererScope(activeScope);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonIcon
|
||||
aria-label={SHOW_TOP(field)}
|
||||
className="securitySolution__hoverActionButton"
|
||||
data-test-subj="show-top-field"
|
||||
iconSize="s"
|
||||
iconType="visBarVertical"
|
||||
onClick={onClick}
|
||||
/>
|
||||
),
|
||||
[field, onClick]
|
||||
() =>
|
||||
Component ? (
|
||||
<Component
|
||||
aria-label={SHOW_TOP(field)}
|
||||
data-test-subj="show-top-field"
|
||||
iconType="visBarVertical"
|
||||
onClick={onClick}
|
||||
title={SHOW_TOP(field)}
|
||||
>
|
||||
{SHOW_TOP(field)}
|
||||
</Component>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={SHOW_TOP(field)}
|
||||
className="securitySolution__hoverActionButton"
|
||||
data-test-subj="show-top-field"
|
||||
iconSize="s"
|
||||
iconType="visBarVertical"
|
||||
onClick={onClick}
|
||||
/>
|
||||
),
|
||||
[Component, field, onClick]
|
||||
);
|
||||
|
||||
return showTopN ? (
|
||||
<EuiPopover button={button} isOpen={showTopN} closePopover={onClick}>
|
||||
<StatefulTopN
|
||||
|
@ -69,7 +95,7 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
value={value}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : (
|
||||
) : showTooltip ? (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<TooltipWithKeyboardShortcut
|
||||
|
@ -85,6 +111,8 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useState, useMemo } from 'react';
|
||||
|
||||
import type {
|
||||
BrowserFields,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../timelines/common/search_strategy';
|
||||
import { DataProvider, TGridCellAction } from '../../../../../timelines/common/types';
|
||||
import { TimelineId } from '../../../../common';
|
||||
import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { allowTopN, escapeDataProviderId } from '../../components/drag_and_drop/helpers';
|
||||
import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n';
|
||||
import { getAllFieldsByName } from '../../containers/source';
|
||||
import { useKibana } from '../kibana';
|
||||
|
||||
/** a noop required by the filter in / out buttons */
|
||||
const onFilterAdded = () => {};
|
||||
|
||||
/** a hook to eliminate the verbose boilerplate required to use common services */
|
||||
const useKibanaServices = () => {
|
||||
const {
|
||||
timelines,
|
||||
data: {
|
||||
query: { filterManager },
|
||||
},
|
||||
} = useKibana().services;
|
||||
|
||||
return { timelines, filterManager };
|
||||
};
|
||||
|
||||
/** the default actions shown in `EuiDataGrid` cells */
|
||||
export const defaultCellActions: TGridCellAction[] = [
|
||||
({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getFilterForValueButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getFilterOutValueButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
const { timelines } = useKibanaServices();
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
const dataProvider: DataProvider[] = useMemo(
|
||||
() =>
|
||||
value?.map((x) => ({
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: `${escapeDataProviderId(columnId)}-row-${rowIndex}-col-${columnId}-val-${x}`,
|
||||
name: x,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: columnId,
|
||||
value: x,
|
||||
operator: IS_OPERATOR,
|
||||
},
|
||||
})) ?? [],
|
||||
[columnId, rowIndex, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getAddToTimelineButton({
|
||||
Component,
|
||||
dataProvider,
|
||||
field: columnId,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ browserFields, data }: { browserFields: BrowserFields; data: TimelineNonEcsData[][] }) => ({
|
||||
rowIndex,
|
||||
columnId,
|
||||
Component,
|
||||
}) => {
|
||||
const [showTopN, setShowTopN] = useState(false);
|
||||
const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]);
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[rowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowTopN({
|
||||
browserField: getAllFieldsByName(browserFields)[columnId],
|
||||
fieldName: columnId,
|
||||
}) && (
|
||||
<ShowTopNButton
|
||||
Component={Component}
|
||||
data-test-subj="hover-actions-show-top-n"
|
||||
field={columnId}
|
||||
onClick={onClick}
|
||||
onFilterAdded={onFilterAdded}
|
||||
ownFocus={false}
|
||||
showTopN={showTopN}
|
||||
showTooltip={false}
|
||||
timelineId={TimelineId.active}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ 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,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
];
|
|
@ -53,6 +53,7 @@ import { defaultRowRenderers } from '../../../timelines/components/timeline/body
|
|||
import { columns, RenderCellValue } from '../../configurations/security_solution_detections';
|
||||
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions';
|
||||
|
||||
interface OwnProps {
|
||||
defaultFilters?: Filter[];
|
||||
|
@ -388,6 +389,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
return (
|
||||
<StatefulEventsViewer
|
||||
pageFilters={defaultFiltersMemo}
|
||||
defaultCellActions={defaultCellActions}
|
||||
defaultModel={defaultTimelineModel}
|
||||
end={to}
|
||||
currentFilter={filterGroup}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
|
|||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
|
||||
import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions';
|
||||
|
||||
const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery';
|
||||
|
||||
|
@ -108,6 +109,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
|
|||
/>
|
||||
)}
|
||||
<StatefulEventsViewer
|
||||
defaultCellActions={defaultCellActions}
|
||||
defaultModel={eventsDefaultModel}
|
||||
end={endDate}
|
||||
id={TimelineId.hostsPageEvents}
|
||||
|
|
|
@ -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;
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue