[Security Solution][Lens] Check filter action type to hide default filter actions in visualizations (#171284)

## Summary

Bug: https://github.com/elastic/kibana/issues/171167

The [previous
implementation](https://github.com/elastic/kibana/pull/170127) solved a
different bug using a new `shouldShowLegendAction` property. This
approach had a limitation on the Security Dashboards page since the
Security app has no control over the properties passed to the
visualization components when they are rendered through portable
Dashboards.

This PR fixes the problem by checking if any of the registered actions
is a "filter" action `type` in the visualizations. If customized filter
actions are found, the default filter actions hardcoded in the
visualizations code are not added, preventing duplication of filter
actions.

The specific action `type` used for the check is the
`FILTER_CELL_ACTION_TYPE = 'cellAction-filter'` constant exported by the
`@kbn/cell-actions` package.

This new approach uses a property stored in the registered actions
themselves, so we don't need to pass any extra property to the
visualization components, it works transparently. So the
`shouldShowLegendAction` property has also been cleaned.

## Demos

Timeline using `showTopN` visualization:


491c08e8-0740-4c9e-9cb7-81267b9ba040


Alerts page using `Counts` table visualization and `showTopN`
visualization


683eec6c-382e-4ae9-9400-c1022922e488


Portable Dashboard visualizations:


50f09a65-488d-41f2-a5b8-3882d9c80678


Security actions are "compatible" only inside the Security app, in the
Lens app the default filter actions are displayed:

<img width="1682" alt="lens-actions-screenshot"
src="1e7ce929-129d-45a9-ba71-8d28e3726454">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2023-11-20 15:33:31 +01:00 committed by GitHub
parent 48711622bd
commit 9bf7e38b1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 129 additions and 262 deletions

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './src/constants';

View file

@ -20,7 +20,7 @@ export type {
export type { UseDataGridColumnsCellActions, UseDataGridColumnsCellActionsProps } from './hooks';
// Constants
export { CellActionsMode, FILTER_CELL_ACTION_TYPE, COPY_CELL_ACTION_TYPE } from './constants';
export { CellActionsMode } from './constants';
// Components and hooks
export { CellActionsProvider } from './context';

View file

@ -47,6 +47,7 @@ export interface MultiFilterEvent {
export interface CellValueAction {
id: string;
type?: string;
iconType: string;
displayName: string;
execute: (data: CellValueContext['data']) => void;

View file

@ -14,10 +14,15 @@ import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { Datatable } from '@kbn/expressions-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { FILTER_CELL_ACTION_TYPE } from '@kbn/cell-actions/constants';
import { PartitionVisParams } from '../../common/types';
import { ColumnCellValueActions, FilterEvent } from '../types';
import { CellValueAction, ColumnCellValueActions, FilterEvent } from '../types';
import { getSeriesValueColumnIndex, getFilterPopoverTitle } from './filter_helpers';
const hasFilterCellAction = (actions: CellValueAction[]) => {
return actions.some(({ type }) => type === FILTER_CELL_ACTION_TYPE);
};
export const getLegendActions = (
canFilter: (
data: FilterEvent | null,
@ -58,9 +63,10 @@ export const getLegendActions = (
pieSeries.key
);
const panelItems: EuiContextMenuPanelDescriptor['items'] = [];
const compatibleCellActions = columnCellValueActions[columnIndex] ?? [];
if (isFilterable && filterData) {
const panelItems: EuiContextMenuPanelDescriptor['items'] = [];
if (!hasFilterCellAction(compatibleCellActions) && isFilterable && filterData) {
panelItems.push(
{
name: i18n.translate('expressionPartitionVis.legend.filterForValueButtonAriaLabel', {
@ -87,20 +93,18 @@ export const getLegendActions = (
);
}
if (columnCellValueActions[columnIndex]) {
const columnMeta = visData.columns[columnIndex].meta;
columnCellValueActions[columnIndex].forEach((action) => {
panelItems.push({
name: action.displayName,
'data-test-subj': `legend-${title}-${action.id}`,
icon: <EuiIcon type={action.iconType} size="m" />,
onClick: () => {
action.execute([{ columnMeta, value: pieSeries.key }]);
setPopoverOpen(false);
},
});
const columnMeta = visData.columns[columnIndex].meta;
compatibleCellActions.forEach((action) => {
panelItems.push({
name: action.displayName,
'data-test-subj': `legend-${title}-${action.id}`,
icon: <EuiIcon type={action.iconType} size="m" />,
onClick: () => {
action.execute([{ columnMeta, value: pieSeries.key }]);
setPopoverOpen(false);
},
});
}
});
if (panelItems.length === 0) {
return null;

View file

@ -29,6 +29,7 @@
"@kbn/analytics",
"@kbn/chart-icons",
"@kbn/chart-expressions-common",
"@kbn/cell-actions",
],
"exclude": [
"target/**/*",

View file

@ -10,7 +10,6 @@ import { layeredXyVisFunction } from '.';
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
import { sampleArgs, sampleExtendedLayer } from '../__mocks__';
import { XY_VIS } from '../constants';
import { shouldShowLegendActionDefault } from '../helpers/visualization';
describe('layeredXyVis', () => {
test('it renders with the specified data and args', async () => {
@ -31,7 +30,6 @@ describe('layeredXyVis', () => {
syncTooltips: false,
syncCursor: true,
canNavigateToLens: false,
shouldShowLegendAction: shouldShowLegendActionDefault,
},
});
});

View file

@ -18,7 +18,6 @@ import {
validateAxes,
} from './validate';
import { appendLayerIds, getDataLayers } from '../helpers';
import { shouldShowLegendActionDefault } from '../helpers/visualization';
export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => {
const layers = appendLayerIds(args.layers ?? [], 'layers');
@ -67,7 +66,6 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers)
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
shouldShowLegendAction: handlers?.shouldShowLegendAction ?? shouldShowLegendActionDefault,
},
};
};

View file

@ -10,7 +10,6 @@ import { xyVisFunction } from '.';
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
import { sampleArgs, sampleLayer } from '../__mocks__';
import { XY_VIS } from '../constants';
import { shouldShowLegendActionDefault } from '../helpers/visualization';
describe('xyVis', () => {
test('it renders with the specified data and args', async () => {
@ -43,7 +42,6 @@ describe('xyVis', () => {
syncColors: false,
syncTooltips: false,
syncCursor: true,
shouldShowLegendAction: shouldShowLegendActionDefault,
},
});
});
@ -354,7 +352,6 @@ describe('xyVis', () => {
syncColors: false,
syncTooltips: false,
syncCursor: true,
shouldShowLegendAction: shouldShowLegendActionDefault,
},
});
});
@ -404,7 +401,6 @@ describe('xyVis', () => {
syncTooltips: false,
syncCursor: true,
overrides,
shouldShowLegendAction: shouldShowLegendActionDefault,
},
});
});

View file

@ -30,7 +30,6 @@ import {
validateAxes,
} from './validate';
import { logDatatable } from '../utils';
import { shouldShowLegendActionDefault } from '../helpers/visualization';
const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => {
const accessors = getAccessors<string | ExpressionValueVisDimension, XYArgs>(args, table);
@ -140,7 +139,6 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
shouldShowLegendAction: handlers?.shouldShowLegendAction ?? shouldShowLegendActionDefault,
},
};
};

View file

@ -19,5 +19,3 @@ export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) {
(!l.xScaleType || l.xScaleType === XScaleTypes.TIME)
);
}
export const shouldShowLegendActionDefault = () => true;

View file

@ -203,8 +203,7 @@ describe('getLegendAction', function () {
formattedColumns: {},
},
},
{},
() => true
{}
);
let wrapper: ReactWrapper<LegendActionProps>;

View file

@ -28,7 +28,6 @@ export const getLegendAction = (
fieldFormats: LayersFieldFormats,
formattedDatatables: DatatablesWithFormatInfo,
titles: LayersAccessorsTitles,
shouldShowLegendAction?: (actionId: string) => boolean,
singleTable?: boolean
): LegendAction =>
React.memo(({ series: [xySeries] }) => {
@ -110,7 +109,6 @@ export const getLegendAction = (
}
onFilter={filterHandler}
legendCellValueActions={legendCellValueActions}
shouldShowLegendAction={shouldShowLegendAction}
/>
);
});

View file

@ -8,16 +8,14 @@
import React, { useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiContextMenuPanelDescriptor,
EuiIcon,
EuiPopover,
EuiContextMenu,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
import { FILTER_CELL_ACTION_TYPE } from '@kbn/cell-actions/constants';
import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import { useLegendAction } from '@elastic/charts';
import type { CellValueAction } from '../types';
import { shouldShowLegendActionDefault } from '../../common/helpers/visualization';
const hasFilterCellAction = (actions: CellValueAction[]) => {
return actions.some(({ type }) => type === FILTER_CELL_ACTION_TYPE);
};
export type LegendCellValueActions = Array<
Omit<CellValueAction, 'execute'> & { execute: () => void }
@ -36,20 +34,18 @@ export interface LegendActionPopoverProps {
* Compatible actions to be added to the popover actions
*/
legendCellValueActions?: LegendCellValueActions;
shouldShowLegendAction?: (actionId: string) => boolean;
}
export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverProps> = ({
label,
onFilter,
legendCellValueActions = [],
shouldShowLegendAction = shouldShowLegendActionDefault,
}) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [ref, onClose] = useLegendAction<HTMLDivElement>();
const panels: EuiContextMenuPanelDescriptor[] = useMemo(() => {
const defaultActions = [
const defaultFilterActions = [
{
id: 'filterIn',
displayName: i18n.translate('expressionXY.legend.filterForValueButtonAriaLabel', {
@ -76,22 +72,21 @@ export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverPro
},
];
const legendCellValueActionPanelItems = [...defaultActions, ...legendCellValueActions].reduce<
EuiContextMenuPanelItemDescriptor[]
>((acc, action) => {
if (shouldShowLegendAction(action.id)) {
acc.push({
name: action.displayName,
'data-test-subj': `legend-${label}-${action.id}`,
icon: <EuiIcon type={action.iconType} size="m" />,
onClick: () => {
action.execute();
setPopoverOpen(false);
},
});
}
return acc;
}, []);
const allActions = [
...(!hasFilterCellAction(legendCellValueActions) ? defaultFilterActions : []),
...legendCellValueActions,
];
const legendCellValueActionPanelItems = allActions.map((action) => ({
name: action.displayName,
'data-test-subj': `legend-${label}-${action.id}`,
icon: <EuiIcon type={action.iconType} size="m" />,
onClick: () => {
action.execute();
setPopoverOpen(false);
},
}));
return [
{
id: 'main',
@ -99,7 +94,7 @@ export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverPro
items: legendCellValueActionPanelItems,
},
];
}, [label, legendCellValueActions, onFilter, shouldShowLegendAction]);
}, [label, legendCellValueActions, onFilter]);
const Button = (
<div

View file

@ -145,7 +145,6 @@ export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
renderComplete: () => void;
uiState?: PersistedState;
timeFormat: string;
shouldShowLegendAction?: (actionId: string) => boolean;
};
function nonNullable<T>(v: T): v is NonNullable<T> {
@ -208,7 +207,6 @@ export function XYChart({
uiState,
timeFormat,
overrides,
shouldShowLegendAction,
}: XYChartRenderProps) {
const {
legend,
@ -841,7 +839,6 @@ export function XYChart({
fieldFormats,
formattedDatatables,
titles,
shouldShowLegendAction,
singleTable
)
: undefined

View file

@ -277,7 +277,6 @@ export const getXyChartRenderer = ({
syncCursor={config.syncCursor}
uiState={handlers.uiState as PersistedState}
renderComplete={renderComplete}
shouldShowLegendAction={handlers.shouldShowLegendAction}
/>
</div>
</I18nProvider>

View file

@ -120,6 +120,7 @@ export interface VisualizationType {
export interface CellValueAction {
id: string;
type?: string;
iconType: string;
displayName: string;
execute: (data: CellValueContext['data']) => void;

View file

@ -34,6 +34,7 @@
"@kbn/event-annotation-common",
"@kbn/visualization-ui-components",
"@kbn/es-query",
"@kbn/cell-actions",
],
"exclude": [
"target/**/*",

View file

@ -84,8 +84,6 @@ export interface ExecutionContext<
* Logs datatable.
*/
logDatatable?(name: string, datatable: Datatable): void;
shouldShowLegendAction?: (actionId: string) => boolean;
}
/**

View file

@ -105,5 +105,4 @@ export interface IInterpreterRenderHandlers {
uiState?: unknown;
getExecutionContext(): KibanaExecutionContext | undefined;
shouldShowLegendAction?: (actionId: string) => boolean;
}

View file

@ -63,7 +63,6 @@ export class ExpressionLoader {
hasCompatibleActions: params?.hasCompatibleActions,
getCompatibleCellValueActions: params?.getCompatibleCellValueActions,
executionContext: params?.executionContext,
shouldShowLegendAction: params?.shouldShowLegendAction,
});
this.render$ = this.renderHandler.render$;
this.update$ = this.renderHandler.update$;

View file

@ -24,7 +24,6 @@ export interface ReactExpressionRendererProps
error?: ExpressionRenderError | null
) => React.ReactElement | React.ReactElement[];
padding?: 'xs' | 's' | 'm' | 'l' | 'xl';
shouldShowLegendAction?: (actionId: string) => boolean;
}
export type ReactExpressionRendererType = React.ComponentType<ReactExpressionRendererProps>;

View file

@ -36,7 +36,6 @@ export interface ExpressionRenderHandlerParams {
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
getCompatibleCellValueActions?: (data: object[]) => Promise<unknown[]>;
executionContext?: KibanaExecutionContext;
shouldShowLegendAction?: (actionId: string) => boolean;
}
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
@ -67,7 +66,6 @@ export class ExpressionRenderHandler {
hasCompatibleActions = async () => false,
getCompatibleCellValueActions = async () => [],
executionContext,
shouldShowLegendAction,
}: ExpressionRenderHandlerParams = {}
) {
this.element = element;
@ -120,9 +118,6 @@ export class ExpressionRenderHandler {
},
hasCompatibleActions,
getCompatibleCellValueActions,
shouldShowLegendAction: (actionId: string) => {
return shouldShowLegendAction?.(actionId) ?? true;
},
};
}

View file

@ -67,7 +67,6 @@ export interface IExpressionLoaderParams {
* By default, it equals 1000.
*/
throttle?: number;
shouldShowLegendAction?: (actionId: string) => boolean;
}
export interface ExpressionRenderError extends Error {

View file

@ -176,7 +176,6 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
onTableRowClick?: (
data: Simplify<LensTableRowContextMenuEvent['data'] & PreventableEvent>
) => void;
shouldShowLegendAction?: (actionId: string) => boolean;
}
export type LensByValueInput = {
@ -1110,7 +1109,6 @@ export class Embeddable
}}
noPadding={this.visDisplayOptions.noPadding}
docLinks={this.deps.coreStart.docLinks}
shouldShowLegendAction={input.shouldShowLegendAction}
/>
</KibanaThemeProvider>
<MessagesBadge
@ -1218,6 +1216,7 @@ export class Embeddable
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
.map((action) => ({
id: action.id,
type: action.type,
iconType: action.getIconType({ embeddable, data, trigger: cellValueTrigger })!,
displayName: action.getDisplayName({ embeddable, data, trigger: cellValueTrigger }),
execute: (cellData) =>

View file

@ -47,7 +47,6 @@ export interface ExpressionWrapperProps {
lensInspector: LensInspector;
noPadding?: boolean;
docLinks: CoreStart['docLinks'];
shouldShowLegendAction?: (actionId: string) => boolean;
}
export function ExpressionWrapper({
@ -74,7 +73,6 @@ export function ExpressionWrapper({
lensInspector,
noPadding,
docLinks,
shouldShowLegendAction,
}: ExpressionWrapperProps) {
if (!expression) return null;
return (
@ -106,7 +104,6 @@ export function ExpressionWrapper({
onEvent={handleEvent}
hasCompatibleActions={hasCompatibleActions}
getCompatibleCellValueActions={getCompatibleCellValueActions}
shouldShowLegendAction={shouldShowLegendAction}
/>
</div>
</I18nProvider>

View file

@ -1416,6 +1416,7 @@ export type LensTopNavMenuEntryGenerator = (props: {
export interface LensCellValueAction {
id: string;
iconType: string;
type?: string;
displayName: string;
execute: (data: CellValueContext['data']) => void;
}

View file

@ -19,10 +19,15 @@ import type {
DatatableColumnMeta,
} from '@kbn/expressions-plugin/common';
import { EuiDataGridColumnCellAction } from '@elastic/eui/src/components/datagrid/data_grid_types';
import { FILTER_CELL_ACTION_TYPE } from '@kbn/cell-actions/constants';
import type { FormatFactory } from '../../../../common/types';
import type { ColumnConfig } from '../../../../common/expressions';
import { LensCellValueAction } from '../../../types';
const hasFilterCellAction = (actions: LensCellValueAction[]) => {
return actions.some(({ type }) => type === FILTER_CELL_ACTION_TYPE);
};
export const createGridColumns = (
bucketColumns: string[],
table: Datatable,
@ -81,7 +86,16 @@ export const createGridColumns = (
const columnArgs = columnConfig.columns.find(({ columnId }) => columnId === field);
const cellActions: EuiDataGridColumnCellAction[] = [];
if (filterable && handleFilterClick && !columnArgs?.oneClickFilter) {
// compatible cell actions from actions registry
const compatibleCellActions = columnCellValueActions?.[colIndex] ?? [];
if (
!hasFilterCellAction(compatibleCellActions) &&
filterable &&
handleFilterClick &&
!columnArgs?.oneClickFilter
) {
cellActions.push(
({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
const { rowValue, contentsIsDefined, cellContent } = getContentData({
@ -166,8 +180,6 @@ export const createGridColumns = (
);
}
// Add all the column compatible cell actions
const compatibleCellActions = columnCellValueActions?.[colIndex] ?? [];
compatibleCellActions.forEach((action) => {
cellActions.push(({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
const rowValue = table.rows[rowIndex][columnId];

View file

@ -91,7 +91,8 @@
"@kbn/logging",
"@kbn/core-plugins-server",
"@kbn/field-utils",
"@kbn/shared-ux-button-toolbar"
"@kbn/shared-ux-button-toolbar",
"@kbn/cell-actions"
],
"exclude": [
"target/**/*",

View file

@ -4,6 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FILTER_CELL_ACTION_TYPE, COPY_CELL_ACTION_TYPE } from '@kbn/cell-actions/constants';
export enum SecurityCellActionsTrigger {
DEFAULT = 'security-default-cellActions',
DETAILS_FLYOUT = 'security-detailsFlyout-cellActions',
@ -26,3 +29,8 @@ export enum SecurityCellActionType {
SHOW_TOP_N = 'security-cellAction-type-showTopN',
TOGGLE_COLUMN = 'security-cellAction-type-toggleColumn',
}
export const DefaultCellActionTypes = {
FILTER: FILTER_CELL_ACTION_TYPE,
COPY: COPY_CELL_ACTION_TYPE,
} as const;

View file

@ -9,7 +9,5 @@ export { createFilterInCellActionFactory } from './cell_action/filter_in';
export { createFilterOutCellActionFactory } from './cell_action/filter_out';
export { createFilterInDiscoverCellActionFactory } from './discover/filter_in';
export { createFilterOutDiscoverCellActionFactory } from './discover/filter_out';
export { createTimelineHistogramFilterInLegendActionFactory } from './lens/filter_in_timeline';
export { createFilterInHistogramLegendActionFactory } from './lens/filter_in';
export { createTimelineHistogramFilterOutLegendActionFactory } from './lens/filter_out_timeline';
export { createFilterOutHistogramLegendActionFactory } from './lens/filter_out';
export { createFilterInLensAction } from './lens/filter_in';
export { createFilterOutLensAction } from './lens/filter_out';

View file

@ -16,15 +16,12 @@ import type { CellValueContext } from '@kbn/embeddable-plugin/public';
import { createAction } from '@kbn/ui-actions-plugin/public';
import { ACTION_INCOMPATIBLE_VALUE_WARNING } from '@kbn/cell-actions/src/actions/translations';
import { i18n } from '@kbn/i18n';
import { KibanaServices } from '../../../common/lib/kibana';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { fieldHasCellActions, isInSecurityApp, isLensEmbeddable } from '../../utils';
import { TimelineId } from '../../../../common/types';
import { SecurityCellActionType } from '../../constants';
import { DefaultCellActionTypes } from '../../constants';
import type { SecurityAppStore } from '../../../common/store';
import type { StartServices } from '../../../types';
import { HISTOGRAM_LEGEND_ACTION_FILTER_IN } from './filter_in';
import { HISTOGRAM_LEGEND_ACTION_FILTER_OUT } from './filter_out';
function isDataColumnsValid(data?: CellValueContext['data']): boolean {
return (
@ -34,7 +31,7 @@ function isDataColumnsValid(data?: CellValueContext['data']): boolean {
);
}
export const createHistogramFilterLegendActionFactory = ({
export const createFilterLensAction = ({
id,
order,
store,
@ -47,14 +44,13 @@ export const createHistogramFilterLegendActionFactory = ({
services: StartServices;
negate?: boolean;
}) => {
const { application: applicationService } = KibanaServices.get();
const { application, notifications, data: dataService, topValuesPopover } = services;
let currentAppId: string | undefined;
applicationService.currentAppId$.subscribe((appId) => {
application.currentAppId$.subscribe((appId) => {
currentAppId = appId;
});
const getTimelineById = timelineSelectors.getTimelineByIdSelector();
const { notifications } = services;
const { filterManager } = services.data.query;
return createAction<CellValueContext>({
id,
@ -68,7 +64,7 @@ export const createHistogramFilterLegendActionFactory = ({
: i18n.translate('xpack.securitySolution.actions.filterForTimeline', {
defaultMessage: `Filter for`,
}),
type: SecurityCellActionType.FILTER,
type: DefaultCellActionTypes.FILTER,
isCompatible: async ({ embeddable, data }) =>
!isErrorEmbeddable(embeddable) &&
isLensEmbeddable(embeddable) &&
@ -85,27 +81,19 @@ export const createHistogramFilterLegendActionFactory = ({
});
return;
}
if (!field) return;
const timeline = getTimelineById(store.getState(), TimelineId.active);
services.topValuesPopover.closePopover();
topValuesPopover.closePopover();
if (!negate) {
addFilterIn({
filterManager:
id === HISTOGRAM_LEGEND_ACTION_FILTER_IN ? filterManager : timeline.filterManager,
fieldName: field,
value,
});
} else {
addFilterOut({
filterManager:
id === HISTOGRAM_LEGEND_ACTION_FILTER_OUT ? filterManager : timeline.filterManager,
fieldName: field,
value,
});
}
const addFilter = negate === true ? addFilterOut : addFilterIn;
const timeline = getTimelineById(store.getState(), TimelineId.active);
// timeline is open add the filter to timeline, otherwise add filter to global filters
const filterManager = timeline?.show
? timeline.filterManager
: dataService.query.filterManager;
addFilter({ filterManager, fieldName: field, value });
},
});
};

View file

@ -8,11 +8,11 @@
import type { SecurityAppStore } from '../../../common/store';
import type { StartServices } from '../../../types';
import { createHistogramFilterLegendActionFactory } from './helpers';
import { createFilterLensAction } from './create_action';
export const HISTOGRAM_LEGEND_ACTION_FILTER_IN = 'histogramLegendActionFilterIn';
export const ACTION_ID = 'embeddable_filterIn';
export const createFilterInHistogramLegendActionFactory = ({
export const createFilterInLensAction = ({
store,
order,
services,
@ -21,8 +21,8 @@ export const createFilterInHistogramLegendActionFactory = ({
order: number;
services: StartServices;
}) =>
createHistogramFilterLegendActionFactory({
id: HISTOGRAM_LEGEND_ACTION_FILTER_IN,
createFilterLensAction({
id: ACTION_ID,
order,
store,
services,

View file

@ -1,29 +0,0 @@
/*
* 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 type { SecurityAppStore } from '../../../common/store';
import type { StartServices } from '../../../types';
import { createHistogramFilterLegendActionFactory } from './helpers';
export const TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_IN = 'timelineHistogramLegendActionFilterIn';
export const createTimelineHistogramFilterInLegendActionFactory = ({
store,
order,
services,
}: {
store: SecurityAppStore;
order: number;
services: StartServices;
}) =>
createHistogramFilterLegendActionFactory({
id: TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_IN,
order,
store,
services,
});

View file

@ -8,11 +8,11 @@
import type { SecurityAppStore } from '../../../common/store';
import type { StartServices } from '../../../types';
import { createHistogramFilterLegendActionFactory } from './helpers';
import { createFilterLensAction } from './create_action';
export const HISTOGRAM_LEGEND_ACTION_FILTER_OUT = 'histogramLegendActionFilterOut';
export const ACTION_ID = 'embeddable_filterOut';
export const createFilterOutHistogramLegendActionFactory = ({
export const createFilterOutLensAction = ({
store,
order,
services,
@ -21,8 +21,8 @@ export const createFilterOutHistogramLegendActionFactory = ({
order: number;
services: StartServices;
}) =>
createHistogramFilterLegendActionFactory({
id: HISTOGRAM_LEGEND_ACTION_FILTER_OUT,
createFilterLensAction({
id: ACTION_ID,
order,
store,
services,

View file

@ -1,30 +0,0 @@
/*
* 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 type { SecurityAppStore } from '../../../common/store';
import type { StartServices } from '../../../types';
import { createHistogramFilterLegendActionFactory } from './helpers';
export const TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_OUT = 'timelineHistogramLegendActionFilterOut';
export const createTimelineHistogramFilterOutLegendActionFactory = ({
store,
order,
services,
}: {
store: SecurityAppStore;
order: number;
services: StartServices;
}) =>
createHistogramFilterLegendActionFactory({
id: TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_OUT,
order,
store,
services,
negate: true,
});

View file

@ -13,12 +13,8 @@ import type { StartServices } from '../types';
import {
createFilterInCellActionFactory,
createFilterInDiscoverCellActionFactory,
createTimelineHistogramFilterInLegendActionFactory,
createFilterInHistogramLegendActionFactory,
createFilterOutCellActionFactory,
createFilterOutDiscoverCellActionFactory,
createFilterOutHistogramLegendActionFactory,
createTimelineHistogramFilterOutLegendActionFactory,
} from './filter';
import {
createAddToTimelineLensAction,
@ -41,7 +37,9 @@ import type {
SecurityCellActions,
} from './types';
import { enhanceActionWithTelemetry } from './telemetry';
import { registerDiscoverHistogramActions } from './discover_in_timeline/vis_apply_filter';
import { registerDiscoverHistogramActions } from './register_discover_histogram_actions';
import { createFilterInLensAction } from './filter/lens/filter_in';
import { createFilterOutLensAction } from './filter/lens/filter_out';
export const registerUIActions = (
store: SecurityAppStore,
@ -51,45 +49,24 @@ export const registerUIActions = (
registerLensEmbeddableActions(store, services);
registerDiscoverCellActions(store, services);
registerCellActions(store, history, services);
// TODO: Remove discover histogram actions when timeline esql tab is extracted from discover
registerDiscoverHistogramActions(store, history, services);
};
const registerLensEmbeddableActions = (store: SecurityAppStore, services: StartServices) => {
const { uiActions } = services;
const filterInLegendActions = createFilterInLensAction({ store, order: 2, services });
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterInLegendActions);
const filterOutLegendActions = createFilterOutLensAction({ store, order: 3, services });
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterOutLegendActions);
const addToTimelineAction = createAddToTimelineLensAction({ store, order: 4 });
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, addToTimelineAction);
const copyToClipboardAction = createCopyToClipboardLensAction({ order: 5 });
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, copyToClipboardAction);
const filterInTimelineLegendActions = createTimelineHistogramFilterInLegendActionFactory({
store,
order: 0,
services,
});
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterInTimelineLegendActions);
const filterOutTimelineLegendActions = createTimelineHistogramFilterOutLegendActionFactory({
store,
order: 1,
services,
});
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterOutTimelineLegendActions);
const filterInLegendActions = createFilterInHistogramLegendActionFactory({
store,
order: 2,
services,
});
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterInLegendActions);
const filterOutLegendActions = createFilterOutHistogramLegendActionFactory({
store,
order: 3,
services,
});
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, filterOutLegendActions);
};
const registerDiscoverCellActions = (store: SecurityAppStore, services: StartServices) => {

View file

@ -7,9 +7,9 @@
import { createFilterAction } from '@kbn/unified-search-plugin/public';
import type { History } from 'history';
import type { SecurityAppStore } from '../../common/store';
import type { StartServices } from '../../types';
import { EsqlInTimelineTrigger, EsqlInTimelineAction } from '../constants';
import type { SecurityAppStore } from '../common/store';
import type { StartServices } from '../types';
import { EsqlInTimelineTrigger, EsqlInTimelineAction } from './constants';
const createDiscoverHistogramCustomFilterAction = (
store: SecurityAppStore,

View file

@ -24,7 +24,7 @@ import { inputsSelectors } from '../../store';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { ModalInspectQuery } from '../inspect/modal';
import { InputsModelId } from '../../store/inputs/constants';
import { getRequestsAndResponses, showLegendActionsByActionId } from './utils';
import { getRequestsAndResponses } from './utils';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { VisualizationActions } from './actions';
@ -218,11 +218,6 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
[attributes?.state?.adHocDataViews]
);
const shouldShowLegendAction = useCallback(
(actionId: string) => showLegendActionsByActionId({ actionId, scopeId }),
[scopeId]
);
if (!searchSessionId) {
return null;
}
@ -286,7 +281,6 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
showInspector={false}
syncTooltips={false}
syncCursor={false}
shouldShowLegendAction={shouldShowLegendAction}
/>
</LensComponentWrapper>
)}

View file

@ -8,10 +8,6 @@
import type { Filter } from '@kbn/es-query';
import { SecurityPageName } from '../../../../common/constants';
import { HISTOGRAM_LEGEND_ACTION_FILTER_IN } from '../../../actions/filter/lens/filter_in';
import { TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_IN } from '../../../actions/filter/lens/filter_in_timeline';
import { HISTOGRAM_LEGEND_ACTION_FILTER_OUT } from '../../../actions/filter/lens/filter_out';
import { TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_OUT } from '../../../actions/filter/lens/filter_out_timeline';
import type { Request } from './types';
export const VISUALIZATION_ACTIONS_BUTTON_CLASS = 'histogram-actions-trigger';
@ -199,28 +195,3 @@ export const parseVisualizationData = <T>(data: string[]): T[] =>
return acc;
}
}, [] as T[]);
export const showLegendActionsByActionId = ({
actionId,
scopeId,
}: {
actionId: string;
scopeId: string;
}) => {
switch (actionId) {
/** We no longer use Lens' default filter in / out actions
* as extra custom actions needed after filters applied.
* For example: hide the topN panel after filters applied */
case FILTER_IN_LEGEND_ACTION:
case FILTER_OUT_LEGEND_ACTION:
return false;
case HISTOGRAM_LEGEND_ACTION_FILTER_IN:
case HISTOGRAM_LEGEND_ACTION_FILTER_OUT:
return scopeId !== 'timeline';
case TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_IN:
case TIMELINE_HISTOGRAM_LEGEND_ACTION_FILTER_OUT:
return scopeId === 'timeline';
default:
return true;
}
};

View file

@ -140,10 +140,8 @@ export const SELECT_HISTOGRAM = '[data-test-subj="chart-select-trend"]';
export const LEGEND_ACTIONS = {
ADD_TO_TIMELINE: (ruleName: string) =>
`[data-test-subj="legend-${ruleName}-embeddable_addToTimeline"]`,
FILTER_FOR: (ruleName: string) =>
`[data-test-subj="legend-${ruleName}-histogramLegendActionFilterIn"]`,
FILTER_OUT: (ruleName: string) =>
`[data-test-subj="legend-${ruleName}-histogramLegendActionFilterOut"]`,
FILTER_FOR: (ruleName: string) => `[data-test-subj="legend-${ruleName}-embeddable_filterIn"]`,
FILTER_OUT: (ruleName: string) => `[data-test-subj="legend-${ruleName}-embeddable_filterOut"]`,
COPY: (ruleName: string) => `[data-test-subj="legend-${ruleName}-embeddable_copyToClipboard"]`,
};