mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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` visualization683eec6c
-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:
parent
48711622bd
commit
9bf7e38b1d
40 changed files with 129 additions and 262 deletions
9
packages/kbn-cell-actions/constants.ts
Normal file
9
packages/kbn-cell-actions/constants.ts
Normal 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';
|
|
@ -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';
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface MultiFilterEvent {
|
|||
|
||||
export interface CellValueAction {
|
||||
id: string;
|
||||
type?: string;
|
||||
iconType: string;
|
||||
displayName: string;
|
||||
execute: (data: CellValueContext['data']) => void;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"@kbn/analytics",
|
||||
"@kbn/chart-icons",
|
||||
"@kbn/chart-expressions-common",
|
||||
"@kbn/cell-actions",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -19,5 +19,3 @@ export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) {
|
|||
(!l.xScaleType || l.xScaleType === XScaleTypes.TIME)
|
||||
);
|
||||
}
|
||||
|
||||
export const shouldShowLegendActionDefault = () => true;
|
||||
|
|
|
@ -203,8 +203,7 @@ describe('getLegendAction', function () {
|
|||
formattedColumns: {},
|
||||
},
|
||||
},
|
||||
{},
|
||||
() => true
|
||||
{}
|
||||
);
|
||||
let wrapper: ReactWrapper<LegendActionProps>;
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -277,7 +277,6 @@ export const getXyChartRenderer = ({
|
|||
syncCursor={config.syncCursor}
|
||||
uiState={handlers.uiState as PersistedState}
|
||||
renderComplete={renderComplete}
|
||||
shouldShowLegendAction={handlers.shouldShowLegendAction}
|
||||
/>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -120,6 +120,7 @@ export interface VisualizationType {
|
|||
|
||||
export interface CellValueAction {
|
||||
id: string;
|
||||
type?: string;
|
||||
iconType: string;
|
||||
displayName: string;
|
||||
execute: (data: CellValueContext['data']) => void;
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"@kbn/event-annotation-common",
|
||||
"@kbn/visualization-ui-components",
|
||||
"@kbn/es-query",
|
||||
"@kbn/cell-actions",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -84,8 +84,6 @@ export interface ExecutionContext<
|
|||
* Logs datatable.
|
||||
*/
|
||||
logDatatable?(name: string, datatable: Datatable): void;
|
||||
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -105,5 +105,4 @@ export interface IInterpreterRenderHandlers {
|
|||
uiState?: unknown;
|
||||
|
||||
getExecutionContext(): KibanaExecutionContext | undefined;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
|
|
@ -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$;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,6 @@ export interface IExpressionLoaderParams {
|
|||
* By default, it equals 1000.
|
||||
*/
|
||||
throttle?: number;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ExpressionRenderError extends Error {
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1416,6 +1416,7 @@ export type LensTopNavMenuEntryGenerator = (props: {
|
|||
export interface LensCellValueAction {
|
||||
id: string;
|
||||
iconType: string;
|
||||
type?: string;
|
||||
displayName: string;
|
||||
execute: (data: CellValueContext['data']) => void;
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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"]`,
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue