mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Visualizations] Enables multiple values filtering on tooltip actions (#148372)
## Summary Enables tooltip actions on: - Lens embeddables (as soon as the unified search bar exists) - Lens - Agg based (as they share the same renderer with Lens) I am not enabling this in TSVB.  Also the action is not enabled for Lens if: - there are only series with metrics ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f3741b7a61
commit
5ec4bde603
24 changed files with 802 additions and 156 deletions
|
@ -56,7 +56,7 @@ const cleanUpFilter = (filter: Filter) => {
|
|||
export function buildCombinedFilter(
|
||||
relation: BooleanRelation,
|
||||
filters: Filter[],
|
||||
indexPattern: DataViewBase,
|
||||
indexPattern: Pick<DataViewBase, 'id'>,
|
||||
disabled: FilterMeta['disabled'] = false,
|
||||
negate: FilterMeta['negate'] = false,
|
||||
alias?: FilterMeta['alias'],
|
||||
|
|
|
@ -557,6 +557,19 @@ exports[`XYChart component it renders area 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -608,14 +621,6 @@ exports[`XYChart component it renders area 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -1539,6 +1544,19 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -1590,14 +1608,6 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -2521,6 +2531,19 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -2572,14 +2595,6 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -3503,6 +3518,19 @@ exports[`XYChart component it renders line 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -3554,14 +3582,6 @@ exports[`XYChart component it renders line 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -4485,6 +4505,19 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -4536,14 +4569,6 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -5467,6 +5492,19 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -5518,14 +5556,6 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -6449,6 +6479,19 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -6500,14 +6543,6 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -7461,6 +7496,19 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -7512,14 +7560,6 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -8681,6 +8721,19 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -8732,14 +8785,6 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
@ -9894,6 +9939,19 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
<Chart
|
||||
renderer="canvas"
|
||||
>
|
||||
<Tooltip
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": [Function],
|
||||
"label": [Function],
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
headerFormatter={[Function]}
|
||||
type="vertical"
|
||||
/>
|
||||
<Settings
|
||||
allowBrushingLastHistogramBin={false}
|
||||
ariaUseDefaultSummary={true}
|
||||
|
@ -9945,14 +10003,6 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
"markSizeRatio": undefined,
|
||||
}
|
||||
}
|
||||
tooltip={
|
||||
Object {
|
||||
"boundary": undefined,
|
||||
"customTooltip": undefined,
|
||||
"headerFormatter": [Function],
|
||||
"type": "vertical",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<XYCurrentTime
|
||||
enabled={false}
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
SmallMultiples,
|
||||
VerticalAlignment,
|
||||
XYChartSeriesIdentifier,
|
||||
Tooltip,
|
||||
} from '@elastic/charts';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||
|
@ -61,6 +62,7 @@ import { LegendSize } from '@kbn/visualizations-plugin/common';
|
|||
import type { LayerCellValueActions } from '../types';
|
||||
|
||||
const onClickValue = jest.fn();
|
||||
const onClickMultiValue = jest.fn();
|
||||
const layerCellValueActions: LayerCellValueActions = [];
|
||||
const onSelectRange = jest.fn();
|
||||
|
||||
|
@ -116,6 +118,7 @@ describe('XYChart component', () => {
|
|||
paletteService,
|
||||
minInterval: 50,
|
||||
onClickValue,
|
||||
onClickMultiValue,
|
||||
layerCellValueActions,
|
||||
onSelectRange,
|
||||
syncColors: false,
|
||||
|
@ -1102,6 +1105,126 @@ describe('XYChart component', () => {
|
|||
expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true);
|
||||
});
|
||||
|
||||
test('should not have tooltip actions for the detailed tooltip', () => {
|
||||
const { args, data } = sampleArgs();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
args={{
|
||||
...args,
|
||||
detailedTooltip: true,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
isStacked: true,
|
||||
isHorizontal: false,
|
||||
isPercentage: false,
|
||||
showLines: true,
|
||||
xAccessor: 'b',
|
||||
xScaleType: 'time',
|
||||
splitAccessors: ['b'],
|
||||
accessors: ['d'],
|
||||
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
||||
palette: mockPaletteOutput,
|
||||
table: data,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltip = wrapper.find(Tooltip);
|
||||
const actions = tooltip.prop('actions');
|
||||
expect(actions).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not have tooltip actions for no split accessor', () => {
|
||||
const { args, data } = sampleArgs();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
args={{
|
||||
...args,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
isStacked: true,
|
||||
isHorizontal: false,
|
||||
isPercentage: false,
|
||||
showLines: true,
|
||||
xAccessor: 'b',
|
||||
xScaleType: 'time',
|
||||
splitAccessors: undefined,
|
||||
accessors: ['d'],
|
||||
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
||||
palette: mockPaletteOutput,
|
||||
table: data,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltip = wrapper.find(Tooltip);
|
||||
const actions = tooltip.prop('actions');
|
||||
expect(actions).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should have tooltip actions for split accessor and default tooltip', () => {
|
||||
const { args, data } = sampleArgs();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
args={{
|
||||
...args,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
type: 'dataLayer',
|
||||
layerType: LayerTypes.DATA,
|
||||
isHistogram: true,
|
||||
seriesType: 'bar',
|
||||
isStacked: true,
|
||||
isHorizontal: false,
|
||||
isPercentage: false,
|
||||
showLines: true,
|
||||
xAccessor: 'b',
|
||||
xScaleType: 'time',
|
||||
splitAccessors: ['d'],
|
||||
accessors: ['d'],
|
||||
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
|
||||
palette: mockPaletteOutput,
|
||||
table: data,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltip = wrapper.find(Tooltip);
|
||||
const actions = tooltip.prop('actions');
|
||||
expect(actions?.length).toBe(1);
|
||||
expect(actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
onSelect: expect.any(Function),
|
||||
disabled: expect.any(Function),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('onElementClick returns correct context data', () => {
|
||||
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} };
|
||||
const series = {
|
||||
|
@ -3284,14 +3407,11 @@ describe('XYChart component', () => {
|
|||
}}
|
||||
/>
|
||||
);
|
||||
const settings = component.find(Settings);
|
||||
const tooltip = settings.prop('tooltip');
|
||||
expect(tooltip).toEqual(
|
||||
expect.objectContaining({
|
||||
headerFormatter: undefined,
|
||||
customTooltip: expect.any(Function),
|
||||
})
|
||||
);
|
||||
const tooltip = component.find(Tooltip);
|
||||
const customTooltip = tooltip.prop('customTooltip');
|
||||
expect(customTooltip).not.toBeUndefined();
|
||||
const headerFormatter = tooltip.prop('headerFormatter');
|
||||
expect(headerFormatter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should render default tooltip, if detailed tooltip is hidden', () => {
|
||||
|
@ -3306,14 +3426,11 @@ describe('XYChart component', () => {
|
|||
}}
|
||||
/>
|
||||
);
|
||||
const settings = component.find(Settings);
|
||||
const tooltip = settings.prop('tooltip');
|
||||
expect(tooltip).toEqual(
|
||||
expect.objectContaining({
|
||||
headerFormatter: expect.any(Function),
|
||||
customTooltip: undefined,
|
||||
})
|
||||
);
|
||||
const tooltip = component.find(Tooltip);
|
||||
const customTooltip = tooltip.prop('customTooltip');
|
||||
expect(customTooltip).toBeUndefined();
|
||||
const headerFormatter = tooltip.prop('headerFormatter');
|
||||
expect(headerFormatter).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Chart,
|
||||
Settings,
|
||||
|
@ -27,6 +28,9 @@ import {
|
|||
Placement,
|
||||
Direction,
|
||||
XYChartElementEvent,
|
||||
Tooltip,
|
||||
XYChartSeriesIdentifier,
|
||||
TooltipValue,
|
||||
} from '@elastic/charts';
|
||||
import { partition } from 'lodash';
|
||||
import { IconType } from '@elastic/eui';
|
||||
|
@ -47,7 +51,13 @@ import {
|
|||
LegendSizeToPixels,
|
||||
} from '@kbn/visualizations-plugin/common/constants';
|
||||
import { PersistedState } from '@kbn/visualizations-plugin/public';
|
||||
import type { FilterEvent, BrushEvent, FormatFactory, LayerCellValueActions } from '../types';
|
||||
import type {
|
||||
FilterEvent,
|
||||
BrushEvent,
|
||||
FormatFactory,
|
||||
LayerCellValueActions,
|
||||
MultiFilterEvent,
|
||||
} from '../types';
|
||||
import { isTimeChart } from '../../common/helpers';
|
||||
import type {
|
||||
CommonXYDataLayerConfig,
|
||||
|
@ -94,7 +104,7 @@ import {
|
|||
} from './annotations';
|
||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants';
|
||||
import { DataLayers } from './data_layers';
|
||||
import { Tooltip } from './tooltip';
|
||||
import { Tooltip as CustomTooltip } from './tooltip';
|
||||
import { XYCurrentTime } from './xy_current_time';
|
||||
|
||||
import './xy_chart.scss';
|
||||
|
@ -121,6 +131,7 @@ export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
|
|||
minInterval: number | undefined;
|
||||
interactive?: boolean;
|
||||
onClickValue: (data: FilterEvent['data']) => void;
|
||||
onClickMultiValue: (data: MultiFilterEvent['data']) => void;
|
||||
layerCellValueActions: LayerCellValueActions;
|
||||
onSelectRange: (data: BrushEvent['data']) => void;
|
||||
renderMode: RenderMode;
|
||||
|
@ -133,6 +144,10 @@ export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
|
|||
timeFormat: string;
|
||||
};
|
||||
|
||||
function nonNullable<T>(v: T): v is NonNullable<T> {
|
||||
return v != null;
|
||||
}
|
||||
|
||||
function getValueLabelsStyling(isHorizontal: boolean): {
|
||||
displayValue: RecursivePartial<DisplayValueStyle>;
|
||||
} {
|
||||
|
@ -173,6 +188,9 @@ function createSplitPoint(
|
|||
table: Datatable
|
||||
) {
|
||||
const splitPointRowIndex = rows.findIndex((row) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.includes(row[accessor]);
|
||||
}
|
||||
return row[accessor] === value;
|
||||
});
|
||||
if (splitPointRowIndex !== -1) {
|
||||
|
@ -197,6 +215,7 @@ export function XYChart({
|
|||
paletteService,
|
||||
minInterval,
|
||||
onClickValue,
|
||||
onClickMultiValue,
|
||||
layerCellValueActions,
|
||||
onSelectRange,
|
||||
interactive = true,
|
||||
|
@ -534,6 +553,54 @@ export function XYChart({
|
|||
valueLabels !== ValueLabelModes.HIDE &&
|
||||
getValueLabelsStyling(shouldRotate);
|
||||
|
||||
const filterSelectedTooltipValues = (
|
||||
tooltipSelectedValues: Array<
|
||||
TooltipValue<Record<string, string | number>, XYChartSeriesIdentifier>
|
||||
>
|
||||
) => {
|
||||
const layerIndexes: number[] = [];
|
||||
tooltipSelectedValues.forEach((v) => {
|
||||
const index = dataLayers.findIndex((l) =>
|
||||
v.seriesIdentifier.seriesKeys.some((key: string | number) =>
|
||||
l.accessors.some(
|
||||
(accessor) => getAccessorByDimension(accessor, l.table.columns) === key.toString()
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!layerIndexes.includes(index) && index !== -1) {
|
||||
layerIndexes.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerIndexes.length) return;
|
||||
layerIndexes.forEach((layerIndex) => {
|
||||
const layer = dataLayers[layerIndex];
|
||||
const { table } = layer;
|
||||
|
||||
if (layer.splitAccessors?.length !== 1) return;
|
||||
|
||||
const splitAccessor = getAccessorByDimension(layer.splitAccessors[0], table.columns);
|
||||
const filterValues = tooltipSelectedValues
|
||||
.map((v) => v.datum?.[splitAccessor])
|
||||
.filter(nonNullable);
|
||||
|
||||
const splitPoints = filterValues
|
||||
.map((v) =>
|
||||
createSplitPoint(splitAccessor, v, formattedDatatables[layer.layerId].table.rows, table)
|
||||
)
|
||||
.filter(nonNullable);
|
||||
if (splitPoints.length) {
|
||||
onClickMultiValue({
|
||||
data: {
|
||||
column: splitPoints[0].column,
|
||||
value: splitPoints.map(({ value }) => value),
|
||||
table,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clickHandler: ElementClickListener = ([elementEvent]) => {
|
||||
// this cast is safe because we are rendering a cartesian chart
|
||||
const [xyGeometry, xySeries] = elementEvent as XYChartElementEvent;
|
||||
|
@ -722,6 +789,8 @@ export function XYChart({
|
|||
overflowX: 'hidden',
|
||||
position: uiState ? 'absolute' : 'relative',
|
||||
});
|
||||
// enable the tooltip actions only if there is at least one splitAccessor to the dataLayer
|
||||
const hasTooltipActions = dataLayers.some((dataLayer) => dataLayer.splitAccessors) && interactive;
|
||||
|
||||
return (
|
||||
<div css={chartContainerStyle}>
|
||||
|
@ -745,6 +814,60 @@ export function XYChart({
|
|||
}}
|
||||
>
|
||||
<Chart ref={chartRef}>
|
||||
<Tooltip<Record<string, string | number>, XYChartSeriesIdentifier>
|
||||
boundary={document.getElementById('app-fixed-viewport') ?? undefined}
|
||||
headerFormatter={
|
||||
!args.detailedTooltip
|
||||
? ({ value }) => (
|
||||
<TooltipHeader
|
||||
value={value}
|
||||
formatter={safeXAccessorLabelRenderer}
|
||||
xDomain={rawXDomain}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
!args.detailedTooltip && hasTooltipActions
|
||||
? [
|
||||
{
|
||||
disabled: (selected) => selected.length < 1,
|
||||
label: (selected) =>
|
||||
selected.length === 0
|
||||
? i18n.translate('expressionXY.tooltipActions.emptyFilterSelection', {
|
||||
defaultMessage: 'Select at least one series to filter',
|
||||
})
|
||||
: i18n.translate('expressionXY.tooltipActions.filterValues', {
|
||||
defaultMessage: 'Filter {seriesNumber} series',
|
||||
values: { seriesNumber: selected.length },
|
||||
}),
|
||||
onSelect: filterSelectedTooltipValues,
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
customTooltip={
|
||||
args.detailedTooltip
|
||||
? ({ header, values }) => (
|
||||
<CustomTooltip
|
||||
header={header}
|
||||
values={values}
|
||||
titles={titles}
|
||||
fieldFormats={fieldFormats}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={formattedDatatables}
|
||||
splitAccessors={{
|
||||
splitColumnAccessor: splitColumnId,
|
||||
splitRowAccessor: splitRowId,
|
||||
}}
|
||||
layers={dataLayers}
|
||||
xDomain={isTimeViz ? rawXDomain : undefined}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
type={args.showTooltip ? TooltipType.VerticalCursor : TooltipType.None}
|
||||
/>
|
||||
<Settings
|
||||
noResults={
|
||||
<EmptyPlaceholder
|
||||
|
@ -789,37 +912,6 @@ export function XYChart({
|
|||
markSizeRatio: args.markSizeRatio,
|
||||
}}
|
||||
baseTheme={chartBaseTheme}
|
||||
tooltip={{
|
||||
boundary: document.getElementById('app-fixed-viewport') ?? undefined,
|
||||
headerFormatter: !args.detailedTooltip
|
||||
? ({ value }) => (
|
||||
<TooltipHeader
|
||||
value={value}
|
||||
formatter={safeXAccessorLabelRenderer}
|
||||
xDomain={rawXDomain}
|
||||
/>
|
||||
)
|
||||
: undefined,
|
||||
customTooltip: args.detailedTooltip
|
||||
? ({ header, values }) => (
|
||||
<Tooltip
|
||||
header={header}
|
||||
values={values}
|
||||
titles={titles}
|
||||
fieldFormats={fieldFormats}
|
||||
formatFactory={formatFactory}
|
||||
formattedDatatables={formattedDatatables}
|
||||
splitAccessors={{
|
||||
splitColumnAccessor: splitColumnId,
|
||||
splitRowAccessor: splitRowId,
|
||||
}}
|
||||
layers={dataLayers}
|
||||
xDomain={isTimeViz ? rawXDomain : undefined}
|
||||
/>
|
||||
)
|
||||
: undefined,
|
||||
type: args.showTooltip ? TooltipType.VerticalCursor : TooltipType.None,
|
||||
}}
|
||||
allowBrushingLastHistogramBin={isTimeViz}
|
||||
rotation={shouldRotate ? 90 : 0}
|
||||
xDomain={xDomain}
|
||||
|
|
|
@ -31,7 +31,12 @@ import { extractContainerType, extractVisualizationType } from '@kbn/chart-expre
|
|||
import type { getDataLayers } from '../helpers';
|
||||
import { LayerTypes, SeriesTypes } from '../../common/constants';
|
||||
import type { CommonXYDataLayerConfig, XYChartProps } from '../../common';
|
||||
import type { BrushEvent, FilterEvent, GetCompatibleCellValueActions } from '../types';
|
||||
import type {
|
||||
BrushEvent,
|
||||
FilterEvent,
|
||||
GetCompatibleCellValueActions,
|
||||
MultiFilterEvent,
|
||||
} from '../types';
|
||||
|
||||
export type GetStartDepsFn = () => Promise<{
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -207,6 +212,9 @@ export const getXyChartRenderer = ({
|
|||
const onSelectRange = (data: BrushEvent['data']) => {
|
||||
handlers.event({ name: 'brush', data });
|
||||
};
|
||||
const onClickMultiValue = (data: MultiFilterEvent['data']) => {
|
||||
handlers.event({ name: 'multiFilter', data });
|
||||
};
|
||||
|
||||
const layerCellValueActions = await getLayerCellValueActions(
|
||||
getDataLayers(config.args.layers),
|
||||
|
@ -260,6 +268,7 @@ export const getXyChartRenderer = ({
|
|||
minInterval={calculateMinInterval(deps.data.datatableUtilities, config)}
|
||||
interactive={handlers.isInteractive()}
|
||||
onClickValue={onClickValue}
|
||||
onClickMultiValue={onClickMultiValue}
|
||||
layerCellValueActions={layerCellValueActions}
|
||||
onSelectRange={onSelectRange}
|
||||
renderMode={handlers.getRenderMode()}
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
CellValueContext,
|
||||
RangeSelectContext,
|
||||
ValueClickContext,
|
||||
MultiValueClickContext,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { ExpressionsServiceStart, ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
|
||||
|
@ -37,6 +38,11 @@ export interface FilterEvent {
|
|||
data: ValueClickContext['data'];
|
||||
}
|
||||
|
||||
export interface MultiFilterEvent {
|
||||
name: 'multiFilter';
|
||||
data: MultiValueClickContext['data'];
|
||||
}
|
||||
|
||||
export interface BrushEvent {
|
||||
name: 'brush';
|
||||
data: RangeSelectContext['data'];
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
// TODO: https://github.com/elastic/kibana/issues/110891
|
||||
/* eslint-disable @kbn/eslint/no_export_all */
|
||||
|
||||
import { RangeSelectContext, ValueClickContext } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
RangeSelectContext,
|
||||
ValueClickContext,
|
||||
MultiValueClickContext,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { ChartsPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new ChartsPlugin();
|
||||
|
@ -30,6 +34,11 @@ export interface BrushTriggerEvent {
|
|||
data: RangeSelectContext['data'];
|
||||
}
|
||||
|
||||
export interface MultiClickTriggerEvent {
|
||||
name: 'multiFilter';
|
||||
data: MultiValueClickContext['data'];
|
||||
}
|
||||
|
||||
export type {
|
||||
CustomPaletteArguments,
|
||||
CustomPaletteState,
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { dataPluginMock } from '../../mocks';
|
||||
import { setIndexPatterns, setSearchService } from '../../services';
|
||||
import { createFiltersFromMultiValueClickAction } from './create_filters_from_multi_value_click';
|
||||
import { FieldFormatsGetConfigFn, BytesFormat } from '@kbn/field-formats-plugin/common';
|
||||
|
||||
const mockField = {
|
||||
name: 'bytes',
|
||||
filterable: true,
|
||||
};
|
||||
|
||||
describe('createFiltersFromMultiValueClickAction', () => {
|
||||
let dataPoints: Parameters<typeof createFiltersFromMultiValueClickAction>[0]['data'];
|
||||
|
||||
beforeEach(() => {
|
||||
dataPoints = {
|
||||
table: {
|
||||
columns: [
|
||||
{
|
||||
name: 'test',
|
||||
id: '1-1',
|
||||
meta: {
|
||||
type: 'date',
|
||||
source: 'esaggs',
|
||||
sourceParams: {
|
||||
indexPatternId: 'logstash-*',
|
||||
type: 'histogram',
|
||||
params: {
|
||||
field: 'bytes',
|
||||
interval: 30,
|
||||
otherBucket: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
'1-1': '2048',
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
source: 'dataview-1',
|
||||
type: 'esaggs',
|
||||
},
|
||||
},
|
||||
column: 0,
|
||||
value: ['2048'],
|
||||
};
|
||||
|
||||
const dataStart = dataPluginMock.createStartContract();
|
||||
setSearchService(dataStart.search);
|
||||
setIndexPatterns({
|
||||
...dataStart.indexPatterns,
|
||||
get: async () => ({
|
||||
id: 'logstash-*',
|
||||
fields: {
|
||||
getByName: () => mockField,
|
||||
filter: () => [mockField],
|
||||
},
|
||||
getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn),
|
||||
}),
|
||||
} as unknown as DataViewsContract);
|
||||
});
|
||||
|
||||
test('ignores event when value for rows is not provided', async () => {
|
||||
dataPoints.table.rows[0]['1-1'] = null;
|
||||
const filters = await createFiltersFromMultiValueClickAction({ data: dataPoints });
|
||||
|
||||
expect(filters).toBeUndefined();
|
||||
});
|
||||
|
||||
test('ignores event when dataview id is not provided', async () => {
|
||||
dataPoints.table.meta = undefined;
|
||||
const filters = await createFiltersFromMultiValueClickAction({ data: dataPoints });
|
||||
|
||||
expect(filters).toBeUndefined();
|
||||
});
|
||||
|
||||
test('handles an event when aggregations type is a terms', async () => {
|
||||
(dataPoints.table.columns[0].meta.sourceParams as any).type = 'terms';
|
||||
const filters = await createFiltersFromMultiValueClickAction({ data: dataPoints });
|
||||
|
||||
expect(filters?.query?.match_phrase?.bytes).toEqual('2048');
|
||||
});
|
||||
|
||||
test('handles an event when aggregations type is not terms', async () => {
|
||||
const filters = await createFiltersFromMultiValueClickAction({ data: dataPoints });
|
||||
|
||||
expect(filters?.query?.range.bytes.gte).toEqual(2048);
|
||||
expect(filters?.query?.range.bytes.lt).toEqual(2078);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { buildCombinedFilter, Filter, toggleFilterNegated, BooleanRelation } from '@kbn/es-query';
|
||||
import { createFilter } from './create_filters_from_value_click';
|
||||
import type { MultiValueClickContext } from '../multi_value_click_action';
|
||||
|
||||
type MultiValueClickDataContext = MultiValueClickContext['data'];
|
||||
|
||||
/** @public */
|
||||
export const createFiltersFromMultiValueClickAction = async ({
|
||||
data,
|
||||
negate,
|
||||
}: MultiValueClickDataContext) => {
|
||||
const { table, column, value } = data;
|
||||
const dataViewId = table?.meta?.source;
|
||||
if (!dataViewId) return;
|
||||
|
||||
const columnId = table.columns[column].id;
|
||||
|
||||
const filters = (
|
||||
await Promise.all(
|
||||
value.map(async (v) => {
|
||||
return (
|
||||
await createFilter(
|
||||
table,
|
||||
column,
|
||||
table.rows.findIndex((r) => r[columnId] === v)
|
||||
)
|
||||
)?.[0];
|
||||
})
|
||||
)
|
||||
).filter(Boolean) as Filter[];
|
||||
if (filters.length === 0) return;
|
||||
// no need for combined filter in case of one filter
|
||||
if (filters.length === 1) {
|
||||
if (filters[0] && negate) {
|
||||
return toggleFilterNegated(filters[0]);
|
||||
}
|
||||
return filters[0];
|
||||
}
|
||||
const filtersHaveAlias = filters.every((f) => f.meta.alias);
|
||||
let alias = '';
|
||||
if (filtersHaveAlias) {
|
||||
filters.forEach((f, i) => {
|
||||
if (i === filters.length - 1) {
|
||||
alias += `${f.meta.alias}`;
|
||||
} else {
|
||||
alias += `${f.meta.alias} ${BooleanRelation.OR} `;
|
||||
}
|
||||
});
|
||||
}
|
||||
let filter: Filter = buildCombinedFilter(
|
||||
BooleanRelation.OR,
|
||||
filters,
|
||||
{
|
||||
id: dataViewId,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
alias
|
||||
);
|
||||
if (filter && negate) {
|
||||
filter = toggleFilterNegated(filter);
|
||||
}
|
||||
|
||||
return filter;
|
||||
};
|
|
@ -71,7 +71,7 @@ const getOtherBucketFilterTerms = (
|
|||
* @param {string} cellValue - value of the current cell
|
||||
* @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters()
|
||||
*/
|
||||
const createFilter = async (
|
||||
export const createFilter = async (
|
||||
table: Pick<Datatable, 'rows' | 'columns'>,
|
||||
columnIndex: number,
|
||||
rowIndex: number
|
||||
|
|
|
@ -8,5 +8,7 @@
|
|||
|
||||
export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click';
|
||||
export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select';
|
||||
export { createFiltersFromMultiValueClickAction } from './filters/create_filters_from_multi_value_click';
|
||||
export * from './select_range_action';
|
||||
export * from './value_click_action';
|
||||
export * from './multi_value_click_action';
|
||||
|
|
49
src/plugins/data/public/actions/multi_value_click_action.ts
Normal file
49
src/plugins/data/public/actions/multi_value_click_action.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
|
||||
import { FilterManager } from '../query';
|
||||
import { createFiltersFromMultiValueClickAction } from './filters/create_filters_from_multi_value_click';
|
||||
|
||||
export type MultiValueClickActionContext = MultiValueClickContext;
|
||||
export const ACTION_MULTI_VALUE_CLICK = 'ACTION_MULTI_VALUE_CLICK';
|
||||
|
||||
export interface MultiValueClickContext {
|
||||
// Need to make this unknown to prevent circular dependencies.
|
||||
// Apps using this property will need to cast to `IEmbeddable`.
|
||||
embeddable?: unknown;
|
||||
data: {
|
||||
data: {
|
||||
table: Pick<Datatable, 'rows' | 'columns' | 'meta'>;
|
||||
column: number;
|
||||
value: any[];
|
||||
};
|
||||
timeFieldName?: string;
|
||||
negate?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function createMultiValueClickActionDefinition(
|
||||
getStartServices: () => { filterManager: FilterManager }
|
||||
): UiActionsActionDefinition<MultiValueClickContext> {
|
||||
return {
|
||||
type: ACTION_MULTI_VALUE_CLICK,
|
||||
id: ACTION_MULTI_VALUE_CLICK,
|
||||
shouldAutoExecute: async () => true,
|
||||
isCompatible: async (context: MultiValueClickContext) => {
|
||||
const filters = await createFiltersFromMultiValueClickAction(context.data);
|
||||
return Boolean(filters);
|
||||
},
|
||||
execute: async (context: MultiValueClickActionContext) => {
|
||||
const filter = (await createFiltersFromMultiValueClickAction(context.data)) as Filter;
|
||||
getStartServices().filterManager.addFilters(filter);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -34,6 +34,7 @@ import {
|
|||
import {
|
||||
createFiltersFromValueClickAction,
|
||||
createFiltersFromRangeSelectAction,
|
||||
createMultiValueClickActionDefinition,
|
||||
createValueClickActionDefinition,
|
||||
createSelectRangeActionDefinition,
|
||||
} from './actions';
|
||||
|
@ -154,6 +155,13 @@ export class DataPublicPlugin
|
|||
}))
|
||||
);
|
||||
|
||||
uiActions.addTriggerAction(
|
||||
'MULTI_VALUE_CLICK_TRIGGER',
|
||||
createMultiValueClickActionDefinition(() => ({
|
||||
filterManager: query.filterManager,
|
||||
}))
|
||||
);
|
||||
|
||||
const datatableUtilities = new DatatableUtilitiesService(search.aggs, dataViews, fieldFormats);
|
||||
const dataServices = {
|
||||
actions: {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
contextMenuTrigger,
|
||||
multiValueClickTrigger,
|
||||
panelBadgeTrigger,
|
||||
panelNotificationTrigger,
|
||||
selectRangeTrigger,
|
||||
|
@ -26,5 +27,6 @@ export const bootstrap = (uiActions: UiActionsSetup) => {
|
|||
uiActions.registerTrigger(panelNotificationTrigger);
|
||||
uiActions.registerTrigger(selectRangeTrigger);
|
||||
uiActions.registerTrigger(valueClickTrigger);
|
||||
uiActions.registerTrigger(multiValueClickTrigger);
|
||||
uiActions.registerTrigger(cellValueTrigger);
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ export type {
|
|||
EmbeddableInstanceConfiguration,
|
||||
EmbeddableOutput,
|
||||
ValueClickContext,
|
||||
MultiValueClickContext,
|
||||
CellValueContext,
|
||||
RangeSelectContext,
|
||||
IContainer,
|
||||
|
@ -69,6 +70,7 @@ export {
|
|||
PanelNotFoundError,
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
MULTI_VALUE_CLICK_TRIGGER,
|
||||
CELL_VALUE_TRIGGER,
|
||||
cellValueTrigger,
|
||||
ViewMode,
|
||||
|
@ -78,6 +80,7 @@ export {
|
|||
isSavedObjectEmbeddableInput,
|
||||
isRangeSelectTriggerContext,
|
||||
isValueClickTriggerContext,
|
||||
isMultiValueClickTriggerContext,
|
||||
isRowClickTriggerContext,
|
||||
isContextMenuTriggerContext,
|
||||
EmbeddableStateTransfer,
|
||||
|
|
|
@ -29,6 +29,19 @@ export interface ValueClickContext<T extends IEmbeddable = IEmbeddable> {
|
|||
};
|
||||
}
|
||||
|
||||
export interface MultiValueClickContext<T extends IEmbeddable = IEmbeddable> {
|
||||
embeddable?: T;
|
||||
data: {
|
||||
data: {
|
||||
table: Pick<Datatable, 'rows' | 'columns'>;
|
||||
column: number;
|
||||
value: any[];
|
||||
};
|
||||
timeFieldName?: string;
|
||||
negate?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CellValueContext<T extends IEmbeddable = IEmbeddable> {
|
||||
embeddable: T;
|
||||
data: Array<{
|
||||
|
@ -50,6 +63,7 @@ export interface RangeSelectContext<T extends IEmbeddable = IEmbeddable> {
|
|||
|
||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
|
||||
| ValueClickContext<T>
|
||||
| MultiValueClickContext<T>
|
||||
| RangeSelectContext<T>
|
||||
| RowClickContext;
|
||||
|
||||
|
@ -108,6 +122,17 @@ export const valueClickTrigger: Trigger = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const MULTI_VALUE_CLICK_TRIGGER = 'MULTI_VALUE_CLICK_TRIGGER';
|
||||
export const multiValueClickTrigger: Trigger = {
|
||||
id: MULTI_VALUE_CLICK_TRIGGER,
|
||||
title: i18n.translate('embeddableApi.multiValueClickTrigger.title', {
|
||||
defaultMessage: 'Multi click',
|
||||
}),
|
||||
description: i18n.translate('embeddableApi.multiValueClickTrigger.description', {
|
||||
defaultMessage: 'Selecting multiple values of a single dimension on the visualization',
|
||||
}),
|
||||
};
|
||||
|
||||
export const CELL_VALUE_TRIGGER = 'CELL_VALUE_TRIGGER';
|
||||
export const cellValueTrigger: Trigger = {
|
||||
id: CELL_VALUE_TRIGGER,
|
||||
|
@ -123,6 +148,11 @@ export const isValueClickTriggerContext = (
|
|||
context: ChartActionContext
|
||||
): context is ValueClickContext => context.data && 'data' in context.data;
|
||||
|
||||
export const isMultiValueClickTriggerContext = (
|
||||
context: ChartActionContext
|
||||
): context is MultiValueClickContext =>
|
||||
context.data && 'data' in context.data && !Array.isArray(context.data.data);
|
||||
|
||||
export const isRangeSelectTriggerContext = (
|
||||
context: ChartActionContext
|
||||
): context is RangeSelectContext => context.data && 'range' in context.data;
|
||||
|
|
|
@ -288,13 +288,17 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private hasCombinedFilterCustomType(filters: Filter[]) {
|
||||
return filters.some((filter) => filter.meta.type === 'custom');
|
||||
}
|
||||
|
||||
private renderFiltersBuilderEditor() {
|
||||
const { selectedDataView, localFilter } = this.state;
|
||||
const flattenedFilters = flattenFilters([localFilter]);
|
||||
|
||||
const shouldShowPreview =
|
||||
selectedDataView &&
|
||||
(flattenedFilters.length > 1 ||
|
||||
((flattenedFilters.length > 1 && !this.hasCombinedFilterCustomType(flattenedFilters)) ||
|
||||
(flattenedFilters.length === 1 &&
|
||||
isFilterValid(
|
||||
selectedDataView,
|
||||
|
@ -386,7 +390,10 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
|||
};
|
||||
|
||||
private isUnknownFilterType() {
|
||||
const { type } = this.props.filter.meta;
|
||||
const { type, params } = this.props.filter.meta;
|
||||
if (params && type === 'combined') {
|
||||
return this.hasCombinedFilterCustomType(params);
|
||||
}
|
||||
return !!type && !['phrase', 'phrases', 'range', 'exists', 'combined'].includes(type);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,17 @@
|
|||
|
||||
import { ROW_CLICK_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
|
||||
import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
SELECT_RANGE_TRIGGER,
|
||||
VALUE_CLICK_TRIGGER,
|
||||
MULTI_VALUE_CLICK_TRIGGER,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
|
||||
export interface VisEventToTrigger {
|
||||
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
|
||||
['brush']: typeof SELECT_RANGE_TRIGGER;
|
||||
['filter']: typeof VALUE_CLICK_TRIGGER;
|
||||
['multiFilter']: typeof MULTI_VALUE_CLICK_TRIGGER;
|
||||
['tableRowContextMenuClick']: typeof ROW_CLICK_TRIGGER;
|
||||
}
|
||||
|
||||
|
@ -21,5 +26,6 @@ export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
|
|||
applyFilter: APPLY_FILTER_TRIGGER,
|
||||
brush: SELECT_RANGE_TRIGGER,
|
||||
filter: VALUE_CLICK_TRIGGER,
|
||||
multiFilter: MULTI_VALUE_CLICK_TRIGGER,
|
||||
tableRowContextMenuClick: ROW_CLICK_TRIGGER,
|
||||
};
|
||||
|
|
|
@ -502,6 +502,40 @@ describe('workspace_panel', () => {
|
|||
expect(trigger.exec).toHaveBeenCalledWith({ data: { ...eventData, timeFieldName: undefined } });
|
||||
});
|
||||
|
||||
it('should execute a multi value click trigger on expression event', async () => {
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
mockDatasource.toExpression.mockReturnValue('datasource');
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const props = defaultProps;
|
||||
|
||||
const mounted = await mountWithProvider(
|
||||
<WorkspacePanel
|
||||
{...props}
|
||||
datasourceMap={{
|
||||
testDatasource: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
visualizationMap={{
|
||||
testVis: { ...mockVisualization, toExpression: () => 'testVis' },
|
||||
}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
plugins={{ ...props.plugins, uiActions: uiActionsMock }}
|
||||
/>
|
||||
);
|
||||
instance = mounted.instance;
|
||||
|
||||
const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!;
|
||||
|
||||
const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 };
|
||||
onEvent({ name: 'multiFilter', data: eventData });
|
||||
|
||||
expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.multiFilter);
|
||||
expect(trigger.exec).toHaveBeenCalledWith({ data: { ...eventData, timeFieldName: undefined } });
|
||||
});
|
||||
|
||||
it('should call getTriggerCompatibleActions on hasCompatibleActions call from within renderer', async () => {
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
FramePublicAPI,
|
||||
isLensBrushEvent,
|
||||
isLensFilterEvent,
|
||||
isLensMultiFilterEvent,
|
||||
isLensEditEvent,
|
||||
VisualizationMap,
|
||||
DatasourceMap,
|
||||
|
@ -398,7 +399,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
},
|
||||
});
|
||||
}
|
||||
if (isLensFilterEvent(event)) {
|
||||
if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) {
|
||||
plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
||||
data: {
|
||||
...event.data,
|
||||
|
@ -424,7 +425,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
// ui actions not available, not handling event...
|
||||
return false;
|
||||
}
|
||||
if (!isLensFilterEvent(event)) {
|
||||
if (!isLensFilterEvent(event) && !isLensMultiFilterEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -67,7 +67,12 @@ import type {
|
|||
ThemeServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { BrushTriggerEvent, ClickTriggerEvent, Warnings } from '@kbn/charts-plugin/public';
|
||||
import {
|
||||
BrushTriggerEvent,
|
||||
ClickTriggerEvent,
|
||||
Warnings,
|
||||
MultiClickTriggerEvent,
|
||||
} from '@kbn/charts-plugin/public';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
||||
import { Document } from '../persistence';
|
||||
|
@ -75,6 +80,7 @@ import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'
|
|||
import {
|
||||
isLensBrushEvent,
|
||||
isLensFilterEvent,
|
||||
isLensMultiFilterEvent,
|
||||
isLensEditEvent,
|
||||
isLensTableRowContextMenuClickEvent,
|
||||
LensTableRowContextMenuEvent,
|
||||
|
@ -133,7 +139,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
|
|||
noPadding?: boolean;
|
||||
onBrushEnd?: (data: BrushTriggerEvent['data']) => void;
|
||||
onLoad?: (isLoading: boolean, adapters?: Partial<DefaultInspectorAdapters>) => void;
|
||||
onFilter?: (data: ClickTriggerEvent['data']) => void;
|
||||
onFilter?: (data: ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']) => void;
|
||||
onTableRowClick?: (data: LensTableRowContextMenuEvent['data']) => void;
|
||||
}
|
||||
|
||||
|
@ -896,7 +902,11 @@ export class Embeddable
|
|||
private readonly hasCompatibleActions = async (
|
||||
event: ExpressionRendererEvent
|
||||
): Promise<boolean> => {
|
||||
if (isLensTableRowContextMenuClickEvent(event) || isLensFilterEvent(event)) {
|
||||
if (
|
||||
isLensTableRowContextMenuClickEvent(event) ||
|
||||
isLensMultiFilterEvent(event) ||
|
||||
isLensFilterEvent(event)
|
||||
) {
|
||||
const { getTriggerCompatibleActions } = this.deps;
|
||||
if (!getTriggerCompatibleActions) {
|
||||
return false;
|
||||
|
@ -992,7 +1002,7 @@ export class Embeddable
|
|||
this.input.onBrushEnd(event.data);
|
||||
}
|
||||
}
|
||||
if (isLensFilterEvent(event)) {
|
||||
if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) {
|
||||
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
||||
data: {
|
||||
...event.data,
|
||||
|
|
|
@ -24,7 +24,11 @@ import type {
|
|||
RowClickContext,
|
||||
VisualizeFieldContext,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import type { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public';
|
||||
import type {
|
||||
ClickTriggerEvent,
|
||||
BrushTriggerEvent,
|
||||
MultiClickTriggerEvent,
|
||||
} from '@kbn/charts-plugin/public';
|
||||
import type { IndexPatternAggRestrictions } from '@kbn/data-plugin/public';
|
||||
import type { FieldSpec, DataViewSpec, DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
|
@ -1340,6 +1344,12 @@ export function isLensFilterEvent(event: ExpressionRendererEvent): event is Clic
|
|||
return event.name === 'filter';
|
||||
}
|
||||
|
||||
export function isLensMultiFilterEvent(
|
||||
event: ExpressionRendererEvent
|
||||
): event is MultiClickTriggerEvent {
|
||||
return event.name === 'multiFilter';
|
||||
}
|
||||
|
||||
export function isLensBrushEvent(event: ExpressionRendererEvent): event is BrushTriggerEvent {
|
||||
return event.name === 'brush';
|
||||
}
|
||||
|
|
|
@ -13,7 +13,11 @@ import type { TimefilterContract } from '@kbn/data-plugin/public';
|
|||
import type { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public';
|
||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
||||
import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public';
|
||||
import {
|
||||
BrushTriggerEvent,
|
||||
ClickTriggerEvent,
|
||||
MultiClickTriggerEvent,
|
||||
} from '@kbn/charts-plugin/public';
|
||||
import { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||
import { ISearchStart } from '@kbn/data-plugin/public';
|
||||
import type { Document } from './persistence/saved_object_store';
|
||||
|
@ -209,7 +213,7 @@ export function getRemoveOperation(
|
|||
|
||||
export function inferTimeField(
|
||||
datatableUtilities: DatatableUtilitiesService,
|
||||
context: BrushTriggerEvent['data'] | ClickTriggerEvent['data']
|
||||
context: BrushTriggerEvent['data'] | ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']
|
||||
) {
|
||||
const tablesAndColumns =
|
||||
'table' in context
|
||||
|
@ -218,17 +222,19 @@ export function inferTimeField(
|
|||
? context.data
|
||||
: // if it's a negated filter, never respect bound time field
|
||||
[];
|
||||
return tablesAndColumns
|
||||
.map(({ table, column }) => {
|
||||
const tableColumn = table.columns[column];
|
||||
const hasTimeRange = Boolean(
|
||||
tableColumn && datatableUtilities.getDateHistogramMeta(tableColumn)?.timeRange
|
||||
);
|
||||
if (hasTimeRange) {
|
||||
return tableColumn.meta.field;
|
||||
}
|
||||
})
|
||||
.find(Boolean);
|
||||
return !Array.isArray(tablesAndColumns)
|
||||
? [tablesAndColumns]
|
||||
: tablesAndColumns
|
||||
.map(({ table, column }) => {
|
||||
const tableColumn = table.columns[column];
|
||||
const hasTimeRange = Boolean(
|
||||
tableColumn && datatableUtilities.getDateHistogramMeta(tableColumn)?.timeRange
|
||||
);
|
||||
if (hasTimeRange) {
|
||||
return tableColumn.meta.field;
|
||||
}
|
||||
})
|
||||
.find(Boolean);
|
||||
}
|
||||
|
||||
export function renewIDs<T = unknown>(
|
||||
|
|
|
@ -34,6 +34,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await browser.getActions().move({ x, y, origin: el._webElement }).click().perform();
|
||||
}
|
||||
|
||||
async function rightClickInChart(x: number, y: number) {
|
||||
const el = await elasticChart.getCanvas();
|
||||
await browser.getActions().move({ x, y, origin: el._webElement }).contextClick().perform();
|
||||
}
|
||||
|
||||
describe('lens dashboard tests', () => {
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
|
@ -90,6 +95,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(hasIpFilter).to.be(true);
|
||||
});
|
||||
|
||||
it('should be able to add filters by right clicking in XYChart', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await dashboardAddPanel.clickOpenAddPanel();
|
||||
await dashboardAddPanel.filterEmbeddableNames('lnsXYvis');
|
||||
await find.clickByButtonText('lnsXYvis');
|
||||
await dashboardAddPanel.closeAddPanel();
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await retry.try(async () => {
|
||||
// show the tooltip actions
|
||||
await rightClickInChart(30, 5); // hardcoded position of bar, depends heavy on data and charts implementation
|
||||
await (await find.byCssSelector('.echTooltipActions__action')).click();
|
||||
const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248');
|
||||
expect(hasIpFilter).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Requires xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled
|
||||
// setting set in kibana.yml to test (not enabled by default)
|
||||
it('should hide old "explore underlying data" action', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue