mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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(
|
export function buildCombinedFilter(
|
||||||
relation: BooleanRelation,
|
relation: BooleanRelation,
|
||||||
filters: Filter[],
|
filters: Filter[],
|
||||||
indexPattern: DataViewBase,
|
indexPattern: Pick<DataViewBase, 'id'>,
|
||||||
disabled: FilterMeta['disabled'] = false,
|
disabled: FilterMeta['disabled'] = false,
|
||||||
negate: FilterMeta['negate'] = false,
|
negate: FilterMeta['negate'] = false,
|
||||||
alias?: FilterMeta['alias'],
|
alias?: FilterMeta['alias'],
|
||||||
|
|
|
@ -557,6 +557,19 @@ exports[`XYChart component it renders area 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -608,14 +621,6 @@ exports[`XYChart component it renders area 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -1539,6 +1544,19 @@ exports[`XYChart component it renders bar 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -1590,14 +1608,6 @@ exports[`XYChart component it renders bar 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -2521,6 +2531,19 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -2572,14 +2595,6 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -3503,6 +3518,19 @@ exports[`XYChart component it renders line 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -3554,14 +3582,6 @@ exports[`XYChart component it renders line 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -4485,6 +4505,19 @@ exports[`XYChart component it renders stacked area 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -4536,14 +4569,6 @@ exports[`XYChart component it renders stacked area 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -5467,6 +5492,19 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -5518,14 +5556,6 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -6449,6 +6479,19 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -6500,14 +6543,6 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -7461,6 +7496,19 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -7512,14 +7560,6 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -8681,6 +8721,19 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -8732,14 +8785,6 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
@ -9894,6 +9939,19 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
||||||
<Chart
|
<Chart
|
||||||
renderer="canvas"
|
renderer="canvas"
|
||||||
>
|
>
|
||||||
|
<Tooltip
|
||||||
|
actions={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": [Function],
|
||||||
|
"label": [Function],
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headerFormatter={[Function]}
|
||||||
|
type="vertical"
|
||||||
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
allowBrushingLastHistogramBin={false}
|
allowBrushingLastHistogramBin={false}
|
||||||
ariaUseDefaultSummary={true}
|
ariaUseDefaultSummary={true}
|
||||||
|
@ -9945,14 +10003,6 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
||||||
"markSizeRatio": undefined,
|
"markSizeRatio": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tooltip={
|
|
||||||
Object {
|
|
||||||
"boundary": undefined,
|
|
||||||
"customTooltip": undefined,
|
|
||||||
"headerFormatter": [Function],
|
|
||||||
"type": "vertical",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<XYCurrentTime
|
<XYCurrentTime
|
||||||
enabled={false}
|
enabled={false}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
SmallMultiples,
|
SmallMultiples,
|
||||||
VerticalAlignment,
|
VerticalAlignment,
|
||||||
XYChartSeriesIdentifier,
|
XYChartSeriesIdentifier,
|
||||||
|
Tooltip,
|
||||||
} from '@elastic/charts';
|
} from '@elastic/charts';
|
||||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||||
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||||
|
@ -61,6 +62,7 @@ import { LegendSize } from '@kbn/visualizations-plugin/common';
|
||||||
import type { LayerCellValueActions } from '../types';
|
import type { LayerCellValueActions } from '../types';
|
||||||
|
|
||||||
const onClickValue = jest.fn();
|
const onClickValue = jest.fn();
|
||||||
|
const onClickMultiValue = jest.fn();
|
||||||
const layerCellValueActions: LayerCellValueActions = [];
|
const layerCellValueActions: LayerCellValueActions = [];
|
||||||
const onSelectRange = jest.fn();
|
const onSelectRange = jest.fn();
|
||||||
|
|
||||||
|
@ -116,6 +118,7 @@ describe('XYChart component', () => {
|
||||||
paletteService,
|
paletteService,
|
||||||
minInterval: 50,
|
minInterval: 50,
|
||||||
onClickValue,
|
onClickValue,
|
||||||
|
onClickMultiValue,
|
||||||
layerCellValueActions,
|
layerCellValueActions,
|
||||||
onSelectRange,
|
onSelectRange,
|
||||||
syncColors: false,
|
syncColors: false,
|
||||||
|
@ -1102,6 +1105,126 @@ describe('XYChart component', () => {
|
||||||
expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true);
|
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', () => {
|
test('onElementClick returns correct context data', () => {
|
||||||
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} };
|
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} };
|
||||||
const series = {
|
const series = {
|
||||||
|
@ -3284,14 +3407,11 @@ describe('XYChart component', () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const settings = component.find(Settings);
|
const tooltip = component.find(Tooltip);
|
||||||
const tooltip = settings.prop('tooltip');
|
const customTooltip = tooltip.prop('customTooltip');
|
||||||
expect(tooltip).toEqual(
|
expect(customTooltip).not.toBeUndefined();
|
||||||
expect.objectContaining({
|
const headerFormatter = tooltip.prop('headerFormatter');
|
||||||
headerFormatter: undefined,
|
expect(headerFormatter).toBeUndefined();
|
||||||
customTooltip: expect.any(Function),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render default tooltip, if detailed tooltip is hidden', () => {
|
it('should render default tooltip, if detailed tooltip is hidden', () => {
|
||||||
|
@ -3306,14 +3426,11 @@ describe('XYChart component', () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const settings = component.find(Settings);
|
const tooltip = component.find(Tooltip);
|
||||||
const tooltip = settings.prop('tooltip');
|
const customTooltip = tooltip.prop('customTooltip');
|
||||||
expect(tooltip).toEqual(
|
expect(customTooltip).toBeUndefined();
|
||||||
expect.objectContaining({
|
const headerFormatter = tooltip.prop('headerFormatter');
|
||||||
headerFormatter: expect.any(Function),
|
expect(headerFormatter).not.toBeUndefined();
|
||||||
customTooltip: undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
Settings,
|
Settings,
|
||||||
|
@ -27,6 +28,9 @@ import {
|
||||||
Placement,
|
Placement,
|
||||||
Direction,
|
Direction,
|
||||||
XYChartElementEvent,
|
XYChartElementEvent,
|
||||||
|
Tooltip,
|
||||||
|
XYChartSeriesIdentifier,
|
||||||
|
TooltipValue,
|
||||||
} from '@elastic/charts';
|
} from '@elastic/charts';
|
||||||
import { partition } from 'lodash';
|
import { partition } from 'lodash';
|
||||||
import { IconType } from '@elastic/eui';
|
import { IconType } from '@elastic/eui';
|
||||||
|
@ -47,7 +51,13 @@ import {
|
||||||
LegendSizeToPixels,
|
LegendSizeToPixels,
|
||||||
} from '@kbn/visualizations-plugin/common/constants';
|
} from '@kbn/visualizations-plugin/common/constants';
|
||||||
import { PersistedState } from '@kbn/visualizations-plugin/public';
|
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 { isTimeChart } from '../../common/helpers';
|
||||||
import type {
|
import type {
|
||||||
CommonXYDataLayerConfig,
|
CommonXYDataLayerConfig,
|
||||||
|
@ -94,7 +104,7 @@ import {
|
||||||
} from './annotations';
|
} from './annotations';
|
||||||
import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants';
|
import { AxisExtentModes, SeriesTypes, ValueLabelModes, XScaleTypes } from '../../common/constants';
|
||||||
import { DataLayers } from './data_layers';
|
import { DataLayers } from './data_layers';
|
||||||
import { Tooltip } from './tooltip';
|
import { Tooltip as CustomTooltip } from './tooltip';
|
||||||
import { XYCurrentTime } from './xy_current_time';
|
import { XYCurrentTime } from './xy_current_time';
|
||||||
|
|
||||||
import './xy_chart.scss';
|
import './xy_chart.scss';
|
||||||
|
@ -121,6 +131,7 @@ export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
|
||||||
minInterval: number | undefined;
|
minInterval: number | undefined;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
onClickValue: (data: FilterEvent['data']) => void;
|
onClickValue: (data: FilterEvent['data']) => void;
|
||||||
|
onClickMultiValue: (data: MultiFilterEvent['data']) => void;
|
||||||
layerCellValueActions: LayerCellValueActions;
|
layerCellValueActions: LayerCellValueActions;
|
||||||
onSelectRange: (data: BrushEvent['data']) => void;
|
onSelectRange: (data: BrushEvent['data']) => void;
|
||||||
renderMode: RenderMode;
|
renderMode: RenderMode;
|
||||||
|
@ -133,6 +144,10 @@ export type XYChartRenderProps = Omit<XYChartProps, 'canNavigateToLens'> & {
|
||||||
timeFormat: string;
|
timeFormat: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function nonNullable<T>(v: T): v is NonNullable<T> {
|
||||||
|
return v != null;
|
||||||
|
}
|
||||||
|
|
||||||
function getValueLabelsStyling(isHorizontal: boolean): {
|
function getValueLabelsStyling(isHorizontal: boolean): {
|
||||||
displayValue: RecursivePartial<DisplayValueStyle>;
|
displayValue: RecursivePartial<DisplayValueStyle>;
|
||||||
} {
|
} {
|
||||||
|
@ -173,6 +188,9 @@ function createSplitPoint(
|
||||||
table: Datatable
|
table: Datatable
|
||||||
) {
|
) {
|
||||||
const splitPointRowIndex = rows.findIndex((row) => {
|
const splitPointRowIndex = rows.findIndex((row) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.includes(row[accessor]);
|
||||||
|
}
|
||||||
return row[accessor] === value;
|
return row[accessor] === value;
|
||||||
});
|
});
|
||||||
if (splitPointRowIndex !== -1) {
|
if (splitPointRowIndex !== -1) {
|
||||||
|
@ -197,6 +215,7 @@ export function XYChart({
|
||||||
paletteService,
|
paletteService,
|
||||||
minInterval,
|
minInterval,
|
||||||
onClickValue,
|
onClickValue,
|
||||||
|
onClickMultiValue,
|
||||||
layerCellValueActions,
|
layerCellValueActions,
|
||||||
onSelectRange,
|
onSelectRange,
|
||||||
interactive = true,
|
interactive = true,
|
||||||
|
@ -534,6 +553,54 @@ export function XYChart({
|
||||||
valueLabels !== ValueLabelModes.HIDE &&
|
valueLabels !== ValueLabelModes.HIDE &&
|
||||||
getValueLabelsStyling(shouldRotate);
|
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]) => {
|
const clickHandler: ElementClickListener = ([elementEvent]) => {
|
||||||
// this cast is safe because we are rendering a cartesian chart
|
// this cast is safe because we are rendering a cartesian chart
|
||||||
const [xyGeometry, xySeries] = elementEvent as XYChartElementEvent;
|
const [xyGeometry, xySeries] = elementEvent as XYChartElementEvent;
|
||||||
|
@ -722,6 +789,8 @@ export function XYChart({
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
position: uiState ? 'absolute' : 'relative',
|
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 (
|
return (
|
||||||
<div css={chartContainerStyle}>
|
<div css={chartContainerStyle}>
|
||||||
|
@ -745,6 +814,60 @@ export function XYChart({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Chart ref={chartRef}>
|
<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
|
<Settings
|
||||||
noResults={
|
noResults={
|
||||||
<EmptyPlaceholder
|
<EmptyPlaceholder
|
||||||
|
@ -789,37 +912,6 @@ export function XYChart({
|
||||||
markSizeRatio: args.markSizeRatio,
|
markSizeRatio: args.markSizeRatio,
|
||||||
}}
|
}}
|
||||||
baseTheme={chartBaseTheme}
|
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}
|
allowBrushingLastHistogramBin={isTimeViz}
|
||||||
rotation={shouldRotate ? 90 : 0}
|
rotation={shouldRotate ? 90 : 0}
|
||||||
xDomain={xDomain}
|
xDomain={xDomain}
|
||||||
|
|
|
@ -31,7 +31,12 @@ import { extractContainerType, extractVisualizationType } from '@kbn/chart-expre
|
||||||
import type { getDataLayers } from '../helpers';
|
import type { getDataLayers } from '../helpers';
|
||||||
import { LayerTypes, SeriesTypes } from '../../common/constants';
|
import { LayerTypes, SeriesTypes } from '../../common/constants';
|
||||||
import type { CommonXYDataLayerConfig, XYChartProps } from '../../common';
|
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<{
|
export type GetStartDepsFn = () => Promise<{
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
|
@ -207,6 +212,9 @@ export const getXyChartRenderer = ({
|
||||||
const onSelectRange = (data: BrushEvent['data']) => {
|
const onSelectRange = (data: BrushEvent['data']) => {
|
||||||
handlers.event({ name: 'brush', data });
|
handlers.event({ name: 'brush', data });
|
||||||
};
|
};
|
||||||
|
const onClickMultiValue = (data: MultiFilterEvent['data']) => {
|
||||||
|
handlers.event({ name: 'multiFilter', data });
|
||||||
|
};
|
||||||
|
|
||||||
const layerCellValueActions = await getLayerCellValueActions(
|
const layerCellValueActions = await getLayerCellValueActions(
|
||||||
getDataLayers(config.args.layers),
|
getDataLayers(config.args.layers),
|
||||||
|
@ -260,6 +268,7 @@ export const getXyChartRenderer = ({
|
||||||
minInterval={calculateMinInterval(deps.data.datatableUtilities, config)}
|
minInterval={calculateMinInterval(deps.data.datatableUtilities, config)}
|
||||||
interactive={handlers.isInteractive()}
|
interactive={handlers.isInteractive()}
|
||||||
onClickValue={onClickValue}
|
onClickValue={onClickValue}
|
||||||
|
onClickMultiValue={onClickMultiValue}
|
||||||
layerCellValueActions={layerCellValueActions}
|
layerCellValueActions={layerCellValueActions}
|
||||||
onSelectRange={onSelectRange}
|
onSelectRange={onSelectRange}
|
||||||
renderMode={handlers.getRenderMode()}
|
renderMode={handlers.getRenderMode()}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
||||||
CellValueContext,
|
CellValueContext,
|
||||||
RangeSelectContext,
|
RangeSelectContext,
|
||||||
ValueClickContext,
|
ValueClickContext,
|
||||||
|
MultiValueClickContext,
|
||||||
} from '@kbn/embeddable-plugin/public';
|
} from '@kbn/embeddable-plugin/public';
|
||||||
import { ExpressionsServiceStart, ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
import { ExpressionsServiceStart, ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||||
|
|
||||||
|
@ -37,6 +38,11 @@ export interface FilterEvent {
|
||||||
data: ValueClickContext['data'];
|
data: ValueClickContext['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MultiFilterEvent {
|
||||||
|
name: 'multiFilter';
|
||||||
|
data: MultiValueClickContext['data'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface BrushEvent {
|
export interface BrushEvent {
|
||||||
name: 'brush';
|
name: 'brush';
|
||||||
data: RangeSelectContext['data'];
|
data: RangeSelectContext['data'];
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
// TODO: https://github.com/elastic/kibana/issues/110891
|
// TODO: https://github.com/elastic/kibana/issues/110891
|
||||||
/* eslint-disable @kbn/eslint/no_export_all */
|
/* 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';
|
import { ChartsPlugin } from './plugin';
|
||||||
|
|
||||||
export const plugin = () => new ChartsPlugin();
|
export const plugin = () => new ChartsPlugin();
|
||||||
|
@ -30,6 +34,11 @@ export interface BrushTriggerEvent {
|
||||||
data: RangeSelectContext['data'];
|
data: RangeSelectContext['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MultiClickTriggerEvent {
|
||||||
|
name: 'multiFilter';
|
||||||
|
data: MultiValueClickContext['data'];
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CustomPaletteArguments,
|
CustomPaletteArguments,
|
||||||
CustomPaletteState,
|
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
|
* @param {string} cellValue - value of the current cell
|
||||||
* @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters()
|
* @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters()
|
||||||
*/
|
*/
|
||||||
const createFilter = async (
|
export const createFilter = async (
|
||||||
table: Pick<Datatable, 'rows' | 'columns'>,
|
table: Pick<Datatable, 'rows' | 'columns'>,
|
||||||
columnIndex: number,
|
columnIndex: number,
|
||||||
rowIndex: number
|
rowIndex: number
|
||||||
|
|
|
@ -8,5 +8,7 @@
|
||||||
|
|
||||||
export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click';
|
export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click';
|
||||||
export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select';
|
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 './select_range_action';
|
||||||
export * from './value_click_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 {
|
import {
|
||||||
createFiltersFromValueClickAction,
|
createFiltersFromValueClickAction,
|
||||||
createFiltersFromRangeSelectAction,
|
createFiltersFromRangeSelectAction,
|
||||||
|
createMultiValueClickActionDefinition,
|
||||||
createValueClickActionDefinition,
|
createValueClickActionDefinition,
|
||||||
createSelectRangeActionDefinition,
|
createSelectRangeActionDefinition,
|
||||||
} from './actions';
|
} 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 datatableUtilities = new DatatableUtilitiesService(search.aggs, dataViews, fieldFormats);
|
||||||
const dataServices = {
|
const dataServices = {
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
import { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
||||||
import {
|
import {
|
||||||
contextMenuTrigger,
|
contextMenuTrigger,
|
||||||
|
multiValueClickTrigger,
|
||||||
panelBadgeTrigger,
|
panelBadgeTrigger,
|
||||||
panelNotificationTrigger,
|
panelNotificationTrigger,
|
||||||
selectRangeTrigger,
|
selectRangeTrigger,
|
||||||
|
@ -26,5 +27,6 @@ export const bootstrap = (uiActions: UiActionsSetup) => {
|
||||||
uiActions.registerTrigger(panelNotificationTrigger);
|
uiActions.registerTrigger(panelNotificationTrigger);
|
||||||
uiActions.registerTrigger(selectRangeTrigger);
|
uiActions.registerTrigger(selectRangeTrigger);
|
||||||
uiActions.registerTrigger(valueClickTrigger);
|
uiActions.registerTrigger(valueClickTrigger);
|
||||||
|
uiActions.registerTrigger(multiValueClickTrigger);
|
||||||
uiActions.registerTrigger(cellValueTrigger);
|
uiActions.registerTrigger(cellValueTrigger);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,7 @@ export type {
|
||||||
EmbeddableInstanceConfiguration,
|
EmbeddableInstanceConfiguration,
|
||||||
EmbeddableOutput,
|
EmbeddableOutput,
|
||||||
ValueClickContext,
|
ValueClickContext,
|
||||||
|
MultiValueClickContext,
|
||||||
CellValueContext,
|
CellValueContext,
|
||||||
RangeSelectContext,
|
RangeSelectContext,
|
||||||
IContainer,
|
IContainer,
|
||||||
|
@ -69,6 +70,7 @@ export {
|
||||||
PanelNotFoundError,
|
PanelNotFoundError,
|
||||||
SELECT_RANGE_TRIGGER,
|
SELECT_RANGE_TRIGGER,
|
||||||
VALUE_CLICK_TRIGGER,
|
VALUE_CLICK_TRIGGER,
|
||||||
|
MULTI_VALUE_CLICK_TRIGGER,
|
||||||
CELL_VALUE_TRIGGER,
|
CELL_VALUE_TRIGGER,
|
||||||
cellValueTrigger,
|
cellValueTrigger,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
|
@ -78,6 +80,7 @@ export {
|
||||||
isSavedObjectEmbeddableInput,
|
isSavedObjectEmbeddableInput,
|
||||||
isRangeSelectTriggerContext,
|
isRangeSelectTriggerContext,
|
||||||
isValueClickTriggerContext,
|
isValueClickTriggerContext,
|
||||||
|
isMultiValueClickTriggerContext,
|
||||||
isRowClickTriggerContext,
|
isRowClickTriggerContext,
|
||||||
isContextMenuTriggerContext,
|
isContextMenuTriggerContext,
|
||||||
EmbeddableStateTransfer,
|
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> {
|
export interface CellValueContext<T extends IEmbeddable = IEmbeddable> {
|
||||||
embeddable: T;
|
embeddable: T;
|
||||||
data: Array<{
|
data: Array<{
|
||||||
|
@ -50,6 +63,7 @@ export interface RangeSelectContext<T extends IEmbeddable = IEmbeddable> {
|
||||||
|
|
||||||
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
|
export type ChartActionContext<T extends IEmbeddable = IEmbeddable> =
|
||||||
| ValueClickContext<T>
|
| ValueClickContext<T>
|
||||||
|
| MultiValueClickContext<T>
|
||||||
| RangeSelectContext<T>
|
| RangeSelectContext<T>
|
||||||
| RowClickContext;
|
| 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 CELL_VALUE_TRIGGER = 'CELL_VALUE_TRIGGER';
|
||||||
export const cellValueTrigger: Trigger = {
|
export const cellValueTrigger: Trigger = {
|
||||||
id: CELL_VALUE_TRIGGER,
|
id: CELL_VALUE_TRIGGER,
|
||||||
|
@ -123,6 +148,11 @@ export const isValueClickTriggerContext = (
|
||||||
context: ChartActionContext
|
context: ChartActionContext
|
||||||
): context is ValueClickContext => context.data && 'data' in context.data;
|
): 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 = (
|
export const isRangeSelectTriggerContext = (
|
||||||
context: ChartActionContext
|
context: ChartActionContext
|
||||||
): context is RangeSelectContext => context.data && 'range' in context.data;
|
): 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() {
|
private renderFiltersBuilderEditor() {
|
||||||
const { selectedDataView, localFilter } = this.state;
|
const { selectedDataView, localFilter } = this.state;
|
||||||
const flattenedFilters = flattenFilters([localFilter]);
|
const flattenedFilters = flattenFilters([localFilter]);
|
||||||
|
|
||||||
const shouldShowPreview =
|
const shouldShowPreview =
|
||||||
selectedDataView &&
|
selectedDataView &&
|
||||||
(flattenedFilters.length > 1 ||
|
((flattenedFilters.length > 1 && !this.hasCombinedFilterCustomType(flattenedFilters)) ||
|
||||||
(flattenedFilters.length === 1 &&
|
(flattenedFilters.length === 1 &&
|
||||||
isFilterValid(
|
isFilterValid(
|
||||||
selectedDataView,
|
selectedDataView,
|
||||||
|
@ -386,7 +390,10 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private isUnknownFilterType() {
|
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);
|
return !!type && !['phrase', 'phrases', 'range', 'exists', 'combined'].includes(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,17 @@
|
||||||
|
|
||||||
import { ROW_CLICK_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
import { ROW_CLICK_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||||
import { APPLY_FILTER_TRIGGER } from '@kbn/data-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 {
|
export interface VisEventToTrigger {
|
||||||
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
|
['applyFilter']: typeof APPLY_FILTER_TRIGGER;
|
||||||
['brush']: typeof SELECT_RANGE_TRIGGER;
|
['brush']: typeof SELECT_RANGE_TRIGGER;
|
||||||
['filter']: typeof VALUE_CLICK_TRIGGER;
|
['filter']: typeof VALUE_CLICK_TRIGGER;
|
||||||
|
['multiFilter']: typeof MULTI_VALUE_CLICK_TRIGGER;
|
||||||
['tableRowContextMenuClick']: typeof ROW_CLICK_TRIGGER;
|
['tableRowContextMenuClick']: typeof ROW_CLICK_TRIGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,5 +26,6 @@ export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = {
|
||||||
applyFilter: APPLY_FILTER_TRIGGER,
|
applyFilter: APPLY_FILTER_TRIGGER,
|
||||||
brush: SELECT_RANGE_TRIGGER,
|
brush: SELECT_RANGE_TRIGGER,
|
||||||
filter: VALUE_CLICK_TRIGGER,
|
filter: VALUE_CLICK_TRIGGER,
|
||||||
|
multiFilter: MULTI_VALUE_CLICK_TRIGGER,
|
||||||
tableRowContextMenuClick: ROW_CLICK_TRIGGER,
|
tableRowContextMenuClick: ROW_CLICK_TRIGGER,
|
||||||
};
|
};
|
||||||
|
|
|
@ -502,6 +502,40 @@ describe('workspace_panel', () => {
|
||||||
expect(trigger.exec).toHaveBeenCalledWith({ data: { ...eventData, timeFieldName: undefined } });
|
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 () => {
|
it('should call getTriggerCompatibleActions on hasCompatibleActions call from within renderer', async () => {
|
||||||
const framePublicAPI = createMockFramePublicAPI();
|
const framePublicAPI = createMockFramePublicAPI();
|
||||||
framePublicAPI.datasourceLayers = {
|
framePublicAPI.datasourceLayers = {
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
FramePublicAPI,
|
FramePublicAPI,
|
||||||
isLensBrushEvent,
|
isLensBrushEvent,
|
||||||
isLensFilterEvent,
|
isLensFilterEvent,
|
||||||
|
isLensMultiFilterEvent,
|
||||||
isLensEditEvent,
|
isLensEditEvent,
|
||||||
VisualizationMap,
|
VisualizationMap,
|
||||||
DatasourceMap,
|
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({
|
plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
||||||
data: {
|
data: {
|
||||||
...event.data,
|
...event.data,
|
||||||
|
@ -424,7 +425,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
||||||
// ui actions not available, not handling event...
|
// ui actions not available, not handling event...
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isLensFilterEvent(event)) {
|
if (!isLensFilterEvent(event) && !isLensMultiFilterEvent(event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -67,7 +67,12 @@ import type {
|
||||||
ThemeServiceStart,
|
ThemeServiceStart,
|
||||||
} from '@kbn/core/public';
|
} from '@kbn/core/public';
|
||||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/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 { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||||
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
import { getExecutionContextEvents, trackUiCounterEvents } from '../lens_ui_telemetry';
|
||||||
import { Document } from '../persistence';
|
import { Document } from '../persistence';
|
||||||
|
@ -75,6 +80,7 @@ import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'
|
||||||
import {
|
import {
|
||||||
isLensBrushEvent,
|
isLensBrushEvent,
|
||||||
isLensFilterEvent,
|
isLensFilterEvent,
|
||||||
|
isLensMultiFilterEvent,
|
||||||
isLensEditEvent,
|
isLensEditEvent,
|
||||||
isLensTableRowContextMenuClickEvent,
|
isLensTableRowContextMenuClickEvent,
|
||||||
LensTableRowContextMenuEvent,
|
LensTableRowContextMenuEvent,
|
||||||
|
@ -133,7 +139,7 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
|
||||||
noPadding?: boolean;
|
noPadding?: boolean;
|
||||||
onBrushEnd?: (data: BrushTriggerEvent['data']) => void;
|
onBrushEnd?: (data: BrushTriggerEvent['data']) => void;
|
||||||
onLoad?: (isLoading: boolean, adapters?: Partial<DefaultInspectorAdapters>) => 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;
|
onTableRowClick?: (data: LensTableRowContextMenuEvent['data']) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -896,7 +902,11 @@ export class Embeddable
|
||||||
private readonly hasCompatibleActions = async (
|
private readonly hasCompatibleActions = async (
|
||||||
event: ExpressionRendererEvent
|
event: ExpressionRendererEvent
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (isLensTableRowContextMenuClickEvent(event) || isLensFilterEvent(event)) {
|
if (
|
||||||
|
isLensTableRowContextMenuClickEvent(event) ||
|
||||||
|
isLensMultiFilterEvent(event) ||
|
||||||
|
isLensFilterEvent(event)
|
||||||
|
) {
|
||||||
const { getTriggerCompatibleActions } = this.deps;
|
const { getTriggerCompatibleActions } = this.deps;
|
||||||
if (!getTriggerCompatibleActions) {
|
if (!getTriggerCompatibleActions) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -992,7 +1002,7 @@ export class Embeddable
|
||||||
this.input.onBrushEnd(event.data);
|
this.input.onBrushEnd(event.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isLensFilterEvent(event)) {
|
if (isLensFilterEvent(event) || isLensMultiFilterEvent(event)) {
|
||||||
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({
|
||||||
data: {
|
data: {
|
||||||
...event.data,
|
...event.data,
|
||||||
|
|
|
@ -24,7 +24,11 @@ import type {
|
||||||
RowClickContext,
|
RowClickContext,
|
||||||
VisualizeFieldContext,
|
VisualizeFieldContext,
|
||||||
} from '@kbn/ui-actions-plugin/public';
|
} 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 { IndexPatternAggRestrictions } from '@kbn/data-plugin/public';
|
||||||
import type { FieldSpec, DataViewSpec, DataView } from '@kbn/data-views-plugin/common';
|
import type { FieldSpec, DataViewSpec, DataView } from '@kbn/data-views-plugin/common';
|
||||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
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';
|
return event.name === 'filter';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLensMultiFilterEvent(
|
||||||
|
event: ExpressionRendererEvent
|
||||||
|
): event is MultiClickTriggerEvent {
|
||||||
|
return event.name === 'multiFilter';
|
||||||
|
}
|
||||||
|
|
||||||
export function isLensBrushEvent(event: ExpressionRendererEvent): event is BrushTriggerEvent {
|
export function isLensBrushEvent(event: ExpressionRendererEvent): event is BrushTriggerEvent {
|
||||||
return event.name === 'brush';
|
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 { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public';
|
||||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||||
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
|
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 { RequestAdapter } from '@kbn/inspector-plugin/common';
|
||||||
import { ISearchStart } from '@kbn/data-plugin/public';
|
import { ISearchStart } from '@kbn/data-plugin/public';
|
||||||
import type { Document } from './persistence/saved_object_store';
|
import type { Document } from './persistence/saved_object_store';
|
||||||
|
@ -209,7 +213,7 @@ export function getRemoveOperation(
|
||||||
|
|
||||||
export function inferTimeField(
|
export function inferTimeField(
|
||||||
datatableUtilities: DatatableUtilitiesService,
|
datatableUtilities: DatatableUtilitiesService,
|
||||||
context: BrushTriggerEvent['data'] | ClickTriggerEvent['data']
|
context: BrushTriggerEvent['data'] | ClickTriggerEvent['data'] | MultiClickTriggerEvent['data']
|
||||||
) {
|
) {
|
||||||
const tablesAndColumns =
|
const tablesAndColumns =
|
||||||
'table' in context
|
'table' in context
|
||||||
|
@ -218,17 +222,19 @@ export function inferTimeField(
|
||||||
? context.data
|
? context.data
|
||||||
: // if it's a negated filter, never respect bound time field
|
: // if it's a negated filter, never respect bound time field
|
||||||
[];
|
[];
|
||||||
return tablesAndColumns
|
return !Array.isArray(tablesAndColumns)
|
||||||
.map(({ table, column }) => {
|
? [tablesAndColumns]
|
||||||
const tableColumn = table.columns[column];
|
: tablesAndColumns
|
||||||
const hasTimeRange = Boolean(
|
.map(({ table, column }) => {
|
||||||
tableColumn && datatableUtilities.getDateHistogramMeta(tableColumn)?.timeRange
|
const tableColumn = table.columns[column];
|
||||||
);
|
const hasTimeRange = Boolean(
|
||||||
if (hasTimeRange) {
|
tableColumn && datatableUtilities.getDateHistogramMeta(tableColumn)?.timeRange
|
||||||
return tableColumn.meta.field;
|
);
|
||||||
}
|
if (hasTimeRange) {
|
||||||
})
|
return tableColumn.meta.field;
|
||||||
.find(Boolean);
|
}
|
||||||
|
})
|
||||||
|
.find(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renewIDs<T = unknown>(
|
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();
|
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', () => {
|
describe('lens dashboard tests', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await PageObjects.common.navigateToApp('dashboard');
|
await PageObjects.common.navigateToApp('dashboard');
|
||||||
|
@ -90,6 +95,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
expect(hasIpFilter).to.be(true);
|
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
|
// Requires xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled
|
||||||
// setting set in kibana.yml to test (not enabled by default)
|
// setting set in kibana.yml to test (not enabled by default)
|
||||||
it('should hide old "explore underlying data" action', async () => {
|
it('should hide old "explore underlying data" action', async () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue