mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SecuritySolution] Fix topN legend actions - filter in / out in timeline (#170127)
## Summary
https://github.com/elastic/kibana/issues/168199
https://github.com/elastic/kibana/issues/169656
ff5cee55
-6da5-4636-85f5-a697a302f8b5
---------
Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8420d5776b
commit
8103a44585
28 changed files with 405 additions and 45 deletions
|
@ -10,6 +10,7 @@ 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 () => {
|
||||
|
@ -30,6 +31,7 @@ describe('layeredXyVis', () => {
|
|||
syncTooltips: false,
|
||||
syncCursor: true,
|
||||
canNavigateToLens: false,
|
||||
shouldShowLegendAction: shouldShowLegendActionDefault,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ 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');
|
||||
|
@ -66,6 +67,7 @@ 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,6 +10,7 @@ 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 () => {
|
||||
|
@ -42,6 +43,7 @@ describe('xyVis', () => {
|
|||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: true,
|
||||
shouldShowLegendAction: shouldShowLegendActionDefault,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -352,6 +354,7 @@ describe('xyVis', () => {
|
|||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: true,
|
||||
shouldShowLegendAction: shouldShowLegendActionDefault,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -401,6 +404,7 @@ describe('xyVis', () => {
|
|||
syncTooltips: false,
|
||||
syncCursor: true,
|
||||
overrides,
|
||||
shouldShowLegendAction: shouldShowLegendActionDefault,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ 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);
|
||||
|
@ -139,6 +140,7 @@ 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,3 +19,5 @@ export function isTimeChart(layers: CommonXYDataLayerConfigResult[]) {
|
|||
(!l.xScaleType || l.xScaleType === XScaleTypes.TIME)
|
||||
);
|
||||
}
|
||||
|
||||
export const shouldShowLegendActionDefault = () => true;
|
||||
|
|
|
@ -203,7 +203,8 @@ describe('getLegendAction', function () {
|
|||
formattedColumns: {},
|
||||
},
|
||||
},
|
||||
{}
|
||||
{},
|
||||
() => true
|
||||
);
|
||||
let wrapper: ReactWrapper<LegendActionProps>;
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export const getLegendAction = (
|
|||
fieldFormats: LayersFieldFormats,
|
||||
formattedDatatables: DatatablesWithFormatInfo,
|
||||
titles: LayersAccessorsTitles,
|
||||
shouldShowLegendAction?: (actionId: string) => boolean,
|
||||
singleTable?: boolean
|
||||
): LegendAction =>
|
||||
React.memo(({ series: [xySeries] }) => {
|
||||
|
@ -109,6 +110,7 @@ export const getLegendAction = (
|
|||
}
|
||||
onFilter={filterHandler}
|
||||
legendCellValueActions={legendCellValueActions}
|
||||
shouldShowLegendAction={shouldShowLegendAction}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,11 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
|
||||
import {
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
} from '@elastic/eui';
|
||||
import { useLegendAction } from '@elastic/charts';
|
||||
import type { CellValueAction } from '../types';
|
||||
import { shouldShowLegendActionDefault } from '../../common/helpers/visualization';
|
||||
|
||||
export type LegendCellValueActions = Array<
|
||||
Omit<CellValueAction, 'execute'> & { execute: () => void }
|
||||
|
@ -29,57 +36,70 @@ 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 legendCellValueActionPanelItems = legendCellValueActions.map((action) => ({
|
||||
name: action.displayName,
|
||||
'data-test-subj': `legend-${label}-${action.id}`,
|
||||
icon: <EuiIcon type={action.iconType} size="m" />,
|
||||
onClick: () => {
|
||||
action.execute();
|
||||
setPopoverOpen(false);
|
||||
},
|
||||
}));
|
||||
const panels: EuiContextMenuPanelDescriptor[] = useMemo(() => {
|
||||
const defaultActions = [
|
||||
{
|
||||
id: 'filterIn',
|
||||
displayName: i18n.translate('expressionXY.legend.filterForValueButtonAriaLabel', {
|
||||
defaultMessage: 'Filter for',
|
||||
}),
|
||||
'data-test-subj': `legend-${label}-filterIn`,
|
||||
iconType: 'plusInCircle',
|
||||
execute: () => {
|
||||
setPopoverOpen(false);
|
||||
onFilter();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filterOut',
|
||||
displayName: i18n.translate('expressionXY.legend.filterOutValueButtonAriaLabel', {
|
||||
defaultMessage: 'Filter out',
|
||||
}),
|
||||
'data-test-subj': `legend-${label}-filterOut`,
|
||||
iconType: 'minusInCircle',
|
||||
execute: () => {
|
||||
setPopoverOpen(false);
|
||||
onFilter({ negate: true });
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 'main',
|
||||
title: label,
|
||||
items: [
|
||||
{
|
||||
name: i18n.translate('expressionXY.legend.filterForValueButtonAriaLabel', {
|
||||
defaultMessage: 'Filter for',
|
||||
}),
|
||||
'data-test-subj': `legend-${label}-filterIn`,
|
||||
icon: <EuiIcon type="plusInCircle" size="m" />,
|
||||
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);
|
||||
onFilter();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('expressionXY.legend.filterOutValueButtonAriaLabel', {
|
||||
defaultMessage: 'Filter out',
|
||||
}),
|
||||
'data-test-subj': `legend-${label}-filterOut`,
|
||||
icon: <EuiIcon type="minusInCircle" size="m" />,
|
||||
onClick: () => {
|
||||
setPopoverOpen(false);
|
||||
onFilter({ negate: true });
|
||||
},
|
||||
},
|
||||
...legendCellValueActionPanelItems,
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return [
|
||||
{
|
||||
id: 'main',
|
||||
title: label,
|
||||
items: legendCellValueActionPanelItems,
|
||||
},
|
||||
];
|
||||
}, [label, legendCellValueActions, onFilter, shouldShowLegendAction]);
|
||||
|
||||
const Button = (
|
||||
<div
|
||||
|
|
|
@ -145,6 +145,7 @@ 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> {
|
||||
|
@ -207,6 +208,7 @@ export function XYChart({
|
|||
uiState,
|
||||
timeFormat,
|
||||
overrides,
|
||||
shouldShowLegendAction,
|
||||
}: XYChartRenderProps) {
|
||||
const {
|
||||
legend,
|
||||
|
@ -839,6 +841,7 @@ export function XYChart({
|
|||
fieldFormats,
|
||||
formattedDatatables,
|
||||
titles,
|
||||
shouldShowLegendAction,
|
||||
singleTable
|
||||
)
|
||||
: undefined
|
||||
|
|
|
@ -277,6 +277,7 @@ export const getXyChartRenderer = ({
|
|||
syncCursor={config.syncCursor}
|
||||
uiState={handlers.uiState as PersistedState}
|
||||
renderComplete={renderComplete}
|
||||
shouldShowLegendAction={handlers.shouldShowLegendAction}
|
||||
/>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -84,6 +84,8 @@ export interface ExecutionContext<
|
|||
* Logs datatable.
|
||||
*/
|
||||
logDatatable?(name: string, datatable: Datatable): void;
|
||||
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -105,4 +105,5 @@ export interface IInterpreterRenderHandlers {
|
|||
uiState?: unknown;
|
||||
|
||||
getExecutionContext(): KibanaExecutionContext | undefined;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ 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,6 +24,7 @@ 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,6 +36,7 @@ export interface ExpressionRenderHandlerParams {
|
|||
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
|
||||
getCompatibleCellValueActions?: (data: object[]) => Promise<unknown[]>;
|
||||
executionContext?: KibanaExecutionContext;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
|
||||
|
@ -66,6 +67,7 @@ export class ExpressionRenderHandler {
|
|||
hasCompatibleActions = async () => false,
|
||||
getCompatibleCellValueActions = async () => [],
|
||||
executionContext,
|
||||
shouldShowLegendAction,
|
||||
}: ExpressionRenderHandlerParams = {}
|
||||
) {
|
||||
this.element = element;
|
||||
|
@ -118,6 +120,9 @@ export class ExpressionRenderHandler {
|
|||
},
|
||||
hasCompatibleActions,
|
||||
getCompatibleCellValueActions,
|
||||
shouldShowLegendAction: (actionId: string) => {
|
||||
return shouldShowLegendAction?.(actionId) ?? true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ export interface IExpressionLoaderParams {
|
|||
* By default, it equals 1000.
|
||||
*/
|
||||
throttle?: number;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ExpressionRenderError extends Error {
|
||||
|
|
|
@ -176,6 +176,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
|
|||
onTableRowClick?: (
|
||||
data: Simplify<LensTableRowContextMenuEvent['data'] & PreventableEvent>
|
||||
) => void;
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
export type LensByValueInput = {
|
||||
|
@ -1103,6 +1104,7 @@ export class Embeddable
|
|||
}}
|
||||
noPadding={this.visDisplayOptions.noPadding}
|
||||
docLinks={this.deps.coreStart.docLinks}
|
||||
shouldShowLegendAction={input.shouldShowLegendAction}
|
||||
/>
|
||||
</KibanaThemeProvider>
|
||||
<MessagesBadge
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface ExpressionWrapperProps {
|
|||
lensInspector: LensInspector;
|
||||
noPadding?: boolean;
|
||||
docLinks: CoreStart['docLinks'];
|
||||
shouldShowLegendAction?: (actionId: string) => boolean;
|
||||
}
|
||||
|
||||
export function ExpressionWrapper({
|
||||
|
@ -73,6 +74,7 @@ export function ExpressionWrapper({
|
|||
lensInspector,
|
||||
noPadding,
|
||||
docLinks,
|
||||
shouldShowLegendAction,
|
||||
}: ExpressionWrapperProps) {
|
||||
if (!expression) return null;
|
||||
return (
|
||||
|
@ -104,6 +106,7 @@ export function ExpressionWrapper({
|
|||
onEvent={handleEvent}
|
||||
hasCompatibleActions={hasCompatibleActions}
|
||||
getCompatibleCellValueActions={getCompatibleCellValueActions}
|
||||
shouldShowLegendAction={shouldShowLegendAction}
|
||||
/>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -9,3 +9,7 @@ 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';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 HISTOGRAM_LEGEND_ACTION_FILTER_IN = 'histogramLegendActionFilterIn';
|
||||
|
||||
export const createFilterInHistogramLegendActionFactory = ({
|
||||
store,
|
||||
order,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
order: number;
|
||||
services: StartServices;
|
||||
}) =>
|
||||
createHistogramFilterLegendActionFactory({
|
||||
id: HISTOGRAM_LEGEND_ACTION_FILTER_IN,
|
||||
order,
|
||||
store,
|
||||
services,
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 HISTOGRAM_LEGEND_ACTION_FILTER_OUT = 'histogramLegendActionFilterOut';
|
||||
|
||||
export const createFilterOutHistogramLegendActionFactory = ({
|
||||
store,
|
||||
order,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
order: number;
|
||||
services: StartServices;
|
||||
}) =>
|
||||
createHistogramFilterLegendActionFactory({
|
||||
id: HISTOGRAM_LEGEND_ACTION_FILTER_OUT,
|
||||
order,
|
||||
store,
|
||||
services,
|
||||
negate: true,
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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,
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { addFilterIn, addFilterOut } from '@kbn/cell-actions';
|
||||
import {
|
||||
isValueSupportedByDefaultActions,
|
||||
valueToArray,
|
||||
filterOutNullableValues,
|
||||
} from '@kbn/cell-actions/src/actions/utils';
|
||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
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 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 (
|
||||
!!data &&
|
||||
data.length > 0 &&
|
||||
data.every(({ columnMeta }) => columnMeta && fieldHasCellActions(columnMeta.field))
|
||||
);
|
||||
}
|
||||
|
||||
export const createHistogramFilterLegendActionFactory = ({
|
||||
id,
|
||||
order,
|
||||
store,
|
||||
services,
|
||||
negate,
|
||||
}: {
|
||||
id: string;
|
||||
order: number;
|
||||
store: SecurityAppStore;
|
||||
services: StartServices;
|
||||
negate?: boolean;
|
||||
}) => {
|
||||
const { application: applicationService } = KibanaServices.get();
|
||||
let currentAppId: string | undefined;
|
||||
applicationService.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
const getTimelineById = timelineSelectors.getTimelineByIdSelector();
|
||||
const { notifications } = services;
|
||||
const { filterManager } = services.data.query;
|
||||
|
||||
return createAction<CellValueContext>({
|
||||
id,
|
||||
order,
|
||||
getIconType: () => (negate ? 'minusInCircle' : 'plusInCircle'),
|
||||
getDisplayName: () =>
|
||||
negate
|
||||
? i18n.translate('xpack.securitySolution.actions.filterOutTimeline', {
|
||||
defaultMessage: `Filter out`,
|
||||
})
|
||||
: i18n.translate('xpack.securitySolution.actions.filterForTimeline', {
|
||||
defaultMessage: `Filter for`,
|
||||
}),
|
||||
type: SecurityCellActionType.FILTER,
|
||||
isCompatible: async ({ embeddable, data }) =>
|
||||
!isErrorEmbeddable(embeddable) &&
|
||||
isLensEmbeddable(embeddable) &&
|
||||
isDataColumnsValid(data) &&
|
||||
isInSecurityApp(currentAppId),
|
||||
execute: async ({ data }) => {
|
||||
const field = data[0]?.columnMeta?.field;
|
||||
const rawValue = data[0]?.value;
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (!isValueSupportedByDefaultActions(value)) {
|
||||
notifications.toasts.addWarning({
|
||||
title: ACTION_INCOMPATIBLE_VALUE_WARNING,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!field) return;
|
||||
|
||||
const timeline = getTimelineById(store.getState(), TimelineId.active);
|
||||
services.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,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -13,8 +13,12 @@ import type { StartServices } from '../types';
|
|||
import {
|
||||
createFilterInCellActionFactory,
|
||||
createFilterInDiscoverCellActionFactory,
|
||||
createTimelineHistogramFilterInLegendActionFactory,
|
||||
createFilterInHistogramLegendActionFactory,
|
||||
createFilterOutCellActionFactory,
|
||||
createFilterOutDiscoverCellActionFactory,
|
||||
createFilterOutHistogramLegendActionFactory,
|
||||
createTimelineHistogramFilterOutLegendActionFactory,
|
||||
} from './filter';
|
||||
import {
|
||||
createAddToTimelineLensAction,
|
||||
|
@ -53,11 +57,39 @@ export const registerUIActions = (
|
|||
const registerLensEmbeddableActions = (store: SecurityAppStore, services: StartServices) => {
|
||||
const { uiActions } = services;
|
||||
|
||||
const addToTimelineAction = createAddToTimelineLensAction({ store, order: 1 });
|
||||
const addToTimelineAction = createAddToTimelineLensAction({ store, order: 4 });
|
||||
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, addToTimelineAction);
|
||||
|
||||
const copyToClipboardAction = createCopyToClipboardLensAction({ order: 2 });
|
||||
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) => {
|
||||
|
|
|
@ -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 } from './utils';
|
||||
import { getRequestsAndResponses, showLegendActionsByActionId } from './utils';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { VisualizationActions } from './actions';
|
||||
|
||||
|
@ -218,6 +218,11 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
|
|||
[attributes?.state?.adHocDataViews]
|
||||
);
|
||||
|
||||
const shouldShowLegendAction = useCallback(
|
||||
(actionId: string) => showLegendActionsByActionId({ actionId, scopeId }),
|
||||
[scopeId]
|
||||
);
|
||||
|
||||
if (!searchSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
@ -281,6 +286,7 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
|
|||
showInspector={false}
|
||||
syncTooltips={false}
|
||||
syncCursor={false}
|
||||
shouldShowLegendAction={shouldShowLegendAction}
|
||||
/>
|
||||
</LensComponentWrapper>
|
||||
)}
|
||||
|
|
|
@ -6,10 +6,17 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
export const FILTER_IN_LEGEND_ACTION = `filterIn`;
|
||||
export const FILTER_OUT_LEGEND_ACTION = `filterOut`;
|
||||
|
||||
const pageFilterFieldMap: Record<string, string> = {
|
||||
[SecurityPageName.hosts]: 'host',
|
||||
|
@ -192,3 +199,28 @@ 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,8 +140,10 @@ 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}-filterIn"]`,
|
||||
FILTER_OUT: (ruleName: string) => `[data-test-subj="legend-${ruleName}-filterOut"]`,
|
||||
FILTER_FOR: (ruleName: string) =>
|
||||
`[data-test-subj="legend-${ruleName}-histogramLegendActionFilterIn"]`,
|
||||
FILTER_OUT: (ruleName: string) =>
|
||||
`[data-test-subj="legend-${ruleName}-histogramLegendActionFilterOut"]`,
|
||||
COPY: (ruleName: string) => `[data-test-subj="legend-${ruleName}-embeddable_copyToClipboard"]`,
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue