mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Query based annotations (#138753)
* ⚗️ Initial code for query based annotations * 🐛 Solved more conflicts * ⚗️ More scaffolding layout * ⚗️ Initial indexpatetrn move into frame * ⚗️ Make field selection work * 🚧 Fixed almost all dataViews occurrencies, but changeIndexPattern * 🚧 More work on change index pattern * Move lens dataViews state into main state * 🔥 Remove some old cruft from the code * 🐛 Fix dataViews layer change * 🐛 Fix datasourceLayers refs * 🔥 Remove more old cruft * 🐛 Fix bug when loading SO * 🐛 Fix initial existence flag * 🏷️ Fix type issues * 🏷️ Fix types and tests * 🏷️ Fix types issues * ✅ Fix more tests * ✅ Fix with new dataViews structure * ✅ Fix more test mocks * ✅ More tests fixed * 🔥 Removed unused prop * ✅ Down to single broken test suite * 🏷️ Fix type issue * 👌 Integrate selector feedback * ✅ Fix remaining unit tests * 🏷️ fix type issues * 🐛 Fix bug when creating dataview in place * ✨ Update with latest dataview state + fix dataviews picker for annotations * 🐛 Fix edit + remove field flow * Update x-pack/plugins/lens/public/visualizations/xy/types.ts * 📸 Fix snapshot * 🐛 Fix the dataViews switch bug * 🔥 remove old cruft * ♻️ Revert removal from dataviews state branch * ♻️ Load all at once * 🔧 working on persistent state + fix new layer bug * 🔥 remove unused stuff * 🏷️ Fix some typings * 🔧 Fix expression issue * ✅ Add service unit tests * 👌 Integrated feedback * ✨ Add migration code for manual annotations * 🏷️ Fix type issue * ✅ Add some other unit test * 🏷️ Fix more type issues * 🐛 Fix importing issue * ♻️ Make range default color dependant on opint one * 🐛 Fix duplicate fields selection in tooltip section * ✅ Add more unit tests * ✅ Fix broken test * 🏷️ Mute ts error for now * ✅ Fix tests * 🔥 Reduce plugin weight * 🐛 prevent layout shift on panel open * 🐛 Fix extract + inject visualization references * 🏷️ fix type issues * ✨ Add dataview reference migration for annotations * 🔧 Add migration to embedadble * 🏷️ Fix type export * 🐛 Fix more conflicts with main * ✅ Fix tests * 🏷️ Make textField optional * ♻️ Refactor query input to be a shared component * 🐛 Fix missing import * 🐛 fix more import issues * 🔥 remove duplicate code * 🐛 Fix dataView switch bug * 🏷️ Fix type issue * annotations with fetching_event_annotations * portal for kql input fix * timeField goes for default if not filled * limit changes * handle ad-hoc data view references correctly * fix types * adjust tests to datatable format (remove isHidden tests as it's filtered before) * small refactors * fix loading on dashboard * empty is invalid (?) tbd * new tooltip * emptyDatatable * ♻️ Flip field + query inputs * 🏷️ Fix type issue * ✨ Add field validation for text and tooltip fields * tooltip for single annotation * fix tests * fix for non--timefilter dataview * fix annotations test - the cause was that we now don't display label for aggregated annotations ever * use eui elements * newline problem solved * ✅ Add more error tests * 👌 Rename migration state version type * fix types for expression chart * 🐛 Fix i18n id * 🏷️ Fix type issue * fix hidden all annotations * ✅ Fix tests after ishidden removal * 🐛 Revert references migration to an in app solution Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co>
This commit is contained in:
parent
e7a8c875e7
commit
1a1159b0c5
118 changed files with 3357 additions and 961 deletions
|
@ -130,7 +130,7 @@ pageLoadAssetSize:
|
|||
eventAnnotation: 19334
|
||||
screenshotting: 22870
|
||||
synthetics: 40958
|
||||
expressionXY: 36000
|
||||
expressionXY: 38000
|
||||
kibanaUsageCollection: 16463
|
||||
kubernetesSecurity: 77234
|
||||
threatIntelligence: 29195
|
||||
|
|
|
@ -132,6 +132,15 @@ export const createArgsWithLayers = (
|
|||
},
|
||||
],
|
||||
layers: Array.isArray(layers) ? layers : [layers],
|
||||
annotations: {
|
||||
type: 'event_annotations_result',
|
||||
layers: [],
|
||||
datatable: {
|
||||
type: 'datatable',
|
||||
columns: [],
|
||||
rows: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function sampleArgs() {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { ExtendedAnnotationLayerConfigResult } from '../types';
|
||||
import { strings } from '../i18n';
|
||||
import { EXTENDED_ANNOTATION_LAYER } from '../constants';
|
||||
|
||||
export interface EventAnnotationResultArgs {
|
||||
layers?: ExtendedAnnotationLayerConfigResult[];
|
||||
datatable: Datatable;
|
||||
}
|
||||
|
||||
export interface EventAnnotationResultResult {
|
||||
type: 'event_annotations_result';
|
||||
layers: ExtendedAnnotationLayerConfigResult[];
|
||||
datatable: Datatable;
|
||||
}
|
||||
|
||||
export function eventAnnotationsResult(): ExpressionFunctionDefinition<
|
||||
'event_annotations_result',
|
||||
null,
|
||||
EventAnnotationResultArgs,
|
||||
EventAnnotationResultResult
|
||||
> {
|
||||
return {
|
||||
name: 'event_annotations_result',
|
||||
aliases: [],
|
||||
type: 'event_annotations_result',
|
||||
inputTypes: ['null'],
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
args: {
|
||||
layers: {
|
||||
types: [EXTENDED_ANNOTATION_LAYER],
|
||||
multi: true,
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
},
|
||||
datatable: {
|
||||
types: ['datatable'],
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
return {
|
||||
...args,
|
||||
type: 'event_annotations_result',
|
||||
layers: args.layers || [],
|
||||
datatable: args.datatable || {},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -6,14 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { LayerTypes, EXTENDED_ANNOTATION_LAYER } from '../constants';
|
||||
import { ExtendedAnnotationLayerConfigResult, ExtendedAnnotationLayerArgs } from '../types';
|
||||
import { strings } from '../i18n';
|
||||
|
||||
export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition<
|
||||
typeof EXTENDED_ANNOTATION_LAYER,
|
||||
Datatable,
|
||||
null,
|
||||
ExtendedAnnotationLayerArgs,
|
||||
ExtendedAnnotationLayerConfigResult
|
||||
> {
|
||||
|
@ -21,7 +21,7 @@ export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition<
|
|||
name: EXTENDED_ANNOTATION_LAYER,
|
||||
aliases: [],
|
||||
type: EXTENDED_ANNOTATION_LAYER,
|
||||
inputTypes: ['datatable'],
|
||||
inputTypes: ['null'],
|
||||
help: strings.getAnnotationLayerFnHelp(),
|
||||
args: {
|
||||
simpleView: {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
EXTENDED_DATA_LAYER,
|
||||
REFERENCE_LINE_LAYER,
|
||||
LAYERED_XY_VIS,
|
||||
EXTENDED_ANNOTATION_LAYER,
|
||||
REFERENCE_LINE,
|
||||
} from '../constants';
|
||||
import { commonXYArgs } from './common_xy_args';
|
||||
|
@ -26,12 +25,18 @@ export const layeredXyVisFunction: LayeredXyVisFn = {
|
|||
args: {
|
||||
...commonXYArgs,
|
||||
layers: {
|
||||
types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER, REFERENCE_LINE],
|
||||
types: [EXTENDED_DATA_LAYER, REFERENCE_LINE_LAYER, REFERENCE_LINE],
|
||||
help: i18n.translate('expressionXY.layeredXyVis.layers.help', {
|
||||
defaultMessage: 'Layers of visual series',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
annotations: {
|
||||
types: ['event_annotations_result'],
|
||||
help: i18n.translate('expressionXY.layeredXyVis.annotations.help', {
|
||||
defaultMessage: 'Annotations',
|
||||
}),
|
||||
},
|
||||
splitColumnAccessor: {
|
||||
types: ['vis_dimension', 'string'],
|
||||
help: strings.getSplitColumnAccessorHelp(),
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('xyVis', () => {
|
|||
const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer;
|
||||
const result = await xyVisFunction.fn(
|
||||
data,
|
||||
{ ...rest, ...restLayerArgs, referenceLines: [], annotationLayers: [] },
|
||||
{ ...rest, ...restLayerArgs, referenceLines: [] },
|
||||
createMockExecutionContext()
|
||||
);
|
||||
|
||||
|
@ -53,7 +53,6 @@ describe('xyVis', () => {
|
|||
...{ ...sampleLayer, markSizeAccessor: 'b' },
|
||||
markSizeRatio: 0,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
|
@ -67,7 +66,6 @@ describe('xyVis', () => {
|
|||
...{ ...sampleLayer, markSizeAccessor: 'b' },
|
||||
markSizeRatio: 101,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
|
@ -86,7 +84,6 @@ describe('xyVis', () => {
|
|||
...restLayerArgs,
|
||||
minTimeBarInterval: '1q',
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
|
@ -105,7 +102,6 @@ describe('xyVis', () => {
|
|||
...restLayerArgs,
|
||||
minTimeBarInterval: '1h',
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
|
@ -124,7 +120,6 @@ describe('xyVis', () => {
|
|||
...restLayerArgs,
|
||||
addTimeMarker: true,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
},
|
||||
createMockExecutionContext()
|
||||
)
|
||||
|
@ -144,7 +139,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
splitRowAccessor,
|
||||
},
|
||||
createMockExecutionContext()
|
||||
|
@ -165,7 +160,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
splitColumnAccessor,
|
||||
},
|
||||
createMockExecutionContext()
|
||||
|
@ -185,7 +180,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
markSizeRatio: 5,
|
||||
},
|
||||
createMockExecutionContext()
|
||||
|
@ -207,7 +202,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
seriesType: 'bar',
|
||||
showLines: true,
|
||||
},
|
||||
|
@ -230,7 +225,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
isHistogram: true,
|
||||
xScaleType: 'time',
|
||||
xAxisConfig: {
|
||||
|
@ -257,7 +252,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
xAxisConfig: {
|
||||
type: 'xAxisConfig',
|
||||
extent: {
|
||||
|
@ -287,7 +282,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
xAxisConfig: {
|
||||
type: 'xAxisConfig',
|
||||
extent: { type: 'axisExtentConfig', mode: 'dataBounds' },
|
||||
|
@ -308,7 +303,7 @@ describe('xyVis', () => {
|
|||
...rest,
|
||||
...restLayerArgs,
|
||||
referenceLines: [],
|
||||
annotationLayers: [],
|
||||
|
||||
isHistogram: true,
|
||||
xAxisConfig: {
|
||||
type: 'xAxisConfig',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { XyVisFn } from '../types';
|
||||
import { XY_VIS, REFERENCE_LINE, ANNOTATION_LAYER } from '../constants';
|
||||
import { XY_VIS, REFERENCE_LINE } from '../constants';
|
||||
import { strings } from '../i18n';
|
||||
import { commonXYArgs } from './common_xy_args';
|
||||
import { commonDataLayerArgs } from './common_data_layer_args';
|
||||
|
@ -39,11 +39,6 @@ export const xyVisFunction: XyVisFn = {
|
|||
help: strings.getReferenceLinesHelp(),
|
||||
multi: true,
|
||||
},
|
||||
annotationLayers: {
|
||||
types: [ANNOTATION_LAYER],
|
||||
help: strings.getAnnotationLayerHelp(),
|
||||
multi: true,
|
||||
},
|
||||
splitColumnAccessor: {
|
||||
types: ['vis_dimension', 'string'],
|
||||
help: strings.getSplitColumnAccessorHelp(),
|
||||
|
|
|
@ -63,7 +63,6 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
|
|||
|
||||
const {
|
||||
referenceLines = [],
|
||||
annotationLayers = [],
|
||||
// data_layer args
|
||||
seriesType,
|
||||
accessors,
|
||||
|
@ -101,7 +100,6 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
|
|||
const layers: XYLayerConfig[] = [
|
||||
...appendLayerIds(dataLayers, 'dataLayers'),
|
||||
...appendLayerIds(referenceLines, 'referenceLines'),
|
||||
...appendLayerIds(annotationLayers, 'annotationLayers'),
|
||||
];
|
||||
|
||||
logDatatable(data, layers, handlers, args.splitColumnAccessor, args.splitRowAccessor);
|
||||
|
|
|
@ -34,7 +34,7 @@ export type {
|
|||
DataLayerConfig,
|
||||
FittingFunction,
|
||||
AxisExtentConfig,
|
||||
CollectiveConfig,
|
||||
MergedAnnotation,
|
||||
LegendConfigResult,
|
||||
AxesSettingsConfig,
|
||||
XAxisConfigResult,
|
||||
|
|
|
@ -215,7 +215,6 @@ export interface XYArgs extends DataLayerArgs {
|
|||
emphasizeFitting?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
referenceLines: ReferenceLineConfigResult[];
|
||||
annotationLayers: AnnotationLayerConfigResult[];
|
||||
fittingFunction?: FittingFunction;
|
||||
fillOpacity?: number;
|
||||
hideEndzones?: boolean;
|
||||
|
@ -233,12 +232,21 @@ export interface XYArgs extends DataLayerArgs {
|
|||
showTooltip: boolean;
|
||||
}
|
||||
|
||||
export interface ExpressionAnnotationsLayers {
|
||||
layers: AnnotationLayerConfigResult[];
|
||||
datatable: Datatable;
|
||||
}
|
||||
export type ExpressionAnnotationResult = ExpressionAnnotationsLayers & {
|
||||
type: 'event_annotations_result';
|
||||
};
|
||||
|
||||
export interface LayeredXYArgs {
|
||||
legend: LegendConfigResult;
|
||||
endValue?: EndValue;
|
||||
emphasizeFitting?: boolean;
|
||||
valueLabels: ValueLabelMode;
|
||||
layers?: XYExtendedLayerConfigResult[];
|
||||
annotations?: ExpressionAnnotationResult;
|
||||
fittingFunction?: FittingFunction;
|
||||
fillOpacity?: number;
|
||||
hideEndzones?: boolean;
|
||||
|
@ -279,6 +287,7 @@ export interface XYProps {
|
|||
orderBucketsBySum?: boolean;
|
||||
showTooltip: boolean;
|
||||
singleTable?: boolean;
|
||||
annotations?: ExpressionAnnotationResult;
|
||||
}
|
||||
|
||||
export interface AnnotationLayerArgs {
|
||||
|
@ -326,7 +335,6 @@ export type XYExtendedLayerConfig =
|
|||
export type XYExtendedLayerConfigResult =
|
||||
| ExtendedDataLayerConfigResult
|
||||
| ReferenceLineLayerConfigResult
|
||||
| ExtendedAnnotationLayerConfigResult
|
||||
| ReferenceLineConfigResult;
|
||||
|
||||
export interface ExtendedReferenceLineDecorationConfig extends ReferenceLineArgs {
|
||||
|
|
|
@ -24,9 +24,10 @@ export interface XYRender {
|
|||
value: XYChartProps;
|
||||
}
|
||||
|
||||
export interface CollectiveConfig extends Omit<ManualPointEventAnnotationArgs, 'icon'> {
|
||||
export interface MergedAnnotation extends Omit<ManualPointEventAnnotationArgs, 'icon'> {
|
||||
timebucket: number;
|
||||
position: 'bottom';
|
||||
icon?: AvailableAnnotationIcon | string;
|
||||
customTooltipDetails?: AnnotationTooltipFormatter | undefined;
|
||||
customTooltipDetails: AnnotationTooltipFormatter;
|
||||
isGrouped: boolean;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`XYChart component annotations should render basic line annotation 1`] = `
|
||||
<LineAnnotation
|
||||
customTooltipDetails={[Function]}
|
||||
dataValues={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -18,8 +19,10 @@ exports[`XYChart component annotations should render basic line annotation 1`] =
|
|||
<Marker
|
||||
config={
|
||||
Object {
|
||||
"customTooltipDetails": [Function],
|
||||
"icon": "triangle",
|
||||
"id": "annotation",
|
||||
"isGrouped": false,
|
||||
"label": "Annotation",
|
||||
"position": "bottom",
|
||||
"time": "2022-03-18T08:25:17.140Z",
|
||||
|
@ -38,6 +41,7 @@ exports[`XYChart component annotations should render basic line annotation 1`] =
|
|||
/>
|
||||
}
|
||||
markerPosition="top"
|
||||
placement="bottom"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
|
@ -101,9 +105,9 @@ exports[`XYChart component annotations should render grouped line annotations pr
|
|||
dataValues={
|
||||
Array [
|
||||
Object {
|
||||
"dataValue": 1647591900025,
|
||||
"dataValue": 1647591917125,
|
||||
"details": "Event 1",
|
||||
"header": 1647591900000,
|
||||
"header": 1647591917100,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -118,19 +122,19 @@ exports[`XYChart component annotations should render grouped line annotations pr
|
|||
"customTooltipDetails": [Function],
|
||||
"icon": "3",
|
||||
"id": "event1",
|
||||
"isGrouped": true,
|
||||
"label": "Event 1",
|
||||
"lineStyle": "dashed",
|
||||
"lineWidth": 3,
|
||||
"position": "bottom",
|
||||
"textVisibility": undefined,
|
||||
"time": "2022-03-18T08:25:00.000Z",
|
||||
"timebucket": 1647591900000,
|
||||
"timebucket": 1647591917100,
|
||||
"type": "point",
|
||||
}
|
||||
}
|
||||
hasReducedPadding={true}
|
||||
isHorizontal={true}
|
||||
label="Event 1"
|
||||
/>
|
||||
}
|
||||
markerBody={
|
||||
|
@ -139,6 +143,7 @@ exports[`XYChart component annotations should render grouped line annotations pr
|
|||
/>
|
||||
}
|
||||
markerPosition="top"
|
||||
placement="bottom"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
|
@ -161,9 +166,9 @@ exports[`XYChart component annotations should render grouped line annotations wi
|
|||
dataValues={
|
||||
Array [
|
||||
Object {
|
||||
"dataValue": 1647591900025,
|
||||
"dataValue": 1647591917125,
|
||||
"details": "Event 1",
|
||||
"header": 1647591900000,
|
||||
"header": 1647591917100,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -178,19 +183,19 @@ exports[`XYChart component annotations should render grouped line annotations wi
|
|||
"customTooltipDetails": [Function],
|
||||
"icon": "2",
|
||||
"id": "event1",
|
||||
"isGrouped": true,
|
||||
"label": "Event 1",
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1,
|
||||
"position": "bottom",
|
||||
"textVisibility": undefined,
|
||||
"time": "2022-03-18T08:25:00.000Z",
|
||||
"timebucket": 1647591900000,
|
||||
"timebucket": 1647591917100,
|
||||
"type": "point",
|
||||
}
|
||||
}
|
||||
hasReducedPadding={true}
|
||||
isHorizontal={true}
|
||||
label="Event 1"
|
||||
/>
|
||||
}
|
||||
markerBody={
|
||||
|
@ -199,6 +204,7 @@ exports[`XYChart component annotations should render grouped line annotations wi
|
|||
/>
|
||||
}
|
||||
markerPosition="top"
|
||||
placement="bottom"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
|
@ -214,6 +220,7 @@ exports[`XYChart component annotations should render grouped line annotations wi
|
|||
|
||||
exports[`XYChart component annotations should render simplified annotations when simpleView is true 1`] = `
|
||||
<LineAnnotation
|
||||
customTooltipDetails={[Function]}
|
||||
dataValues={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -227,6 +234,7 @@ exports[`XYChart component annotations should render simplified annotations when
|
|||
id="annotation_2022_03_18_t_08_25_17_140_z"
|
||||
key="annotation_2022_03_18_t_08_25_17_140_z"
|
||||
markerPosition="top"
|
||||
placement="bottom"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
|
|
|
@ -10,7 +10,7 @@ import './annotations.scss';
|
|||
import './reference_lines/reference_lines.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { groupBy, snakeCase } from 'lodash';
|
||||
import {
|
||||
AnnotationDomainType,
|
||||
AnnotationTooltipFormatter,
|
||||
|
@ -19,24 +19,33 @@ import {
|
|||
RectAnnotation,
|
||||
} from '@elastic/charts';
|
||||
import moment from 'moment';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type {
|
||||
EventAnnotationOutput,
|
||||
ManualPointEventAnnotationArgs,
|
||||
ManualRangeEventAnnotationRow,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { FieldFormat, FormatFactory } from '@kbn/field-formats-plugin/common';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import { Datatable, DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { ManualPointEventAnnotationRow } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types';
|
||||
import type { CollectiveConfig } from '../../common';
|
||||
import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { PointEventAnnotationRow } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { MergedAnnotation } from '../../common';
|
||||
import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers';
|
||||
import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers';
|
||||
|
||||
export interface AnnotationsProps {
|
||||
groupedLineAnnotations: CollectiveConfig[];
|
||||
groupedLineAnnotations: MergedAnnotation[];
|
||||
rangeAnnotations: ManualRangeEventAnnotationRow[];
|
||||
formatter?: FieldFormat;
|
||||
isHorizontal: boolean;
|
||||
|
@ -47,27 +56,126 @@ export interface AnnotationsProps {
|
|||
outsideDimension: number;
|
||||
}
|
||||
|
||||
const TooltipAnnotationHeader = ({
|
||||
row: { label, color, icon },
|
||||
}: {
|
||||
row: PointEventAnnotationRow;
|
||||
}) => (
|
||||
<div className="echTooltip__item--container" key={snakeCase(label)}>
|
||||
<EuiFlexGroup className="echTooltip__label" gutterSize="xs">
|
||||
{hasIcon(icon) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnnotationIcon type={icon} color={color} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem> {label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TooltipAnnotationDetails = ({
|
||||
row,
|
||||
extraFields,
|
||||
isGrouped,
|
||||
}: {
|
||||
row: PointEventAnnotationRow;
|
||||
extraFields: Array<{
|
||||
key: string;
|
||||
name: string;
|
||||
formatter: FieldFormat | undefined;
|
||||
}>;
|
||||
isGrouped?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="echTooltip__item--container">
|
||||
<span className="echTooltip__value">
|
||||
{isGrouped && <div>{moment(row.time).format('YYYY-MM-DD, hh:mm:ss')}</div>}
|
||||
|
||||
<div className="xyAnnotationTooltip__extraFields">
|
||||
{extraFields.map((field) => (
|
||||
<div>
|
||||
{field.name}:{' '}
|
||||
{field.formatter ? field.formatter.convert(row[field.key]) : row[field.key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getExtraFields = (
|
||||
row: PointEventAnnotationRow,
|
||||
formatFactory: FormatFactory,
|
||||
columns: DatatableColumn[] | undefined
|
||||
) => {
|
||||
return Object.keys(row)
|
||||
.filter((key) => key.startsWith('field:'))
|
||||
.map((key) => {
|
||||
const columnFormatter = columns?.find((c) => c.id === key)?.meta?.params;
|
||||
return {
|
||||
key,
|
||||
name: key.replace('field:', ''),
|
||||
formatter: columnFormatter && formatFactory(columnFormatter),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createCustomTooltipDetails =
|
||||
(
|
||||
config: ManualPointEventAnnotationArgs[],
|
||||
formatter?: FieldFormat
|
||||
): AnnotationTooltipFormatter | undefined =>
|
||||
rows: PointEventAnnotationRow[],
|
||||
formatFactory: FormatFactory,
|
||||
columns: DatatableColumn[] | undefined
|
||||
): AnnotationTooltipFormatter =>
|
||||
() => {
|
||||
const groupedConfigs = groupBy(rows, 'id');
|
||||
const lastElement = rows[rows.length - 1];
|
||||
return (
|
||||
<div key={config[0].time}>
|
||||
{config.map(({ icon, label, time, color }) => (
|
||||
<div className="echTooltip__item--container" key={snakeCase(label)}>
|
||||
<EuiFlexGroup className="echTooltip__label" gutterSize="xs">
|
||||
{hasIcon(icon) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnnotationIcon type={icon} color={color} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem> {label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<span className="echTooltip__value"> {formatter?.convert(time) || String(time)}</span>
|
||||
<div key={rows[0].time} className="xyAnnotationTooltip">
|
||||
{Object.values(groupedConfigs).map((group) => {
|
||||
const firstElement = group[0];
|
||||
const extraFields = getExtraFields(firstElement, formatFactory, columns);
|
||||
|
||||
return (
|
||||
<div className="xyAnnotationTooltip__group">
|
||||
<TooltipAnnotationHeader row={firstElement} />
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
hasShadow={false}
|
||||
paddingSize="xs"
|
||||
borderRadius="none"
|
||||
hasBorder={true}
|
||||
>
|
||||
{group.map((row, index) => (
|
||||
<>
|
||||
{index > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiSpacer size="xs" />
|
||||
</>
|
||||
)}
|
||||
<TooltipAnnotationDetails
|
||||
key={snakeCase(row.time)}
|
||||
isGrouped={rows.length > 1}
|
||||
row={row}
|
||||
extraFields={extraFields}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</EuiPanel>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{lastElement.skippedCount && (
|
||||
<div className="echTooltip__value">
|
||||
<FormattedMessage
|
||||
id="expressionXY.annotations.skippedCount"
|
||||
defaultMessage="... +{value} more"
|
||||
values={{ value: lastElement.skippedCount }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -109,10 +217,12 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8;
|
|||
export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2;
|
||||
|
||||
export const getAnnotationsGroupedByInterval = (
|
||||
annotations: ManualPointEventAnnotationRow[],
|
||||
formatter?: FieldFormat
|
||||
annotations: PointEventAnnotationRow[],
|
||||
configs: EventAnnotationOutput[] | undefined,
|
||||
columns: DatatableColumn[] | undefined,
|
||||
formatFactory: FormatFactory
|
||||
) => {
|
||||
const visibleGroupedConfigs = annotations.reduce<Record<string, ManualPointEventAnnotationRow[]>>(
|
||||
const visibleGroupedConfigs = annotations.reduce<Record<string, PointEventAnnotationRow[]>>(
|
||||
(acc, current) => {
|
||||
const timebucket = moment(current.timebucket).valueOf();
|
||||
return {
|
||||
|
@ -122,24 +232,36 @@ export const getAnnotationsGroupedByInterval = (
|
|||
},
|
||||
{}
|
||||
);
|
||||
let collectiveConfig: CollectiveConfig;
|
||||
return Object.entries(visibleGroupedConfigs).map(([timebucket, configArr]) => {
|
||||
collectiveConfig = {
|
||||
...configArr[0],
|
||||
icon: configArr[0].icon || 'triangle',
|
||||
return Object.entries(visibleGroupedConfigs).map(([timebucket, rowsPerBucket]) => {
|
||||
const firstRow = rowsPerBucket[0];
|
||||
|
||||
const config = configs?.find((c) => c.id === firstRow.id);
|
||||
const textField = config && 'textField' in config && config?.textField;
|
||||
const columnFormatter = columns?.find((c) => c.id === `field:${textField}`)?.meta?.params;
|
||||
const formatter = columnFormatter && formatFactory(columnFormatter);
|
||||
const label =
|
||||
textField && formatter && `field:${textField}` in firstRow
|
||||
? formatter.convert(firstRow[`field:${textField}`])
|
||||
: firstRow.label;
|
||||
const mergedAnnotation: MergedAnnotation = {
|
||||
...firstRow,
|
||||
label,
|
||||
icon: firstRow.icon || 'triangle',
|
||||
timebucket: Number(timebucket),
|
||||
position: 'bottom',
|
||||
customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, formatFactory, columns),
|
||||
isGrouped: false,
|
||||
};
|
||||
if (configArr.length > 1) {
|
||||
const commonStyles = getCommonStyles(configArr);
|
||||
collectiveConfig = {
|
||||
...collectiveConfig,
|
||||
if (rowsPerBucket.length > 1) {
|
||||
const commonStyles = getCommonStyles(rowsPerBucket);
|
||||
return {
|
||||
...mergedAnnotation,
|
||||
...commonStyles,
|
||||
icon: String(configArr.length),
|
||||
customTooltipDetails: createCustomTooltipDetails(configArr, formatter),
|
||||
isGrouped: true,
|
||||
icon: String(rowsPerBucket.length),
|
||||
};
|
||||
}
|
||||
return collectiveConfig;
|
||||
return mergedAnnotation;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -161,20 +283,10 @@ export const Annotations = ({
|
|||
<>
|
||||
{groupedLineAnnotations.map((annotation) => {
|
||||
const markerPositionVertical = Position.Top;
|
||||
const markerPosition = isHorizontal
|
||||
? mapVerticalToHorizontalPlacement(markerPositionVertical)
|
||||
: markerPositionVertical;
|
||||
const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE;
|
||||
const id = snakeCase(`${annotation.id}-${annotation.time}`);
|
||||
const { timebucket, time } = annotation;
|
||||
const isGrouped = Boolean(annotation.customTooltipDetails);
|
||||
const header =
|
||||
formatter?.convert(isGrouped ? timebucket : time) ||
|
||||
moment(isGrouped ? timebucket : time).toISOString();
|
||||
const { timebucket, time, isGrouped, id: configId } = annotation;
|
||||
const strokeWidth = simpleView ? 1 : annotation.lineWidth || 1;
|
||||
const dataValue = isGrouped
|
||||
? moment(isBarChart && minInterval ? timebucket + minInterval / 2 : timebucket).valueOf()
|
||||
: moment(time).valueOf();
|
||||
const id = snakeCase(`${configId}-${time}`);
|
||||
return (
|
||||
<LineAnnotation
|
||||
id={id}
|
||||
|
@ -187,7 +299,7 @@ export const Annotations = ({
|
|||
config: annotation,
|
||||
isHorizontal: !isHorizontal,
|
||||
hasReducedPadding,
|
||||
label: annotation.label,
|
||||
label: !isGrouped ? annotation.label : undefined,
|
||||
rotateClassName: isHorizontal ? 'xyAnnotationIcon_rotate90' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
@ -197,21 +309,34 @@ export const Annotations = ({
|
|||
!simpleView ? (
|
||||
<MarkerBody
|
||||
label={
|
||||
annotation.textVisibility && !hasReducedPadding ? annotation.label : undefined
|
||||
!isGrouped && annotation.textVisibility && !hasReducedPadding
|
||||
? annotation.label
|
||||
: undefined
|
||||
}
|
||||
isHorizontal={!isHorizontal}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
markerPosition={markerPosition}
|
||||
markerPosition={
|
||||
isHorizontal
|
||||
? mapVerticalToHorizontalPlacement(markerPositionVertical)
|
||||
: markerPositionVertical
|
||||
}
|
||||
dataValues={[
|
||||
{
|
||||
dataValue,
|
||||
header,
|
||||
dataValue: isGrouped
|
||||
? moment(
|
||||
isBarChart && minInterval ? timebucket + minInterval / 2 : timebucket
|
||||
).valueOf()
|
||||
: moment(time).valueOf(),
|
||||
header:
|
||||
formatter?.convert(isGrouped ? timebucket : time) ||
|
||||
moment(isGrouped ? timebucket : time).toISOString(),
|
||||
details: annotation.label,
|
||||
},
|
||||
]}
|
||||
customTooltipDetails={annotation.customTooltipDetails}
|
||||
placement={'bottom'}
|
||||
style={{
|
||||
line: {
|
||||
strokeWidth,
|
||||
|
|
|
@ -54,13 +54,8 @@ import {
|
|||
sampleLayer,
|
||||
} from '../../common/__mocks__';
|
||||
import { XYChart, XYChartRenderProps } from './xy_chart';
|
||||
import {
|
||||
CommonXYAnnotationLayerConfig,
|
||||
ExtendedDataLayerConfig,
|
||||
XYProps,
|
||||
} from '../../common/types';
|
||||
import { ExtendedDataLayerConfig, XYProps, AnnotationLayerConfigResult } from '../../common/types';
|
||||
import { DataLayers } from './data_layers';
|
||||
import { Annotations } from './annotations';
|
||||
import { SplitChart } from './split_chart';
|
||||
import { LegendSize } from '@kbn/visualizations-plugin/common';
|
||||
|
||||
|
@ -3068,12 +3063,18 @@ describe('XYChart component', () => {
|
|||
label: 'Event range',
|
||||
type: 'manual_range_event_annotation' as const,
|
||||
};
|
||||
const configToRowHelper = (config: EventAnnotationOutput) => {
|
||||
return {
|
||||
...config,
|
||||
timebucket: 1647591917100,
|
||||
type: config.type === 'manual_point_event_annotation' ? 'point' : 'range',
|
||||
};
|
||||
};
|
||||
const createLayerWithAnnotations = (
|
||||
annotations: EventAnnotationOutput[] = [defaultLineStaticAnnotation]
|
||||
): CommonXYAnnotationLayerConfig => ({
|
||||
): AnnotationLayerConfigResult => ({
|
||||
type: 'annotationLayer',
|
||||
layerType: LayerTypes.ANNOTATIONS,
|
||||
layerId: 'annotation',
|
||||
annotations,
|
||||
});
|
||||
function sampleArgsWithAnnotations(annotationLayers = [createLayerWithAnnotations()]) {
|
||||
|
@ -3081,7 +3082,16 @@ describe('XYChart component', () => {
|
|||
return {
|
||||
args: {
|
||||
...args,
|
||||
layers: [dateHistogramLayer, ...annotationLayers],
|
||||
layers: [dateHistogramLayer],
|
||||
annotations: {
|
||||
type: 'event_annotations_result' as const,
|
||||
layers: annotationLayers,
|
||||
datatable: {
|
||||
type: 'datatable' as const,
|
||||
columns: [],
|
||||
rows: annotationLayers.flatMap((l) => l.annotations.map(configToRowHelper)),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -3102,7 +3112,7 @@ describe('XYChart component', () => {
|
|||
const { args } = sampleArgsWithAnnotations([
|
||||
createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]),
|
||||
]);
|
||||
(args.layers[1] as CommonXYAnnotationLayerConfig).simpleView = true;
|
||||
args.annotations.layers[0].simpleView = true;
|
||||
const component = mount(<XYChart {...defaultProps} args={args} />);
|
||||
expect(component.find('LineAnnotation')).toMatchSnapshot();
|
||||
expect(component.find('RectAnnotation')).toMatchSnapshot();
|
||||
|
@ -3135,7 +3145,7 @@ describe('XYChart component', () => {
|
|||
// checking tooltip
|
||||
const renderLinks = mount(<div>{groupedAnnotation.prop('customTooltipDetails')!()}</div>);
|
||||
expect(renderLinks.text()).toEqual(
|
||||
' Event 1 2022-03-18T08:25:00.000Z Event 3 2022-03-18T08:25:00.001Z Event 2 2022-03-18T08:25:00.020Z'
|
||||
' Event 12022-03-18, 04:25:002022-03-18, 04:25:002022-03-18, 04:25:00'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -3161,28 +3171,6 @@ describe('XYChart component', () => {
|
|||
// styles are default because they are different for both annotations
|
||||
expect(groupedAnnotation).toMatchSnapshot();
|
||||
});
|
||||
test('should not render hidden annotations', () => {
|
||||
const { args } = sampleArgsWithAnnotations([
|
||||
createLayerWithAnnotations([
|
||||
customLineStaticAnnotation,
|
||||
{ ...customLineStaticAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' },
|
||||
{
|
||||
...customLineStaticAnnotation,
|
||||
time: '2022-03-18T08:35:00.001Z',
|
||||
label: 'Event 3',
|
||||
isHidden: true,
|
||||
},
|
||||
defaultRangeStaticAnnotation,
|
||||
{ ...defaultRangeStaticAnnotation, label: 'range', isHidden: true },
|
||||
]),
|
||||
]);
|
||||
const component = mount(<XYChart {...defaultProps} args={args} />);
|
||||
const lineAnnotations = component.find(LineAnnotation);
|
||||
const rectAnnotations = component.find(Annotations).find(RectAnnotation);
|
||||
|
||||
expect(lineAnnotations.length).toEqual(2);
|
||||
expect(rectAnnotations.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('split chart', () => {
|
||||
|
|
|
@ -29,19 +29,13 @@ import {
|
|||
XYChartElementEvent,
|
||||
} from '@elastic/charts';
|
||||
import { partition } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { Datatable, DatatableRow, RenderMode } from '@kbn/expressions-plugin/common';
|
||||
import { Datatable, RenderMode } from '@kbn/expressions-plugin/common';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { EmptyPlaceholder, LegendToggle } from '@kbn/charts-plugin/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import {
|
||||
ManualPointEventAnnotationRow,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
ManualPointEventAnnotationOutput,
|
||||
ManualRangeEventAnnotationRow,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import { PointEventAnnotationRow } from '@kbn/event-annotation-plugin/common';
|
||||
import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public';
|
||||
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
|
||||
import {
|
||||
|
@ -61,11 +55,9 @@ import type {
|
|||
ExtendedReferenceLineDecorationConfig,
|
||||
XYChartProps,
|
||||
AxisExtentConfigResult,
|
||||
CommonXYAnnotationLayerConfig,
|
||||
} from '../../common/types';
|
||||
import {
|
||||
isHorizontalChart,
|
||||
getAnnotationsLayers,
|
||||
getDataLayers,
|
||||
AxisConfiguration,
|
||||
getAxisPosition,
|
||||
|
@ -192,49 +184,6 @@ function createSplitPoint(
|
|||
|
||||
export const XYChartReportable = React.memo(XYChart);
|
||||
|
||||
// TODO: remove this function when we start using fetch_event_annotation expression
|
||||
const convertToAnnotationsTable = (
|
||||
layers: CommonXYAnnotationLayerConfig[],
|
||||
minInterval?: number,
|
||||
firstTimestamp?: number
|
||||
) => {
|
||||
return layers
|
||||
.flatMap(({ annotations }) =>
|
||||
annotations.filter(
|
||||
(a): a is ManualPointEventAnnotationOutput | ManualRangeEventAnnotationOutput =>
|
||||
!a.isHidden && 'time' in a
|
||||
)
|
||||
)
|
||||
.sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf())
|
||||
.map((a) => {
|
||||
const timebucket = getRoundedTimestamp(moment(a.time).valueOf(), firstTimestamp, minInterval);
|
||||
if (a.type === 'manual_point_event_annotation') {
|
||||
const pointRow: ManualPointEventAnnotationRow = {
|
||||
...a,
|
||||
type: 'point',
|
||||
timebucket: moment(timebucket).toISOString(),
|
||||
};
|
||||
return pointRow;
|
||||
}
|
||||
const rangeRow: ManualRangeEventAnnotationRow = {
|
||||
...a,
|
||||
type: 'range',
|
||||
};
|
||||
return rangeRow;
|
||||
});
|
||||
};
|
||||
|
||||
export const sortByTime = (a: DatatableRow, b: DatatableRow) => {
|
||||
return 'time' in a && 'time' in b ? a.time.localeCompare(b.time) : 0;
|
||||
};
|
||||
|
||||
const getRoundedTimestamp = (timestamp: number, firstTimestamp?: number, minInterval?: number) => {
|
||||
if (!firstTimestamp || !minInterval) {
|
||||
return timestamp;
|
||||
}
|
||||
return timestamp - ((timestamp - firstTimestamp) % minInterval);
|
||||
};
|
||||
|
||||
export function XYChart({
|
||||
args,
|
||||
data,
|
||||
|
@ -267,6 +216,7 @@ export function XYChart({
|
|||
splitColumnAccessor,
|
||||
splitRowAccessor,
|
||||
singleTable,
|
||||
annotations,
|
||||
} = args;
|
||||
const chartRef = useRef<Chart>(null);
|
||||
const chartTheme = chartsThemeService.useChartsTheme();
|
||||
|
@ -447,25 +397,18 @@ export function XYChart({
|
|||
};
|
||||
|
||||
const referenceLineLayers = getReferenceLayers(layers);
|
||||
|
||||
const annotationsLayers = getAnnotationsLayers(layers);
|
||||
const firstTable = dataLayers[0]?.table;
|
||||
|
||||
const columnId = dataLayers[0]?.xAccessor
|
||||
? getColumnByAccessor(dataLayers[0]?.xAccessor, firstTable.columns)?.id
|
||||
: null;
|
||||
|
||||
const annotations = convertToAnnotationsTable(
|
||||
annotationsLayers,
|
||||
minInterval,
|
||||
columnId ? firstTable.rows[0]?.[columnId] : undefined
|
||||
const [rangeAnnotations, lineAnnotations] = partition(
|
||||
annotations?.datatable.rows,
|
||||
isRangeAnnotation
|
||||
);
|
||||
|
||||
const [rangeAnnotations, lineAnnotations] = partition(annotations, isRangeAnnotation);
|
||||
const annotationsConfigs = annotations?.layers.flatMap((l) => l.annotations);
|
||||
|
||||
const groupedLineAnnotations = getAnnotationsGroupedByInterval(
|
||||
lineAnnotations as ManualPointEventAnnotationRow[],
|
||||
xAxisFormatter
|
||||
lineAnnotations as PointEventAnnotationRow[],
|
||||
annotationsConfigs,
|
||||
annotations?.datatable.columns,
|
||||
formatFactory
|
||||
);
|
||||
|
||||
const visualConfigs = [
|
||||
|
@ -483,7 +426,10 @@ export function XYChart({
|
|||
...groupedLineAnnotations,
|
||||
].filter(Boolean);
|
||||
|
||||
const shouldHideDetails = annotationsLayers.length > 0 ? annotationsLayers[0].simpleView : false;
|
||||
const shouldHideDetails =
|
||||
annotations?.layers && annotations.layers.length > 0
|
||||
? annotations?.layers[0].simpleView
|
||||
: false;
|
||||
const linesPaddings = !shouldHideDetails
|
||||
? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate)
|
||||
: {};
|
||||
|
|
|
@ -12,7 +12,7 @@ import classnames from 'classnames';
|
|||
import type {
|
||||
IconPosition,
|
||||
ReferenceLineDecorationConfig,
|
||||
CollectiveConfig,
|
||||
MergedAnnotation,
|
||||
} from '../../common/types';
|
||||
import { getBaseIconPlacement } from '../components';
|
||||
import { hasIcon, iconSet } from './icon';
|
||||
|
@ -27,16 +27,16 @@ type PartialReferenceLineDecorationConfig = Pick<
|
|||
position?: Position;
|
||||
};
|
||||
|
||||
type PartialCollectiveConfig = Pick<CollectiveConfig, 'position' | 'icon' | 'textVisibility'>;
|
||||
type PartialMergedAnnotation = Pick<MergedAnnotation, 'position' | 'icon' | 'textVisibility'>;
|
||||
|
||||
const isExtendedDecorationConfig = (
|
||||
config: PartialReferenceLineDecorationConfig | PartialCollectiveConfig | undefined
|
||||
config: PartialReferenceLineDecorationConfig | PartialMergedAnnotation | undefined
|
||||
): config is PartialReferenceLineDecorationConfig =>
|
||||
(config as PartialReferenceLineDecorationConfig)?.iconPosition ? true : false;
|
||||
|
||||
// Note: it does not take into consideration whether the reference line is in view or not
|
||||
export const getLinesCausedPaddings = (
|
||||
visualConfigs: Array<PartialReferenceLineDecorationConfig | PartialCollectiveConfig | undefined>,
|
||||
visualConfigs: Array<PartialReferenceLineDecorationConfig | PartialMergedAnnotation | undefined>,
|
||||
axesMap: AxesMap,
|
||||
shouldRotate: boolean
|
||||
) => {
|
||||
|
|
|
@ -57,10 +57,3 @@ const isAnnotationLayerCommon = (
|
|||
export const isAnnotationsLayer = (
|
||||
layer: CommonXYLayerConfig
|
||||
): layer is CommonXYAnnotationLayerConfig => isAnnotationLayerCommon(layer);
|
||||
|
||||
export const getAnnotationsLayers = (
|
||||
layers: CommonXYLayerConfig[]
|
||||
): CommonXYAnnotationLayerConfig[] =>
|
||||
(layers || []).filter((layer): layer is CommonXYAnnotationLayerConfig =>
|
||||
isAnnotationsLayer(layer)
|
||||
);
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
referenceLineDecorationConfigFunction,
|
||||
} from '../common/expression_functions';
|
||||
import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers';
|
||||
import { eventAnnotationsResult } from '../common/expression_functions/event_annotations_result';
|
||||
|
||||
export interface XYPluginStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -63,6 +64,7 @@ export class ExpressionXyPlugin {
|
|||
expressions.registerFunction(xAxisConfigFunction);
|
||||
expressions.registerFunction(annotationLayerFunction);
|
||||
expressions.registerFunction(extendedAnnotationLayerFunction);
|
||||
expressions.registerFunction(eventAnnotationsResult);
|
||||
expressions.registerFunction(referenceLineFunction);
|
||||
expressions.registerFunction(referenceLineLayerFunction);
|
||||
expressions.registerFunction(xyVisFunction);
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
extendedAnnotationLayerFunction,
|
||||
} from '../common/expression_functions';
|
||||
import { SetupDeps } from './types';
|
||||
import { eventAnnotationsResult } from '../common/expression_functions/event_annotations_result';
|
||||
|
||||
export class ExpressionXyPlugin
|
||||
implements Plugin<ExpressionXyPluginSetup, ExpressionXyPluginStart>
|
||||
|
@ -39,6 +40,7 @@ export class ExpressionXyPlugin
|
|||
expressions.registerFunction(axisExtentConfigFunction);
|
||||
expressions.registerFunction(annotationLayerFunction);
|
||||
expressions.registerFunction(extendedAnnotationLayerFunction);
|
||||
expressions.registerFunction(eventAnnotationsResult);
|
||||
expressions.registerFunction(referenceLineFunction);
|
||||
expressions.registerFunction(referenceLineLayerFunction);
|
||||
expressions.registerFunction(xyVisFunction);
|
||||
|
|
|
@ -29,6 +29,7 @@ export * from './lib/cidr_mask';
|
|||
export * from './lib/date_range';
|
||||
export * from './lib/ip_range';
|
||||
export * from './lib/time_buckets/calc_auto_interval';
|
||||
export { TimeBuckets } from './lib/time_buckets';
|
||||
export * from './migrate_include_exclude_format';
|
||||
export * from './range_fn';
|
||||
export * from './range';
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EventAnnotationGroupOutput } from '../event_annotation_group';
|
||||
|
||||
export interface FetchEventAnnotationArgs {
|
||||
group: EventAnnotationGroupOutput[];
|
||||
}
|
||||
|
||||
export type FetchEventAnnotationOutput = FetchEventAnnotationArgs & {
|
||||
type: 'fetch_event_annotation';
|
||||
};
|
||||
|
||||
export function eventAnnotationGroup(): ExpressionFunctionDefinition<
|
||||
'fetch_event_annotation',
|
||||
null,
|
||||
FetchEventAnnotationArgs,
|
||||
FetchEventAnnotationOutput
|
||||
> {
|
||||
return {
|
||||
name: 'fetch_event_annotation',
|
||||
aliases: [],
|
||||
type: 'fetch_event_annotation',
|
||||
inputTypes: ['null'],
|
||||
help: i18n.translate('eventAnnotation.fetch.description', {
|
||||
defaultMessage: 'Event annotation fetch',
|
||||
}),
|
||||
args: {
|
||||
group: {
|
||||
types: ['event_annotation_group'],
|
||||
help: i18n.translate('eventAnnotation.group.args.annotationGroups', {
|
||||
defaultMessage: 'Annotation group',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
return {
|
||||
type: 'fetch_event_annotation',
|
||||
group: args.group,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -23,7 +23,6 @@ export const getFetchEventAnnotationsMeta: () => Omit<
|
|||
}),
|
||||
args: {
|
||||
groups: {
|
||||
required: true,
|
||||
types: ['event_annotation_group'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.args.annotationConfigs', {
|
||||
defaultMessage: 'Annotation configs',
|
||||
|
|
|
@ -10,13 +10,13 @@ import { defer, firstValueFrom } from 'rxjs';
|
|||
import { partition } from 'lodash';
|
||||
import {
|
||||
AggsStart,
|
||||
DataViewsContract,
|
||||
DataView,
|
||||
DataViewSpec,
|
||||
ExpressionValueSearchContext,
|
||||
parseEsInterval,
|
||||
AggConfigs,
|
||||
IndexPatternExpressionType,
|
||||
} from '@kbn/data-plugin/common';
|
||||
|
||||
import { ExecutionContext } from '@kbn/expressions-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts';
|
||||
|
@ -26,6 +26,7 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
|||
import { handleRequest } from './handle_request';
|
||||
import {
|
||||
ANNOTATIONS_PER_BUCKET,
|
||||
getCalculatedInterval,
|
||||
isInRange,
|
||||
isManualAnnotation,
|
||||
isManualPointAnnotation,
|
||||
|
@ -45,9 +46,9 @@ interface ManualGroup {
|
|||
interface QueryGroup {
|
||||
type: 'query';
|
||||
annotations: QueryPointEventAnnotationOutput[];
|
||||
allFields?: string[];
|
||||
dataView: IndexPatternExpressionType;
|
||||
timeField: string;
|
||||
dataView: DataView;
|
||||
allFields?: string[];
|
||||
}
|
||||
|
||||
export function getTimeZone(uiSettings: IUiSettingsClient) {
|
||||
|
@ -58,6 +59,7 @@ export function getTimeZone(uiSettings: IUiSettingsClient) {
|
|||
|
||||
return configuredTimeZone;
|
||||
}
|
||||
const emptyDatatable = { rows: [], columns: [], type: 'datatable' };
|
||||
|
||||
export const requestEventAnnotations = (
|
||||
input: ExpressionValueSearchContext | null,
|
||||
|
@ -71,20 +73,39 @@ export const requestEventAnnotations = (
|
|||
getStartDependencies: () => Promise<FetchEventAnnotationsStartDependencies>
|
||||
) => {
|
||||
return defer(async () => {
|
||||
if (!input?.timeRange || !args.groups) {
|
||||
return emptyDatatable;
|
||||
}
|
||||
const { aggs, dataViews, searchSource, getNow, uiSettings } = await getStartDependencies();
|
||||
|
||||
const interval = getCalculatedInterval(uiSettings, args.interval, input?.timeRange);
|
||||
if (!interval) {
|
||||
return emptyDatatable;
|
||||
}
|
||||
|
||||
const uniqueDataViewsToLoad = args.groups
|
||||
.map((g) => g.dataView.value)
|
||||
.reduce<DataViewSpec[]>((acc, current) => {
|
||||
if (acc.find((el) => el.id === current.id)) return acc;
|
||||
return [...acc, current];
|
||||
}, []);
|
||||
|
||||
const loadedDataViews = await Promise.all(
|
||||
uniqueDataViewsToLoad.map((dataView) => dataViews.create(dataView, true))
|
||||
);
|
||||
|
||||
const [manualGroups, queryGroups] = partition(
|
||||
regroupForRequestOptimization(args, input),
|
||||
regroupForRequestOptimization(args, input, loadedDataViews),
|
||||
isManualSubGroup
|
||||
);
|
||||
|
||||
const manualAnnotationDatatableRows = manualGroups.length
|
||||
? convertManualToDatatableRows(manualGroups[0], args.interval, getTimeZone(uiSettings))
|
||||
? convertManualToDatatableRows(manualGroups[0], interval, getTimeZone(uiSettings))
|
||||
: [];
|
||||
if (!queryGroups.length) {
|
||||
return manualAnnotationDatatableRows.length
|
||||
? wrapRowsInDatatable(manualAnnotationDatatableRows)
|
||||
: null;
|
||||
: emptyDatatable;
|
||||
}
|
||||
|
||||
const createEsaggsSingleRequest = async ({
|
||||
|
@ -92,7 +113,7 @@ export const requestEventAnnotations = (
|
|||
aggConfigs,
|
||||
timeFields,
|
||||
}: {
|
||||
dataView: any;
|
||||
dataView: DataView;
|
||||
aggConfigs: AggConfigs;
|
||||
timeFields: string[];
|
||||
}) =>
|
||||
|
@ -113,12 +134,7 @@ export const requestEventAnnotations = (
|
|||
})
|
||||
);
|
||||
|
||||
const esaggsGroups = await prepareEsaggsForQueryGroups(
|
||||
queryGroups,
|
||||
args.interval,
|
||||
dataViews,
|
||||
aggs
|
||||
);
|
||||
const esaggsGroups = await prepareEsaggsForQueryGroups(queryGroups, interval, aggs);
|
||||
|
||||
const allQueryAnnotationsConfigs = queryGroups.flatMap((group) => group.annotations);
|
||||
|
||||
|
@ -169,23 +185,9 @@ const convertManualToDatatableRows = (
|
|||
const prepareEsaggsForQueryGroups = async (
|
||||
queryGroups: QueryGroup[],
|
||||
interval: string,
|
||||
dataViews: DataViewsContract,
|
||||
aggs: AggsStart
|
||||
) => {
|
||||
const uniqueDataViewsToLoad = queryGroups
|
||||
.map((g) => g.dataView.value)
|
||||
.reduce<DataViewSpec[]>((acc, current) => {
|
||||
if (acc.find((el) => el.id === current.id)) return acc;
|
||||
return [...acc, current];
|
||||
}, []);
|
||||
|
||||
const loadedDataViews = await Promise.all(
|
||||
uniqueDataViewsToLoad.map((dataView) => dataViews.create(dataView, true))
|
||||
);
|
||||
|
||||
return queryGroups.map((group) => {
|
||||
const dataView = loadedDataViews.find((dv) => dv.id === group.dataView.value.id)!;
|
||||
|
||||
const annotationsFilters = {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
|
@ -260,9 +262,12 @@ const prepareEsaggsForQueryGroups = async (
|
|||
...fieldsTopMetric,
|
||||
];
|
||||
|
||||
const aggConfigs = aggs.createAggConfigs(dataView, aggregations?.map((agg) => agg.value) ?? []);
|
||||
const aggConfigs = aggs.createAggConfigs(
|
||||
group.dataView,
|
||||
aggregations?.map((agg) => agg.value) ?? []
|
||||
);
|
||||
return {
|
||||
esaggsParams: { dataView, aggConfigs, timeFields: [group.timeField] },
|
||||
esaggsParams: { dataView: group.dataView, aggConfigs, timeFields: [group.timeField] },
|
||||
fieldsColIdMap:
|
||||
group.allFields?.reduce<Record<string, string>>(
|
||||
(acc, fieldName, i) => ({
|
||||
|
@ -278,15 +283,12 @@ const prepareEsaggsForQueryGroups = async (
|
|||
|
||||
function regroupForRequestOptimization(
|
||||
{ groups }: FetchEventAnnotationsArgs,
|
||||
input: ExpressionValueSearchContext | null
|
||||
input: ExpressionValueSearchContext | null,
|
||||
loadedDataViews: DataView[]
|
||||
) {
|
||||
const outputGroups = groups
|
||||
.map((g) => {
|
||||
return g.annotations.reduce<Record<string, ManualGroup | QueryGroup>>((acc, current) => {
|
||||
if (current.isHidden) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (isManualAnnotation(current)) {
|
||||
if (!isInRange(current, input?.timeRange)) {
|
||||
return acc;
|
||||
|
@ -297,7 +299,14 @@ function regroupForRequestOptimization(
|
|||
(acc.manual as ManualGroup).annotations.push(current);
|
||||
return acc;
|
||||
} else {
|
||||
const key = `${g.dataView.value.id}-${current.timeField}`;
|
||||
const dataView = loadedDataViews.find((dv) => dv.id === g.dataView.value.id)!;
|
||||
|
||||
const timeField =
|
||||
current.timeField ??
|
||||
(dataView.timeFieldName ||
|
||||
dataView.fields.find((field) => field.type === 'date' && field.displayName)?.name);
|
||||
|
||||
const key = `${g.dataView.value.id}-${timeField}`;
|
||||
const subGroup = acc[key] as QueryGroup;
|
||||
if (subGroup) {
|
||||
let allFields = [...(subGroup.allFields || []), ...(current.extraFields || [])];
|
||||
|
@ -321,8 +330,8 @@ function regroupForRequestOptimization(
|
|||
...acc,
|
||||
[key]: {
|
||||
type: 'query',
|
||||
dataView: g.dataView,
|
||||
timeField: current.timeField,
|
||||
dataView,
|
||||
timeField: timeField!,
|
||||
allFields,
|
||||
annotations: [current],
|
||||
},
|
||||
|
|
|
@ -17,7 +17,9 @@ import { ExpressionFunctionDefinition, Datatable } from '@kbn/expressions-plugin
|
|||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { EventAnnotationGroupOutput } from '../event_annotation_group';
|
||||
|
||||
export type FetchEventAnnotationsOutput = Observable<Datatable | null>;
|
||||
export type FetchEventAnnotationsOutput = Observable<
|
||||
Datatable | { rows: never[]; columns: never[]; type: string }
|
||||
>;
|
||||
|
||||
export interface FetchEventAnnotationsArgs {
|
||||
groups: EventAnnotationGroupOutput[];
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { TimeRange } from '@kbn/data-plugin/common';
|
||||
import { TimeBuckets, TimeRange, UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { omit, pick } from 'lodash';
|
||||
import dateMath from '@kbn/datemath';
|
||||
import moment from 'moment';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import {
|
||||
ManualEventAnnotationOutput,
|
||||
ManualPointEventAnnotationOutput,
|
||||
|
@ -41,15 +43,70 @@ export const isManualAnnotation = (
|
|||
): annotation is ManualPointEventAnnotationOutput | ManualRangeEventAnnotationOutput =>
|
||||
isRangeAnnotation(annotation) || isManualPointAnnotation(annotation);
|
||||
|
||||
function toAbsoluteDate(date: string) {
|
||||
const parsed = dateMath.parse(date);
|
||||
return parsed ? parsed.toDate() : undefined;
|
||||
}
|
||||
|
||||
export function toAbsoluteDates(range: TimeRange) {
|
||||
const fromDate = dateMath.parse(range.from);
|
||||
const toDate = dateMath.parse(range.to, { roundUp: true });
|
||||
|
||||
if (!fromDate || !toDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
from: fromDate.toDate(),
|
||||
to: toDate.toDate(),
|
||||
};
|
||||
}
|
||||
|
||||
export const getCalculatedInterval = (
|
||||
uiSettings: IUiSettingsClient,
|
||||
usedInterval: string,
|
||||
timeRange?: TimeRange
|
||||
) => {
|
||||
const dates = timeRange && toAbsoluteDates(timeRange);
|
||||
if (!dates) {
|
||||
return;
|
||||
}
|
||||
const buckets = new TimeBuckets({
|
||||
'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
dateFormat: uiSettings.get('dateFormat'),
|
||||
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
|
||||
});
|
||||
|
||||
buckets.setInterval(usedInterval);
|
||||
buckets.setBounds({
|
||||
min: moment(dates.from),
|
||||
max: moment(dates.to),
|
||||
});
|
||||
|
||||
return buckets.getInterval().expression;
|
||||
};
|
||||
|
||||
export const isInRange = (annotation: ManualEventAnnotationOutput, timerange?: TimeRange) => {
|
||||
if (!timerange) {
|
||||
return false;
|
||||
}
|
||||
const { from, to } = toAbsoluteDates(timerange) || {};
|
||||
if (!from || !to) {
|
||||
return false;
|
||||
}
|
||||
if (isRangeAnnotation(annotation)) {
|
||||
return !(annotation.time >= timerange.to || annotation.endTime < timerange.from);
|
||||
const time = toAbsoluteDate(annotation.time);
|
||||
const endTime = toAbsoluteDate(annotation.endTime);
|
||||
if (time && endTime) {
|
||||
return !(time >= to || endTime < from);
|
||||
}
|
||||
}
|
||||
if (isManualPointAnnotation(annotation)) {
|
||||
return annotation.time >= timerange.from && annotation.time <= timerange.to;
|
||||
const time = toAbsoluteDate(annotation.time);
|
||||
if (time) {
|
||||
return time >= from && time <= to;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -12,13 +12,12 @@ export type {
|
|||
ManualRangeEventAnnotationArgs,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
ManualRangeEventAnnotationRow,
|
||||
ManualPointEventAnnotationRow,
|
||||
PointEventAnnotationRow,
|
||||
} from './manual_event_annotation/types';
|
||||
export type {
|
||||
QueryPointEventAnnotationArgs,
|
||||
QueryPointEventAnnotationOutput,
|
||||
} from './query_point_event_annotation/types';
|
||||
export type { EventAnnotationArgs, EventAnnotationOutput } from './types';
|
||||
export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation';
|
||||
export { queryPointEventAnnotation } from './query_point_event_annotation';
|
||||
export { eventAnnotationGroup } from './event_annotation_group';
|
||||
|
@ -27,8 +26,11 @@ export type { EventAnnotationGroupArgs } from './event_annotation_group';
|
|||
export type { FetchEventAnnotationsArgs } from './fetch_event_annotations/types';
|
||||
export type {
|
||||
EventAnnotationConfig,
|
||||
EventAnnotationGroupConfig,
|
||||
EventAnnotationArgs,
|
||||
RangeEventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
QueryPointEventAnnotationConfig,
|
||||
AvailableAnnotationIcon,
|
||||
EventAnnotationOutput,
|
||||
} from './types';
|
||||
|
|
|
@ -17,13 +17,14 @@ export type ManualPointEventAnnotationOutput = ManualPointEventAnnotationArgs &
|
|||
type: 'manual_point_event_annotation';
|
||||
};
|
||||
|
||||
export type ManualPointEventAnnotationRow = {
|
||||
export type PointEventAnnotationRow = {
|
||||
id: string;
|
||||
time: string;
|
||||
type: 'point';
|
||||
timebucket: string;
|
||||
skippedCount?: string;
|
||||
} & PointStyleProps;
|
||||
skippedCount?: number;
|
||||
} & PointStyleProps &
|
||||
Record<string, any>;
|
||||
|
||||
export type ManualRangeEventAnnotationArgs = {
|
||||
id: string;
|
||||
|
|
|
@ -38,7 +38,6 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition<
|
|||
help: i18n.translate('eventAnnotation.queryAnnotation.args.filter', {
|
||||
defaultMessage: `Annotation filter`,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
extraFields: {
|
||||
multi: true,
|
||||
|
@ -48,7 +47,6 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition<
|
|||
}),
|
||||
},
|
||||
timeField: {
|
||||
required: true,
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.timeField', {
|
||||
defaultMessage: `The time field of the annotation`,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { PointStyleProps } from '../types';
|
|||
export type QueryPointEventAnnotationArgs = {
|
||||
id: string;
|
||||
filter: KibanaQueryOutput;
|
||||
timeField: string;
|
||||
timeField?: string;
|
||||
extraFields?: string[];
|
||||
textField?: string;
|
||||
} & PointStyleProps;
|
||||
|
|
|
@ -22,35 +22,39 @@ import {
|
|||
|
||||
export type LineStyle = 'solid' | 'dashed' | 'dotted';
|
||||
export type Fill = 'inside' | 'outside' | 'none';
|
||||
export type AnnotationType = 'manual';
|
||||
export type ManualAnnotationType = 'manual';
|
||||
export type QueryAnnotationType = 'query';
|
||||
export type KeyType = 'point_in_time' | 'range';
|
||||
export type AvailableAnnotationIcon = $Values<typeof AvailableAnnotationIcons>;
|
||||
export interface PointStyleProps {
|
||||
|
||||
interface StyleSharedProps {
|
||||
label: string;
|
||||
color?: string;
|
||||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export type PointStyleProps = StyleSharedProps & {
|
||||
icon?: AvailableAnnotationIcon;
|
||||
lineWidth?: number;
|
||||
lineStyle?: LineStyle;
|
||||
textVisibility?: boolean;
|
||||
isHidden?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type PointInTimeEventAnnotationConfig = {
|
||||
id: string;
|
||||
type: ManualAnnotationType;
|
||||
key: {
|
||||
type: 'point_in_time';
|
||||
timestamp: string;
|
||||
};
|
||||
} & PointStyleProps;
|
||||
|
||||
export interface RangeStyleProps {
|
||||
label: string;
|
||||
color?: string;
|
||||
export type RangeStyleProps = StyleSharedProps & {
|
||||
outside?: boolean;
|
||||
isHidden?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type RangeEventAnnotationConfig = {
|
||||
type: ManualAnnotationType;
|
||||
id: string;
|
||||
key: {
|
||||
type: 'range';
|
||||
|
@ -63,9 +67,10 @@ export type StyleProps = PointStyleProps & RangeStyleProps;
|
|||
|
||||
export type QueryPointEventAnnotationConfig = {
|
||||
id: string;
|
||||
type: QueryAnnotationType;
|
||||
filter: KibanaQueryOutput;
|
||||
timeField: string;
|
||||
textField: string;
|
||||
timeField?: string;
|
||||
textField?: string;
|
||||
extraFields?: string[];
|
||||
key: {
|
||||
type: 'point_in_time';
|
||||
|
@ -77,6 +82,11 @@ export type EventAnnotationConfig =
|
|||
| RangeEventAnnotationConfig
|
||||
| QueryPointEventAnnotationConfig;
|
||||
|
||||
export interface EventAnnotationGroupConfig {
|
||||
annotations: EventAnnotationConfig[];
|
||||
indexPatternId: string;
|
||||
}
|
||||
|
||||
export type EventAnnotationArgs =
|
||||
| ManualPointEventAnnotationArgs
|
||||
| ManualRangeEventAnnotationArgs
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
QueryPointEventAnnotationConfig,
|
||||
} from '../../common';
|
||||
export const defaultAnnotationColor = euiLightVars.euiColorAccent;
|
||||
// Do not compute it live as dependencies will add tens of Kbs to the plugin
|
||||
export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1
|
||||
|
||||
export const defaultAnnotationLabel = i18n.translate(
|
||||
|
@ -38,5 +39,5 @@ export const isManualPointAnnotationConfig = (
|
|||
export const isQueryAnnotationConfig = (
|
||||
annotation?: EventAnnotationConfig
|
||||
): annotation is QueryPointEventAnnotationConfig => {
|
||||
return Boolean(annotation && 'filter' in annotation);
|
||||
return Boolean(annotation && annotation.type === 'query');
|
||||
};
|
||||
|
|
|
@ -0,0 +1,352 @@
|
|||
/*
|
||||
* 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 { getEventAnnotationService } from './service';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
|
||||
describe('Event Annotation Service', () => {
|
||||
let eventAnnotationService: EventAnnotationServiceType;
|
||||
beforeAll(() => {
|
||||
eventAnnotationService = getEventAnnotationService();
|
||||
});
|
||||
describe('toExpression', () => {
|
||||
it('should work for an empty list', () => {
|
||||
expect(eventAnnotationService.toExpression([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should skip hidden annotations', () => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: '2022',
|
||||
},
|
||||
label: 'Hello',
|
||||
isHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'myRangeEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: '2021',
|
||||
endTimestamp: '2022',
|
||||
},
|
||||
label: 'Hello Range',
|
||||
isHidden: true,
|
||||
},
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'query',
|
||||
timeField: '@timestamp',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
},
|
||||
label: 'Hello Range',
|
||||
isHidden: true,
|
||||
filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
},
|
||||
])
|
||||
).toEqual([]);
|
||||
});
|
||||
it('should process manual point annotations', () => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: '2022',
|
||||
},
|
||||
label: 'Hello',
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'manual_point_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
time: ['2022'],
|
||||
label: ['Hello'],
|
||||
color: ['#f04e98'],
|
||||
lineWidth: [1],
|
||||
lineStyle: ['solid'],
|
||||
icon: ['triangle'],
|
||||
textVisibility: [false],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should process manual range annotations', () => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: '2021',
|
||||
endTimestamp: '2022',
|
||||
},
|
||||
label: 'Hello',
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'manual_range_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
time: ['2021'],
|
||||
endTime: ['2022'],
|
||||
label: ['Hello'],
|
||||
color: ['#F04E981A'],
|
||||
outside: [false],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should process query based annotations', () => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'query',
|
||||
timeField: '@timestamp',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
},
|
||||
label: 'Hello',
|
||||
filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
timeField: ['@timestamp'],
|
||||
label: ['Hello'],
|
||||
color: ['#f04e98'],
|
||||
lineWidth: [1],
|
||||
lineStyle: ['solid'],
|
||||
icon: ['triangle'],
|
||||
textVisibility: [false],
|
||||
textField: [],
|
||||
filter: [
|
||||
{
|
||||
chain: [
|
||||
{
|
||||
arguments: {
|
||||
q: [''],
|
||||
},
|
||||
function: 'kql',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
type: 'expression',
|
||||
},
|
||||
],
|
||||
extraFields: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should process mixed annotations', () => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: '2022',
|
||||
},
|
||||
label: 'Hello',
|
||||
},
|
||||
{
|
||||
id: 'myRangeEvent',
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'range',
|
||||
timestamp: '2021',
|
||||
endTimestamp: '2022',
|
||||
},
|
||||
label: 'Hello Range',
|
||||
},
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'query',
|
||||
timeField: '@timestamp',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
},
|
||||
label: 'Hello',
|
||||
filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'manual_point_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
time: ['2022'],
|
||||
label: ['Hello'],
|
||||
color: ['#f04e98'],
|
||||
lineWidth: [1],
|
||||
lineStyle: ['solid'],
|
||||
icon: ['triangle'],
|
||||
textVisibility: [false],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'manual_range_event_annotation',
|
||||
arguments: {
|
||||
id: ['myRangeEvent'],
|
||||
time: ['2021'],
|
||||
endTime: ['2022'],
|
||||
label: ['Hello Range'],
|
||||
color: ['#F04E981A'],
|
||||
outside: [false],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
timeField: ['@timestamp'],
|
||||
label: ['Hello'],
|
||||
color: ['#f04e98'],
|
||||
lineWidth: [1],
|
||||
lineStyle: ['solid'],
|
||||
icon: ['triangle'],
|
||||
textVisibility: [false],
|
||||
textField: [],
|
||||
filter: [
|
||||
{
|
||||
chain: [
|
||||
{
|
||||
arguments: {
|
||||
q: [''],
|
||||
},
|
||||
function: 'kql',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
type: 'expression',
|
||||
},
|
||||
],
|
||||
extraFields: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it.each`
|
||||
textVisibility | textField | expected
|
||||
${'true'} | ${''} | ${''}
|
||||
${'false'} | ${''} | ${''}
|
||||
${'true'} | ${'myField'} | ${'myField'}
|
||||
${'false'} | ${''} | ${''}
|
||||
`(
|
||||
"should handle correctly textVisibility when set to '$textVisibility' and textField to '$textField'",
|
||||
({ textVisibility, textField, expected }) => {
|
||||
expect(
|
||||
eventAnnotationService.toExpression([
|
||||
{
|
||||
id: 'myEvent',
|
||||
type: 'query',
|
||||
timeField: '@timestamp',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
},
|
||||
label: 'Hello',
|
||||
filter: { type: 'kibana_query', query: '', language: 'kuery' },
|
||||
textVisibility,
|
||||
textField,
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: ['myEvent'],
|
||||
timeField: ['@timestamp'],
|
||||
label: ['Hello'],
|
||||
color: ['#f04e98'],
|
||||
lineWidth: [1],
|
||||
lineStyle: ['solid'],
|
||||
icon: ['triangle'],
|
||||
textVisibility: [textVisibility],
|
||||
textField: expected ? [expected] : [],
|
||||
filter: [
|
||||
{
|
||||
chain: [
|
||||
{
|
||||
arguments: {
|
||||
q: [''],
|
||||
},
|
||||
function: 'kql',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
type: 'expression',
|
||||
},
|
||||
],
|
||||
extraFields: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -6,38 +6,42 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { partition } from 'lodash';
|
||||
import { queryToAst } from '@kbn/data-plugin/common';
|
||||
import { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
|
||||
import { EventAnnotationConfig } from '../../common';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
defaultAnnotationLabel,
|
||||
isRangeAnnotationConfig,
|
||||
isQueryAnnotationConfig,
|
||||
} from './helpers';
|
||||
import { EventAnnotationConfig } from '../../common';
|
||||
import { RangeEventAnnotationConfig } from '../../common/types';
|
||||
|
||||
export function hasIcon(icon: string | undefined): icon is string {
|
||||
return icon != null && icon !== 'empty';
|
||||
}
|
||||
|
||||
const isRangeAnnotation = (
|
||||
annotation?: EventAnnotationConfig
|
||||
): annotation is RangeEventAnnotationConfig => {
|
||||
return Boolean(annotation && annotation?.key.type === 'range');
|
||||
};
|
||||
|
||||
export function getEventAnnotationService(): EventAnnotationServiceType {
|
||||
return {
|
||||
toExpression: (annotation) => {
|
||||
if (isRangeAnnotation(annotation)) {
|
||||
const { label, isHidden, color, key, outside, id } = annotation;
|
||||
const annotationsToExpression = (annotations: EventAnnotationConfig[]) => {
|
||||
const visibleAnnotations = annotations.filter(({ isHidden }) => !isHidden);
|
||||
const [queryBasedAnnotations, manualBasedAnnotations] = partition(
|
||||
visibleAnnotations,
|
||||
isQueryAnnotationConfig
|
||||
);
|
||||
|
||||
const expressions = [];
|
||||
|
||||
for (const annotation of manualBasedAnnotations) {
|
||||
if (isRangeAnnotationConfig(annotation)) {
|
||||
const { label, color, key, outside, id } = annotation;
|
||||
const { timestamp: time, endTimestamp: endTime } = key;
|
||||
return {
|
||||
type: 'expression',
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
type: 'function' as const,
|
||||
function: 'manual_range_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
|
@ -46,57 +50,17 @@ export function getEventAnnotationService(): EventAnnotationServiceType {
|
|||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationRangeColor],
|
||||
outside: [Boolean(outside)],
|
||||
isHidden: [Boolean(isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (isQueryAnnotationConfig(annotation)) {
|
||||
const {
|
||||
id,
|
||||
extraFields,
|
||||
label,
|
||||
isHidden,
|
||||
color,
|
||||
lineStyle,
|
||||
lineWidth,
|
||||
icon,
|
||||
filter,
|
||||
textVisibility,
|
||||
timeField,
|
||||
textField,
|
||||
} = annotation;
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
filter: filter ? [queryToAst(filter)] : [],
|
||||
timeField: [timeField],
|
||||
textField: [textField],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
isHidden: [Boolean(isHidden)],
|
||||
extraFields: extraFields || [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility, id } =
|
||||
annotation;
|
||||
return {
|
||||
type: 'expression',
|
||||
const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
type: 'function' as const,
|
||||
function: 'manual_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
|
@ -107,12 +71,106 @@ export function getEventAnnotationService(): EventAnnotationServiceType {
|
|||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
isHidden: [Boolean(isHidden)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const annotation of queryBasedAnnotations) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
color,
|
||||
lineStyle,
|
||||
lineWidth,
|
||||
icon,
|
||||
timeField,
|
||||
textVisibility,
|
||||
textField,
|
||||
filter,
|
||||
extraFields,
|
||||
} = annotation;
|
||||
expressions.push({
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
{
|
||||
type: 'function' as const,
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
id: [id],
|
||||
timeField: timeField ? [timeField] : [],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
textField: textVisibility && textField ? [textField] : [],
|
||||
filter: filter ? [queryToAst(filter)] : [],
|
||||
extraFields: extraFields || [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return expressions;
|
||||
};
|
||||
return {
|
||||
toExpression: annotationsToExpression,
|
||||
toFetchExpression: ({ interval, groups }) => {
|
||||
if (groups.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupsExpressions = groups
|
||||
.filter((g) => g.annotations.some((a) => !a.isHidden))
|
||||
.map(({ annotations, indexPatternId }): ExpressionAstExpression => {
|
||||
const indexPatternExpression: ExpressionAstExpression = {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'indexPatternLoad',
|
||||
arguments: {
|
||||
id: [indexPatternId],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const annotationExpressions = annotationsToExpression(annotations);
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'event_annotation_group',
|
||||
arguments: {
|
||||
dataView: [indexPatternExpression],
|
||||
annotations: [...annotationExpressions],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const fetchExpression: ExpressionAstExpression = {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'kibana', arguments: {} },
|
||||
{
|
||||
type: 'function',
|
||||
function: 'fetch_event_annotations',
|
||||
arguments: {
|
||||
interval: [interval],
|
||||
groups: [...groupsExpressions],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [fetchExpression];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,8 +7,12 @@
|
|||
*/
|
||||
|
||||
import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast';
|
||||
import { EventAnnotationConfig } from '../../common';
|
||||
import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../common';
|
||||
|
||||
export interface EventAnnotationServiceType {
|
||||
toExpression: (props: EventAnnotationConfig) => ExpressionAstExpression;
|
||||
toExpression: (props: EventAnnotationConfig[]) => ExpressionAstExpression[];
|
||||
toFetchExpression: (props: {
|
||||
interval: string;
|
||||
groups: EventAnnotationGroupConfig[];
|
||||
}) => ExpressionAstExpression[];
|
||||
}
|
||||
|
|
|
@ -37,6 +37,13 @@ Array [
|
|||
"timebucket": "2022-07-05T04:30:00.000Z",
|
||||
"type": "point",
|
||||
},
|
||||
Object {
|
||||
"id": "mann5",
|
||||
"isHidden": true,
|
||||
"time": "2022-07-05T05:55:00Z",
|
||||
"timebucket": "2022-07-05T05:30:00.000Z",
|
||||
"type": "point",
|
||||
},
|
||||
Object {
|
||||
"id": "mann1",
|
||||
"time": "2022-07-05T11:12:00Z",
|
||||
|
|
|
@ -285,12 +285,12 @@ describe('getFetchEventAnnotations', () => {
|
|||
(startServices[1].data.dataViews.create as jest.Mock).mockClear();
|
||||
(handleRequest as jest.Mock).mockClear();
|
||||
});
|
||||
test('Returns null for empty groups', async () => {
|
||||
test('Returns empty datatable for empty groups', async () => {
|
||||
const result = await runGetFetchEventAnnotations({
|
||||
interval: '2h',
|
||||
groups: [],
|
||||
});
|
||||
expect(result).toEqual(null);
|
||||
expect(result).toEqual({ columns: [], rows: [], type: 'datatable' });
|
||||
});
|
||||
|
||||
describe('Manual annotations', () => {
|
||||
|
@ -322,10 +322,6 @@ describe('getFetchEventAnnotations', () => {
|
|||
],
|
||||
} as unknown as FetchEventAnnotationsArgs;
|
||||
|
||||
test(`Doesn't run dataViews.create for manual annotations groups only`, async () => {
|
||||
await runGetFetchEventAnnotations(manualOnlyArgs);
|
||||
expect(startServices[1].data.dataViews.create).not.toHaveBeenCalled();
|
||||
});
|
||||
test('Sorts annotations by time, assigns correct timebuckets, filters out hidden and out of range annotations', async () => {
|
||||
const result = await runGetFetchEventAnnotations(manualOnlyArgs);
|
||||
expect(result!.rows).toMatchSnapshot();
|
||||
|
@ -340,7 +336,7 @@ describe('getFetchEventAnnotations', () => {
|
|||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [manualAnnotationSamples.point1],
|
||||
dataView1,
|
||||
dataView: dataView1,
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
|
|
|
@ -19,4 +19,5 @@ export {
|
|||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotationConfig,
|
||||
isManualPointAnnotationConfig,
|
||||
isQueryAnnotationConfig,
|
||||
} from './event_annotation_service/helpers';
|
||||
|
|
|
@ -31,7 +31,7 @@ import { SaveModalContainer, runSaveLensVisualization } from './save_modal_conta
|
|||
import { LensInspector } from '../lens_inspector_service';
|
||||
import { getEditPath } from '../../common';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
import { IndexPatternServiceAPI, createIndexPatternService } from '../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI, createIndexPatternService } from '../data_views_service/service';
|
||||
import { replaceIndexpattern } from '../state_management/lens_slice';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
|
@ -403,6 +403,7 @@ export function App({
|
|||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
indicateNoData={indicateNoData}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
title={persistedDoc?.title}
|
||||
lensInspector={lensInspector}
|
||||
currentDoc={currentDoc}
|
||||
|
|
|
@ -210,6 +210,7 @@ export const LensTopNavMenu = ({
|
|||
onAppLeave,
|
||||
redirectToOrigin,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
title,
|
||||
goBackToOriginatingApp,
|
||||
contextOriginatingApp,
|
||||
|
@ -305,6 +306,10 @@ export const LensTopNavMenu = ({
|
|||
{}
|
||||
),
|
||||
datasourceStates,
|
||||
visualizationState: visualization.state,
|
||||
activeVisualization: visualization.activeId
|
||||
? visualizationMap[visualization.activeId]
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
// Add ad-hoc data views from the Lens state even if they are not used
|
||||
|
@ -337,6 +342,8 @@ export const LensTopNavMenu = ({
|
|||
activeDatasourceId,
|
||||
rejectedIndexPatterns,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
visualization,
|
||||
indexPatterns,
|
||||
dataViewsService,
|
||||
dataViews,
|
||||
|
|
|
@ -52,7 +52,7 @@ import type {
|
|||
import type { LensAttributeService } from '../lens_attribute_service';
|
||||
import type { LensEmbeddableInput } from '../embeddable/embeddable';
|
||||
import type { LensInspector } from '../lens_inspector_service';
|
||||
import { IndexPatternServiceAPI } from '../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from '../data_views_service/service';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
|
||||
export interface RedirectToOriginProps {
|
||||
|
@ -106,6 +106,7 @@ export interface LensTopNavMenuProps {
|
|||
setIsSaveModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
runSave: RunSave;
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
title?: string;
|
||||
lensInspector: LensInspector;
|
||||
goBackToOriginatingApp?: () => void;
|
||||
|
|
|
@ -11,22 +11,38 @@ import {
|
|||
createMockedIndexPattern,
|
||||
createMockedRestrictedIndexPattern,
|
||||
} from '../indexpattern_datasource/mocks';
|
||||
import { IndexPattern } from '../types';
|
||||
import { DataViewsState } from '../state_management';
|
||||
import { ExistingFieldsMap, IndexPattern } from '../types';
|
||||
import { getFieldByNameFactory } from './loader';
|
||||
|
||||
export function loadInitialDataViews() {
|
||||
const indexPattern = createMockedIndexPattern();
|
||||
const restricted = createMockedRestrictedIndexPattern();
|
||||
/**
|
||||
* Create a DataViewState from partial parameters, and infer the rest from the passed one.
|
||||
* Passing no parameter will return an empty state.
|
||||
*/
|
||||
export const createMockDataViewsState = ({
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
isFirstExistenceFetch,
|
||||
existingFields,
|
||||
}: Partial<DataViewsState> = {}): DataViewsState => {
|
||||
const refs =
|
||||
indexPatternRefs ??
|
||||
Object.values(indexPatterns ?? {}).map(({ id, title, name }) => ({ id, title, name }));
|
||||
const allFields =
|
||||
existingFields ??
|
||||
refs.reduce((acc, { id, title }) => {
|
||||
if (indexPatterns && id in indexPatterns) {
|
||||
acc[title] = Object.fromEntries(indexPatterns[id].fields.map((f) => [f.displayName, true]));
|
||||
}
|
||||
return acc;
|
||||
}, {} as ExistingFieldsMap);
|
||||
return {
|
||||
indexPatternRefs: [],
|
||||
existingFields: {},
|
||||
indexPatterns: {
|
||||
[indexPattern.id]: indexPattern,
|
||||
[restricted.id]: restricted,
|
||||
},
|
||||
isFirstExistenceFetch: false,
|
||||
indexPatterns: indexPatterns ?? {},
|
||||
indexPatternRefs: refs,
|
||||
isFirstExistenceFetch: Boolean(isFirstExistenceFetch),
|
||||
existingFields: allFields,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const createMockStorage = (lastData?: Record<string, string>) => {
|
||||
return {
|
|
@ -196,9 +196,9 @@ export interface OnVisDropProps<T> {
|
|||
group?: VisualizationDimensionGroupConfig;
|
||||
}
|
||||
|
||||
export function onDropForVisualization<T>(
|
||||
export function onDropForVisualization<T, P = unknown>(
|
||||
props: OnVisDropProps<T>,
|
||||
activeVisualization: Visualization<T>
|
||||
activeVisualization: Visualization<T, P>
|
||||
) {
|
||||
const { prevState, target, frame, dropType, source, group } = props;
|
||||
const { layerId, columnId, groupId } = target;
|
||||
|
|
|
@ -29,7 +29,8 @@ function fromExcludedClickTarget(event: Event) {
|
|||
) {
|
||||
if (
|
||||
node.classList!.contains(DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS) ||
|
||||
node.classList!.contains('euiBody-hasPortalContent')
|
||||
node.classList!.contains('euiBody-hasPortalContent') ||
|
||||
node.getAttribute('data-euiportal') === 'true'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPatternServiceAPI } from '../../../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
import { NativeRenderer } from '../../../native_renderer';
|
||||
import {
|
||||
StateSetter,
|
||||
|
@ -524,8 +524,13 @@ export function LayerPanel(
|
|||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
invalid: group.invalid,
|
||||
invalidMessage: group.invalidMessage,
|
||||
...(activeVisualization?.validateColumn?.(
|
||||
visualizationState,
|
||||
{ dataViews },
|
||||
layerId,
|
||||
columnId,
|
||||
group
|
||||
) || { invalid: false }),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { IndexPatternServiceAPI } from '../../../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from '../../../data_views_service/service';
|
||||
|
||||
import {
|
||||
Visualization,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { Datasource, DatasourceDataPanelProps } from '../../types';
|
||||
import { Datasource, DatasourceDataPanelProps, VisualizationMap } from '../../types';
|
||||
import { DragDropIdentifier } from '../../drag_drop';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { createMockFramePublicAPI, mockStoreDeps, mountWithProvider } from '../../mocks';
|
||||
|
@ -26,12 +26,14 @@ describe('Data Panel Wrapper', () => {
|
|||
const datasourceMap = {
|
||||
activeDatasource: {
|
||||
renderDataPanel,
|
||||
getUsedDataViews: jest.fn(),
|
||||
} as unknown as Datasource,
|
||||
};
|
||||
|
||||
const mountResult = await mountWithProvider(
|
||||
<DataPanelWrapper
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={{} as VisualizationMap}
|
||||
showNoDataPopover={() => {}}
|
||||
core={{} as DatasourceDataPanelProps['core']}
|
||||
dropOntoWorkspace={(field: DragDropIdentifier) => {}}
|
||||
|
|
|
@ -16,7 +16,13 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
|||
import { Easteregg } from './easteregg';
|
||||
import { NativeRenderer } from '../../native_renderer';
|
||||
import { DragContext, DragDropIdentifier } from '../../drag_drop';
|
||||
import { StateSetter, DatasourceDataPanelProps, DatasourceMap, FramePublicAPI } from '../../types';
|
||||
import {
|
||||
StateSetter,
|
||||
DatasourceDataPanelProps,
|
||||
DatasourceMap,
|
||||
FramePublicAPI,
|
||||
VisualizationMap,
|
||||
} from '../../types';
|
||||
import {
|
||||
switchDatasource,
|
||||
useLensDispatch,
|
||||
|
@ -27,14 +33,16 @@ import {
|
|||
selectExecutionContext,
|
||||
selectActiveDatasourceId,
|
||||
selectDatasourceStates,
|
||||
selectVisualizationState,
|
||||
} from '../../state_management';
|
||||
import { initializeSources } from './state_helpers';
|
||||
import type { IndexPatternServiceAPI } from '../../indexpattern_service/service';
|
||||
import type { IndexPatternServiceAPI } from '../../data_views_service/service';
|
||||
import { changeIndexPattern } from '../../state_management/lens_slice';
|
||||
import { getInitialDataViewsObject } from '../../utils';
|
||||
|
||||
interface DataPanelWrapperProps {
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
showNoDataPopover: () => void;
|
||||
core: DatasourceDataPanelProps['core'];
|
||||
dropOntoWorkspace: (field: DragDropIdentifier) => void;
|
||||
|
@ -48,6 +56,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
const externalContext = useLensSelector(selectExecutionContext);
|
||||
const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
|
||||
const datasourceStates = useLensSelector(selectDatasourceStates);
|
||||
const visualizationState = useLensSelector(selectVisualizationState);
|
||||
|
||||
const datasourceIsLoading = activeDatasourceId
|
||||
? datasourceStates[activeDatasourceId].isLoading
|
||||
|
@ -74,6 +83,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
initializeSources(
|
||||
{
|
||||
datasourceMap: props.datasourceMap,
|
||||
visualizationMap: props.visualizationMap,
|
||||
visualizationState,
|
||||
datasourceStates,
|
||||
dataViews: props.plugins.dataViews,
|
||||
references: undefined,
|
||||
|
@ -84,29 +95,38 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
{
|
||||
isFullEditor: true,
|
||||
}
|
||||
).then(({ states, indexPatterns, indexPatternRefs }) => {
|
||||
const newDatasourceStates = Object.entries(states).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
dispatchLens(
|
||||
setState({
|
||||
datasourceStates: newDatasourceStates,
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
})
|
||||
);
|
||||
});
|
||||
).then(
|
||||
({
|
||||
datasourceStates: newDatasourceStates,
|
||||
visualizationState: newVizState,
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
}) => {
|
||||
dispatchLens(
|
||||
setState({
|
||||
visualization: { ...visualizationState, state: newVizState },
|
||||
datasourceStates: Object.entries(newDatasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
...datasourceState,
|
||||
isLoading: false,
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [
|
||||
datasourceStates,
|
||||
visualizationState,
|
||||
activeDatasourceId,
|
||||
props.datasourceMap,
|
||||
props.visualizationMap,
|
||||
dispatchLens,
|
||||
props.plugins.dataViews,
|
||||
props.core.uiSettings,
|
||||
|
@ -145,6 +165,19 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
onChangeIndexPattern,
|
||||
indexPatternService: props.indexPatternService,
|
||||
frame: props.frame,
|
||||
// Visualization can handle dataViews, so need to pass to the data panel the full list of used dataViews
|
||||
usedIndexPatterns: [
|
||||
...((activeDatasourceId &&
|
||||
props.datasourceMap[activeDatasourceId]?.getUsedDataViews(
|
||||
datasourceStates[activeDatasourceId].state
|
||||
)) ||
|
||||
[]),
|
||||
...((visualizationState.activeId &&
|
||||
props.visualizationMap[visualizationState.activeId]?.getUsedDataViews?.(
|
||||
visualizationState.state
|
||||
)) ||
|
||||
[]),
|
||||
],
|
||||
};
|
||||
|
||||
const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false);
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from '../../state_management';
|
||||
import type { LensInspector } from '../../lens_inspector_service';
|
||||
import { ErrorBoundary, showMemoizedErrorNotification } from '../../lens_ui_errors';
|
||||
import { IndexPatternServiceAPI } from '../../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from '../../data_views_service/service';
|
||||
|
||||
export interface EditorFrameProps {
|
||||
datasourceMap: DatasourceMap;
|
||||
|
@ -120,6 +120,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
core={props.core}
|
||||
plugins={props.plugins}
|
||||
datasourceMap={datasourceMap}
|
||||
visualizationMap={visualizationMap}
|
||||
showNoDataPopover={props.showNoDataPopover}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
|
|
|
@ -37,9 +37,9 @@ import {
|
|||
getMissingVisualizationTypeError,
|
||||
getUnknownVisualizationTypeError,
|
||||
} from '../error_helper';
|
||||
import type { DatasourceStates, DataViewsState } from '../../state_management';
|
||||
import type { DatasourceStates, DataViewsState, VisualizationState } from '../../state_management';
|
||||
import { readFromStorage } from '../../settings_storage';
|
||||
import { loadIndexPatternRefs, loadIndexPatterns } from '../../indexpattern_service/loader';
|
||||
import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader';
|
||||
|
||||
function getIndexPatterns(
|
||||
references?: SavedObjectReference[],
|
||||
|
@ -158,6 +158,8 @@ export async function initializeSources(
|
|||
{
|
||||
dataViews,
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
datasourceStates,
|
||||
storage,
|
||||
defaultIndexPatternId,
|
||||
|
@ -167,6 +169,8 @@ export async function initializeSources(
|
|||
}: {
|
||||
dataViews: DataViewsContract;
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
visualizationState: VisualizationState;
|
||||
datasourceStates: DatasourceStates;
|
||||
defaultIndexPatternId: string;
|
||||
storage: IStorageWrapper;
|
||||
|
@ -192,7 +196,7 @@ export async function initializeSources(
|
|||
return {
|
||||
indexPatterns,
|
||||
indexPatternRefs,
|
||||
states: initializeDatasources({
|
||||
datasourceStates: initializeDatasources({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
initialContext,
|
||||
|
@ -200,9 +204,34 @@ export async function initializeSources(
|
|||
indexPatterns,
|
||||
references,
|
||||
}),
|
||||
visualizationState: initializeVisualization({
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
references,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function initializeVisualization({
|
||||
visualizationMap,
|
||||
visualizationState,
|
||||
references,
|
||||
}: {
|
||||
visualizationState: VisualizationState;
|
||||
visualizationMap: VisualizationMap;
|
||||
references?: SavedObjectReference[];
|
||||
}) {
|
||||
if (visualizationState?.activeId) {
|
||||
return (
|
||||
visualizationMap[visualizationState.activeId]?.fromPersistableState?.(
|
||||
visualizationState.state,
|
||||
references
|
||||
) ?? visualizationState.state
|
||||
);
|
||||
}
|
||||
return visualizationState.state;
|
||||
}
|
||||
|
||||
export function initializeDatasources({
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
|
@ -271,7 +300,7 @@ export async function persistedStateToExpression(
|
|||
): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> {
|
||||
const {
|
||||
state: {
|
||||
visualization: visualizationState,
|
||||
visualization: persistedVisualizationState,
|
||||
datasourceStates: persistedDatasourceStates,
|
||||
adHocDataViews,
|
||||
internalReferences,
|
||||
|
@ -294,6 +323,14 @@ export async function persistedStateToExpression(
|
|||
};
|
||||
}
|
||||
const visualization = visualizations[visualizationType!];
|
||||
const visualizationState = initializeVisualization({
|
||||
visualizationMap: visualizations,
|
||||
visualizationState: {
|
||||
state: persistedVisualizationState,
|
||||
activeId: visualizationType,
|
||||
},
|
||||
references: [...references, ...(internalReferences || [])],
|
||||
});
|
||||
const datasourceStatesFromSO = Object.fromEntries(
|
||||
Object.entries(persistedDatasourceStates).map(([id, state]) => [
|
||||
id,
|
||||
|
@ -404,15 +441,15 @@ export const validateDatasourceAndVisualization = (
|
|||
currentDatasourceState: unknown | null,
|
||||
currentVisualization: Visualization | null,
|
||||
currentVisualizationState: unknown | undefined,
|
||||
{ datasourceLayers, dataViews }: Pick<FramePublicAPI, 'datasourceLayers' | 'dataViews'>
|
||||
frame: Pick<FramePublicAPI, 'datasourceLayers' | 'dataViews'>
|
||||
): ErrorMessage[] | undefined => {
|
||||
try {
|
||||
const datasourceValidationErrors = currentDatasourceState
|
||||
? currentDataSource?.getErrorMessages(currentDatasourceState, dataViews.indexPatterns)
|
||||
? currentDataSource?.getErrorMessages(currentDatasourceState, frame.dataViews.indexPatterns)
|
||||
: undefined;
|
||||
|
||||
const visualizationValidationErrors = currentVisualizationState
|
||||
? currentVisualization?.getErrorMessages(currentVisualizationState, datasourceLayers)
|
||||
? currentVisualization?.getErrorMessages(currentVisualizationState, frame)
|
||||
: undefined;
|
||||
|
||||
if (datasourceValidationErrors?.length || visualizationValidationErrors?.length) {
|
||||
|
|
|
@ -243,6 +243,7 @@ export function SuggestionPanel({
|
|||
visualizationMap[visualizationId],
|
||||
suggestionVisualizationState,
|
||||
{
|
||||
...frame,
|
||||
dataViews: frame.dataViews,
|
||||
datasourceLayers: getDatasourceLayers(
|
||||
suggestionDatasourceId
|
||||
|
|
|
@ -81,7 +81,7 @@ import { getLensInspectorService, LensInspector } from '../lens_inspector_servic
|
|||
import { SharingSavedObjectProps, VisualizationDisplayOptions } from '../types';
|
||||
import { getActiveDatasourceIdFromDoc, getIndexPatternsObjects, inferTimeField } from '../utils';
|
||||
import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../indexpattern_service/loader';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../data_views_service/loader';
|
||||
|
||||
export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>;
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { createIndexPatternServiceMock } from '../mocks/data_views_service_mock'
|
|||
import { createMockFramePublicAPI } from '../mocks';
|
||||
import { DataViewsState } from '../state_management';
|
||||
import { ExistingFieldsMap, FramePublicAPI, IndexPattern } from '../types';
|
||||
import { IndexPatternServiceProps } from '../indexpattern_service/service';
|
||||
import { IndexPatternServiceProps } from '../data_views_service/service';
|
||||
import { FieldSpec, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
|
||||
|
@ -408,7 +408,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
stateChanges?: Partial<IndexPatternPrivateState>,
|
||||
propChanges?: Partial<Props>
|
||||
) {
|
||||
const inst = mountWithIntl(<IndexPatternDataPanel {...props} />);
|
||||
const inst = mountWithIntl<Props>(<IndexPatternDataPanel {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
inst.update();
|
||||
|
@ -418,10 +418,10 @@ describe('IndexPattern Data Panel', () => {
|
|||
await act(async () => {
|
||||
inst.setProps({
|
||||
...props,
|
||||
...((propChanges as object) || {}),
|
||||
...(propChanges || {}),
|
||||
state: {
|
||||
...props.state,
|
||||
...((stateChanges as object) || {}),
|
||||
...(stateChanges || {}),
|
||||
},
|
||||
});
|
||||
inst.update();
|
||||
|
|
|
@ -47,7 +47,7 @@ import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon
|
|||
import { getFieldType } from './pure_utils';
|
||||
import { FieldGroups, FieldList } from './field_list';
|
||||
import { fieldContainsData, fieldExists } from '../shared_components';
|
||||
import { IndexPatternServiceAPI } from '../indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from '../data_views_service/service';
|
||||
|
||||
export type Props = Omit<
|
||||
DatasourceDataPanelProps<IndexPatternPrivateState>,
|
||||
|
@ -143,15 +143,16 @@ export function IndexPatternDataPanel({
|
|||
indexPatternService,
|
||||
frame,
|
||||
onIndexPatternRefresh,
|
||||
usedIndexPatterns,
|
||||
}: Props) {
|
||||
const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } =
|
||||
frame.dataViews;
|
||||
const { currentIndexPatternId } = state;
|
||||
|
||||
const indexPatternList = uniq(
|
||||
Object.values(state.layers)
|
||||
.map((l) => l.indexPatternId)
|
||||
.concat(currentIndexPatternId)
|
||||
(
|
||||
usedIndexPatterns ?? Object.values(state.layers).map(({ indexPatternId }) => indexPatternId)
|
||||
).concat(currentIndexPatternId)
|
||||
)
|
||||
.filter((id) => !!indexPatterns[id])
|
||||
.sort()
|
||||
|
@ -283,7 +284,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
onIndexPatternRefresh,
|
||||
}: Omit<
|
||||
DatasourceDataPanelProps,
|
||||
'state' | 'setState' | 'showNoDataPopover' | 'core' | 'onChangeIndexPattern'
|
||||
'state' | 'setState' | 'showNoDataPopover' | 'core' | 'onChangeIndexPattern' | 'usedIndexPatterns'
|
||||
> & {
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
|
|
|
@ -55,8 +55,8 @@ jest.mock('./reference_editor', () => ({
|
|||
ReferenceEditor: () => null,
|
||||
}));
|
||||
jest.mock('../loader');
|
||||
jest.mock('../query_input', () => ({
|
||||
QueryInput: () => null,
|
||||
jest.mock('@kbn/unified-search-plugin/public', () => ({
|
||||
QueryStringInput: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('../operations');
|
||||
|
|
|
@ -19,10 +19,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { GenericIndexPatternColumn, operationDefinitionMap } from '../operations';
|
||||
import { validateQuery } from '../operations/definitions/filters';
|
||||
import { QueryInput } from '../query_input';
|
||||
import type { IndexPatternLayer } from '../types';
|
||||
import { useDebouncedValue } from '../../shared_components';
|
||||
import { QueryInput, useDebouncedValue, validateQuery } from '../../shared_components';
|
||||
import type { IndexPattern } from '../../types';
|
||||
|
||||
const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', {
|
||||
|
|
|
@ -110,6 +110,9 @@ export function columnToOperation(
|
|||
? 'version'
|
||||
: undefined,
|
||||
hasTimeShift: Boolean(timeShift),
|
||||
interval: isColumnOfType<DateHistogramIndexPatternColumn>('date_histogram', column)
|
||||
? column.params.interval
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -751,6 +754,9 @@ export function getIndexPatternDatasource({
|
|||
getUsedDataView: (state: IndexPatternPrivateState, layerId: string) => {
|
||||
return state.layers[layerId].indexPatternId;
|
||||
},
|
||||
getUsedDataViews: (state) => {
|
||||
return Object.values(state.layers).map(({ indexPatternId }) => indexPatternId);
|
||||
},
|
||||
};
|
||||
|
||||
return indexPatternDatasource;
|
||||
|
|
|
@ -2214,6 +2214,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2225,6 +2226,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2289,6 +2291,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2300,6 +2303,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2311,6 +2315,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2396,6 +2401,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2407,6 +2413,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2418,6 +2425,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ratio',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -2526,6 +2534,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'ordinal',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2537,6 +2546,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2548,6 +2558,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -3126,6 +3137,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: 'interval',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3137,6 +3149,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3148,6 +3161,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -3213,6 +3227,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: 'auto',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3224,6 +3239,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3235,6 +3251,7 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
scale: undefined,
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
interval: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from './loader';
|
||||
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
|
||||
import { DateHistogramIndexPatternColumn, TermsIndexPatternColumn } from './operations';
|
||||
import { sampleIndexPatterns } from '../indexpattern_service/mocks';
|
||||
import { sampleIndexPatterns } from '../data_views_service/mocks';
|
||||
|
||||
const createMockStorage = (lastData?: Record<string, string>) => {
|
||||
return {
|
||||
|
|
|
@ -12,8 +12,8 @@ import { EuiPopover, EuiLink } from '@elastic/eui';
|
|||
import { createMockedIndexPattern } from '../../../mocks';
|
||||
import { FilterPopover } from './filter_popover';
|
||||
import { LabelInput } from '../shared_components';
|
||||
import { QueryInput } from '../../../query_input';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import { QueryInput } from '../../../../shared_components';
|
||||
|
||||
jest.mock('.', () => ({
|
||||
isQueryValid: () => true,
|
||||
|
|
|
@ -10,10 +10,12 @@ import './filter_popover.scss';
|
|||
import React from 'react';
|
||||
import { EuiPopover, EuiSpacer } from '@elastic/eui';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
// Need to keep it separate to make it work Jest mocks in dimension_panel tests
|
||||
// import { QueryInput } from '../../../../shared_components/query_input';
|
||||
import { isQueryValid, QueryInput } from '../../../../shared_components';
|
||||
import { IndexPattern } from '../../../../types';
|
||||
import { FilterValue, defaultLabel, isQueryValid } from '.';
|
||||
import { FilterValue, defaultLabel } from '.';
|
||||
import { LabelInput } from '../shared_components';
|
||||
import { QueryInput } from '../../../query_input';
|
||||
|
||||
export const FilterPopover = ({
|
||||
filter,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import './filters.scss';
|
||||
import React, { useState } from 'react';
|
||||
import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { omit } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui';
|
||||
|
@ -15,12 +14,17 @@ import type { Query } from '@kbn/es-query';
|
|||
import type { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { queryFilterToAst } from '@kbn/data-plugin/common';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import {
|
||||
DragDropBuckets,
|
||||
DraggableBucketContainer,
|
||||
isQueryValid,
|
||||
NewBucketButton,
|
||||
} from '../../../../shared_components';
|
||||
import { IndexPattern } from '../../../../types';
|
||||
import { updateColumnParam } from '../../layer_helpers';
|
||||
import type { OperationDefinition } from '..';
|
||||
import type { BaseIndexPatternColumn } from '../column_types';
|
||||
import { FilterPopover } from './filter_popover';
|
||||
import { DragDropBuckets, DraggableBucketContainer, NewBucketButton } from '../shared_components';
|
||||
|
||||
const generateId = htmlIdGenerator();
|
||||
const OPERATION_NAME = 'filters';
|
||||
|
@ -54,29 +58,6 @@ const defaultFilter: Filter = {
|
|||
label: '',
|
||||
};
|
||||
|
||||
export const validateQuery = (input: Query | undefined, indexPattern: IndexPattern) => {
|
||||
let isValid = true;
|
||||
let error: string | undefined;
|
||||
|
||||
try {
|
||||
if (input) {
|
||||
if (input.language === 'kuery') {
|
||||
toElasticsearchQuery(fromKueryExpression(input.query), indexPattern);
|
||||
} else {
|
||||
luceneStringToDsl(input.query);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
return { isValid, error };
|
||||
};
|
||||
|
||||
export const isQueryValid = (input: Query, indexPattern: IndexPattern) =>
|
||||
validateQuery(input, indexPattern).isValid;
|
||||
|
||||
export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn {
|
||||
operationType: typeof OPERATION_NAME;
|
||||
params: {
|
||||
|
|
|
@ -10,13 +10,13 @@ import { createFormulaPublicApi, FormulaPublicApi } from './formula_public_api';
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DateHistogramIndexPatternColumn, PersistedIndexPatternLayer } from '../../../types';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../indexpattern_service/loader';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../data_views_service/loader';
|
||||
|
||||
jest.mock('./parse', () => ({
|
||||
insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../indexpattern_service/loader', () => ({
|
||||
jest.mock('../../../../data_views_service/loader', () => ({
|
||||
convertDataViewIntoLensIndexPattern: jest.fn((v) => v),
|
||||
}));
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../indexpattern_service/loader';
|
||||
import { convertDataViewIntoLensIndexPattern } from '../../../../data_views_service/loader';
|
||||
import type { IndexPattern } from '../../../../types';
|
||||
import type { PersistedIndexPatternLayer } from '../../../types';
|
||||
|
||||
|
|
|
@ -23,15 +23,15 @@ import {
|
|||
keys,
|
||||
} from '@elastic/eui';
|
||||
import { IFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { useDebounceWithOptions } from '../../../../shared_components';
|
||||
import { RangeTypeLens, isValidRange } from './ranges';
|
||||
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
|
||||
import {
|
||||
NewBucketButton,
|
||||
DragDropBuckets,
|
||||
DraggableBucketContainer,
|
||||
LabelInput,
|
||||
} from '../shared_components';
|
||||
NewBucketButton,
|
||||
useDebounceWithOptions,
|
||||
} from '../../../../shared_components';
|
||||
import { RangeTypeLens, isValidRange } from './ranges';
|
||||
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
|
||||
import { LabelInput } from '../shared_components';
|
||||
import { isValidNumber } from '../helpers';
|
||||
|
||||
const generateId = htmlIdGenerator();
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
SLICES,
|
||||
} from './constants';
|
||||
import { RangePopover } from './advanced_editor';
|
||||
import { DragDropBuckets } from '../shared_components';
|
||||
import { DragDropBuckets } from '../../../../shared_components';
|
||||
import { getFieldByNameFactory } from '../../../pure_helpers';
|
||||
import { IndexPattern } from '../../../../types';
|
||||
|
||||
|
|
|
@ -4,6 +4,5 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export * from './buckets';
|
||||
export * from './label_input';
|
||||
export * from './form_row';
|
||||
|
|
|
@ -18,12 +18,16 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExistingFieldsMap, IndexPattern } from '../../../../types';
|
||||
import { TooltipWrapper, useDebouncedValue } from '../../../../shared_components';
|
||||
import {
|
||||
DragDropBuckets,
|
||||
NewBucketButton,
|
||||
TooltipWrapper,
|
||||
useDebouncedValue,
|
||||
} from '../../../../shared_components';
|
||||
import { FieldSelect } from '../../../dimension_panel/field_select';
|
||||
import type { TermsIndexPatternColumn } from './types';
|
||||
import type { OperationSupportMatrix } from '../../../dimension_panel';
|
||||
import { supportedTypes } from './constants';
|
||||
import { DragDropBuckets, NewBucketButton } from '../shared_components';
|
||||
|
||||
const generateId = htmlIdGenerator();
|
||||
export const MAX_MULTI_FIELDS_SIZE = 3;
|
||||
|
|
|
@ -38,12 +38,13 @@ import {
|
|||
} from './operations';
|
||||
|
||||
import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers';
|
||||
import { FiltersIndexPatternColumn, isQueryValid } from './operations/definitions/filters';
|
||||
import { FiltersIndexPatternColumn } from './operations/definitions/filters';
|
||||
import { hasField } from './pure_utils';
|
||||
import { mergeLayer } from './state_helpers';
|
||||
import { supportsRarityRanking } from './operations/definitions/terms';
|
||||
import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms/constants';
|
||||
import { getOriginalId } from '../../common/expressions';
|
||||
import { isQueryValid } from '../shared_components';
|
||||
|
||||
export function isColumnInvalid(
|
||||
layer: IndexPatternLayer,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
createIndexPatternService,
|
||||
IndexPatternServiceProps,
|
||||
IndexPatternServiceAPI,
|
||||
} from '../indexpattern_service/service';
|
||||
} from '../data_views_service/service';
|
||||
|
||||
export function createIndexPatternServiceMock({
|
||||
core = coreMock.createStart(),
|
||||
|
|
|
@ -61,6 +61,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
isValidColumn: jest.fn(),
|
||||
isEqual: jest.fn(),
|
||||
getUsedDataView: jest.fn(),
|
||||
getUsedDataViews: jest.fn(),
|
||||
onRefreshIndexPattern: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createMockDataViewsState } from '../data_views_service/mocks';
|
||||
import { FramePublicAPI, FrameDatasourceAPI } from '../types';
|
||||
export { mockDataPlugin } from './data_plugin_mock';
|
||||
export {
|
||||
|
@ -29,28 +30,36 @@ export { lensPluginMock } from './lens_plugin_mock';
|
|||
|
||||
export type FrameMock = jest.Mocked<FramePublicAPI>;
|
||||
|
||||
export const createMockFramePublicAPI = (): FrameMock => ({
|
||||
datasourceLayers: {},
|
||||
dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' },
|
||||
dataViews: {
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
isFirstExistenceFetch: true,
|
||||
existingFields: {},
|
||||
export const createMockFramePublicAPI = ({
|
||||
datasourceLayers,
|
||||
dateRange,
|
||||
dataViews,
|
||||
activeData,
|
||||
}: Partial<FramePublicAPI> = {}): FrameMock => ({
|
||||
datasourceLayers: datasourceLayers ?? {},
|
||||
dateRange: dateRange ?? {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
toDate: '2022-04-17T08:25:00.000Z',
|
||||
},
|
||||
dataViews: createMockDataViewsState(dataViews),
|
||||
activeData,
|
||||
});
|
||||
|
||||
export type FrameDatasourceMock = jest.Mocked<FrameDatasourceAPI>;
|
||||
|
||||
export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({
|
||||
datasourceLayers: {},
|
||||
dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
dataViews: {
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
isFirstExistenceFetch: true,
|
||||
existingFields: {},
|
||||
export const createMockFrameDatasourceAPI = ({
|
||||
datasourceLayers,
|
||||
dateRange,
|
||||
dataViews,
|
||||
query,
|
||||
filters,
|
||||
}: Partial<FrameDatasourceAPI> = {}): FrameDatasourceMock => ({
|
||||
datasourceLayers: datasourceLayers ?? {},
|
||||
dateRange: dateRange ?? {
|
||||
fromDate: '2022-03-17T08:25:00.000Z',
|
||||
toDate: '2022-04-17T08:25:00.000Z',
|
||||
},
|
||||
query: query ?? { query: '', language: 'lucene' },
|
||||
filters: filters ?? [],
|
||||
dataViews: createMockDataViewsState(dataViews),
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Visualization, VisualizationMap } from '../types';
|
|||
export function createMockVisualization(id = 'testVis'): jest.Mocked<Visualization> {
|
||||
return {
|
||||
id,
|
||||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
clearLayer: jest.fn((state, _layerId, _indexPatternId) => state),
|
||||
removeLayer: jest.fn(),
|
||||
getLayerIds: jest.fn((_state) => ['layer1']),
|
||||
getSupportedLayers: jest.fn(() => [{ type: layerTypes.DATA, label: 'Data Layer' }]),
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { mount, shallow } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { DragDropBuckets, DraggableBucketContainer } from '.';
|
||||
import { DragDropBuckets, DraggableBucketContainer } from './buckets';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
|
@ -12,6 +12,12 @@ export { PalettePicker } from './palette_picker';
|
|||
export { FieldPicker, LensFieldIcon, TruncatedLabel } from './field_picker';
|
||||
export type { FieldOption, FieldOptionValue } from './field_picker';
|
||||
export { ChangeIndexPattern, fieldExists, fieldContainsData } from './dataview_picker';
|
||||
export { QueryInput, isQueryValid, validateQuery } from './query_input';
|
||||
export {
|
||||
NewBucketButton,
|
||||
DraggableBucketContainer,
|
||||
DragDropBuckets,
|
||||
} from './drag_drop_bucket/buckets';
|
||||
export { RangeInputField } from './range_input_field';
|
||||
export {
|
||||
BucketAxisBoundsControl,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { toElasticsearchQuery, fromKueryExpression, luceneStringToDsl } from '@kbn/es-query';
|
||||
import { IndexPattern } from '../../types';
|
||||
|
||||
export const validateQuery = (input: Query | undefined, indexPattern: IndexPattern) => {
|
||||
let isValid = true;
|
||||
let error: string | undefined;
|
||||
|
||||
try {
|
||||
if (input) {
|
||||
if (input.language === 'kuery') {
|
||||
toElasticsearchQuery(fromKueryExpression(input.query), indexPattern);
|
||||
} else {
|
||||
luceneStringToDsl(input.query);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
return { isValid, error };
|
||||
};
|
||||
|
||||
export const isQueryValid = (input: Query, indexPattern: IndexPattern) =>
|
||||
validateQuery(input, indexPattern).isValid;
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { QueryInput } from './query_input';
|
||||
export { validateQuery, isQueryValid } from './helpers';
|
|
@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { isEqual } from 'lodash';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import { useDebouncedValue } from '../shared_components';
|
||||
import { useDebouncedValue } from '../debounced_value';
|
||||
|
||||
export const QueryInput = ({
|
||||
value,
|
|
@ -97,7 +97,13 @@ export function loadInitial(
|
|||
},
|
||||
autoApplyDisabled: boolean
|
||||
) {
|
||||
const { lensServices, datasourceMap, embeddableEditorIncomingState, initialContext } = storeDeps;
|
||||
const {
|
||||
lensServices,
|
||||
datasourceMap,
|
||||
embeddableEditorIncomingState,
|
||||
initialContext,
|
||||
visualizationMap,
|
||||
} = storeDeps;
|
||||
const { resolvedDateRange, searchSessionId, isLinkedToOriginatingApp, ...emptyState } =
|
||||
getPreloadedState(storeDeps);
|
||||
const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices;
|
||||
|
@ -117,6 +123,8 @@ export function loadInitial(
|
|||
return initializeSources(
|
||||
{
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
visualizationState: lens.visualization,
|
||||
datasourceStates: lens.datasourceStates,
|
||||
initialContext,
|
||||
adHocDataViews: lens.persistedDoc?.state.adHocDataViews,
|
||||
|
@ -126,14 +134,14 @@ export function loadInitial(
|
|||
isFullEditor: true,
|
||||
}
|
||||
)
|
||||
.then(({ states, indexPatterns, indexPatternRefs }) => {
|
||||
.then(({ datasourceStates, indexPatterns, indexPatternRefs }) => {
|
||||
store.dispatch(
|
||||
initEmpty({
|
||||
newState: {
|
||||
...emptyState,
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
searchSessionId: data.search.session.getSessionId() || data.search.session.start(),
|
||||
datasourceStates: Object.entries(states).reduce(
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
|
@ -187,9 +195,16 @@ export function loadInitial(
|
|||
const filters = data.query.filterManager.inject(doc.state.filters, doc.references);
|
||||
// Don't overwrite any pinned filters
|
||||
data.query.filterManager.setAppFilters(filters);
|
||||
|
||||
const docVisualizationState = {
|
||||
activeId: doc.visualizationType,
|
||||
state: doc.state.visualization,
|
||||
};
|
||||
return initializeSources(
|
||||
{
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
visualizationState: docVisualizationState,
|
||||
datasourceStates: docDatasourceStates,
|
||||
references: [...doc.references, ...(doc.state.internalReferences || [])],
|
||||
initialContext,
|
||||
|
@ -200,7 +215,7 @@ export function loadInitial(
|
|||
},
|
||||
{ isFullEditor: true }
|
||||
)
|
||||
.then(({ states, indexPatterns, indexPatternRefs }) => {
|
||||
.then(({ datasourceStates, visualizationState, indexPatterns, indexPatternRefs }) => {
|
||||
const currentSessionId = data.search.session.getSessionId();
|
||||
store.dispatch(
|
||||
setState({
|
||||
|
@ -219,10 +234,10 @@ export function loadInitial(
|
|||
activeDatasourceId: getInitialDatasourceId(datasourceMap, doc),
|
||||
visualization: {
|
||||
activeId: doc.visualizationType,
|
||||
state: doc.state.visualization,
|
||||
state: visualizationState,
|
||||
},
|
||||
dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs),
|
||||
datasourceStates: Object.entries(states).reduce(
|
||||
datasourceStates: Object.entries(datasourceStates).reduce(
|
||||
(state, [datasourceId, datasourceState]) => ({
|
||||
...state,
|
||||
[datasourceId]: {
|
||||
|
|
|
@ -227,6 +227,7 @@ describe('lensSlice', () => {
|
|||
removeLayer: (layerIds: unknown, layerId: string) =>
|
||||
(layerIds as string[]).filter((id: string) => id !== layerId),
|
||||
insertLayer: (layerIds: unknown, layerId: string) => [...(layerIds as string[]), layerId],
|
||||
getCurrentIndexPatternId: jest.fn(() => 'indexPattern1'),
|
||||
};
|
||||
};
|
||||
const datasourceStates = {
|
||||
|
|
|
@ -288,6 +288,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
}
|
||||
) => {
|
||||
const activeVisualization = visualizationMap[visualizationId];
|
||||
const activeDataSource = datasourceMap[state.activeDatasourceId!];
|
||||
const isOnlyLayer =
|
||||
getRemoveOperation(
|
||||
activeVisualization,
|
||||
|
@ -309,9 +310,13 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
}
|
||||
);
|
||||
state.stagedPreview = undefined;
|
||||
// reuse the activeDatasource current dataView id for the moment
|
||||
const currentDataViewsId = activeDataSource.getCurrentIndexPatternId(
|
||||
state.datasourceStates[state.activeDatasourceId!].state
|
||||
);
|
||||
state.visualization.state =
|
||||
isOnlyLayer || !activeVisualization.removeLayer
|
||||
? activeVisualization.clearLayer(state.visualization.state, layerId)
|
||||
? activeVisualization.clearLayer(state.visualization.state, layerId, currentDataViewsId)
|
||||
: activeVisualization.removeLayer(state.visualization.state, layerId);
|
||||
},
|
||||
[changeIndexPattern.type]: (
|
||||
|
@ -352,15 +357,15 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
for (const visualizationId of visualizationIds) {
|
||||
const activeVisualization =
|
||||
visualizationId &&
|
||||
state.visualization.activeId !== visualizationId &&
|
||||
state.visualization.activeId === visualizationId &&
|
||||
visualizationMap[visualizationId];
|
||||
if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) {
|
||||
newState.visualization = {
|
||||
...state.visualization,
|
||||
state: activeVisualization.onIndexPatternChange(
|
||||
state.visualization.state,
|
||||
layerId,
|
||||
indexPatternId
|
||||
indexPatternId,
|
||||
layerId
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -806,15 +811,20 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
}
|
||||
|
||||
const activeVisualization = visualizationMap[state.visualization.activeId];
|
||||
const activeDatasource = datasourceMap[state.activeDatasourceId];
|
||||
// reuse the active datasource dataView id for the new layer
|
||||
const currentDataViewsId = activeDatasource.getCurrentIndexPatternId(
|
||||
state.datasourceStates[state.activeDatasourceId!].state
|
||||
);
|
||||
const visualizationState = activeVisualization.appendLayer!(
|
||||
state.visualization.state,
|
||||
layerId,
|
||||
layerType
|
||||
layerType,
|
||||
currentDataViewsId
|
||||
);
|
||||
|
||||
const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap);
|
||||
|
||||
const activeDatasource = datasourceMap[state.activeDatasourceId];
|
||||
const { noDatasource } =
|
||||
activeVisualization
|
||||
.getSupportedLayers(visualizationState, framePublicAPI)
|
||||
|
|
|
@ -34,6 +34,7 @@ export const selectAutoApplyEnabled = (state: LensState) => !state.lens.autoAppl
|
|||
export const selectChangesApplied = (state: LensState) =>
|
||||
!state.lens.autoApplyDisabled || Boolean(state.lens.changesApplied);
|
||||
export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates;
|
||||
export const selectVisualizationState = (state: LensState) => state.lens.visualization;
|
||||
export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId;
|
||||
export const selectActiveData = (state: LensState) => state.lens.activeData;
|
||||
export const selectDataViews = (state: LensState) => state.lens.dataViews;
|
||||
|
@ -97,7 +98,9 @@ export const selectSavedObjectFormat = createSelector(
|
|||
{ datasourceMap, visualizationMap, extractFilterReferences }
|
||||
) => {
|
||||
const activeVisualization =
|
||||
visualization.state && visualization.activeId && visualizationMap[visualization.activeId];
|
||||
visualization.state && visualization.activeId
|
||||
? visualizationMap[visualization.activeId]
|
||||
: null;
|
||||
const activeDatasource =
|
||||
datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading
|
||||
? datasourceMap[activeDatasourceId]
|
||||
|
@ -132,6 +135,20 @@ export const selectSavedObjectFormat = createSelector(
|
|||
});
|
||||
});
|
||||
|
||||
let persistibleVisualizationState = visualization.state;
|
||||
if (activeVisualization.getPersistableState) {
|
||||
const { state: persistableState, savedObjectReferences } =
|
||||
activeVisualization.getPersistableState(visualization.state);
|
||||
persistibleVisualizationState = persistableState;
|
||||
savedObjectReferences.forEach((r) => {
|
||||
if (r.type === 'index-pattern' && adHocDataViews[r.id]) {
|
||||
internalReferences.push(r);
|
||||
} else {
|
||||
references.push(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const persistableAdHocDataViews = Object.fromEntries(
|
||||
Object.entries(adHocDataViews).map(([id, dataView]) => {
|
||||
const { references: dataViewReferences, state } =
|
||||
|
@ -162,7 +179,7 @@ export const selectSavedObjectFormat = createSelector(
|
|||
type: 'lens',
|
||||
references,
|
||||
state: {
|
||||
visualization: visualization.state,
|
||||
visualization: persistibleVisualizationState,
|
||||
query,
|
||||
filters: [...persistableFilters, ...adHocFilters],
|
||||
datasourceStates: persistibleDatasourceStates,
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
import type { LensInspector } from './lens_inspector_service';
|
||||
import type { FormatSelectorOptions } from './indexpattern_datasource/dimension_panel/format_selector';
|
||||
import type { DataViewsState } from './state_management/types';
|
||||
import type { IndexPatternServiceAPI } from './indexpattern_service/service';
|
||||
import type { IndexPatternServiceAPI } from './data_views_service/service';
|
||||
import type { Document } from './persistence/saved_object_store';
|
||||
|
||||
export interface IndexPatternRef {
|
||||
|
@ -118,8 +118,8 @@ export interface EditorFrameSetup {
|
|||
registerDatasource: <T, P>(
|
||||
datasource: Datasource<T, P> | (() => Promise<Datasource<T, P>>)
|
||||
) => void;
|
||||
registerVisualization: <T>(
|
||||
visualization: Visualization<T> | (() => Promise<Visualization<T>>)
|
||||
registerVisualization: <T, P>(
|
||||
visualization: Visualization<T, P> | (() => Promise<Visualization<T, P>>)
|
||||
) => void;
|
||||
}
|
||||
|
||||
|
@ -456,6 +456,10 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
* Get the used DataView value from state
|
||||
*/
|
||||
getUsedDataView: (state: T, layerId: string) => string;
|
||||
/**
|
||||
* Get all the used DataViews from state
|
||||
*/
|
||||
getUsedDataViews: (state: T) => string[];
|
||||
}
|
||||
|
||||
export interface DatasourceFixAction<T> {
|
||||
|
@ -517,6 +521,7 @@ export interface DatasourceDataPanelProps<T = unknown> {
|
|||
uiActions: UiActionsStart;
|
||||
indexPatternService: IndexPatternServiceAPI;
|
||||
frame: FramePublicAPI;
|
||||
usedIndexPatterns?: string[];
|
||||
}
|
||||
|
||||
interface SharedDimensionProps {
|
||||
|
@ -638,6 +643,7 @@ export interface Operation extends OperationMetadata {
|
|||
}
|
||||
|
||||
export interface OperationMetadata {
|
||||
interval?: string;
|
||||
// The output of this operation will have this data type
|
||||
dataType: DataType;
|
||||
// A bucketed operation is grouped by duplicate values, otherwise each row is
|
||||
|
@ -672,12 +678,10 @@ export interface VisualizationConfigProps<T = unknown> {
|
|||
|
||||
export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProps<T> & {
|
||||
setState: (newState: T) => void;
|
||||
onChangeIndexPattern: (indexPatternId: string, layerId: string) => void;
|
||||
onChangeIndexPattern: (indexPatternId: string) => void;
|
||||
};
|
||||
|
||||
export type VisualizationLayerHeaderContentProps<T = unknown> = VisualizationLayerWidgetProps<T> & {
|
||||
defaultIndexPatternId: string;
|
||||
};
|
||||
export type VisualizationLayerHeaderContentProps<T = unknown> = VisualizationLayerWidgetProps<T>;
|
||||
|
||||
export interface VisualizationToolbarProps<T = unknown> {
|
||||
setState: (newState: T) => void;
|
||||
|
@ -892,18 +896,23 @@ export interface VisualizationDisplayOptions {
|
|||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
export interface Visualization<T = unknown> {
|
||||
export interface Visualization<T = unknown, P = unknown> {
|
||||
/** Plugin ID, such as "lnsXY" */
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Initialize is allowed to modify the state stored in memory. The initialize function
|
||||
* is called with a previous state in two cases:
|
||||
* - Loadingn from a saved visualization
|
||||
* - Loading from a saved visualization
|
||||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T;
|
||||
|
||||
/**
|
||||
* Retrieve the used DataViews in the visualization
|
||||
*/
|
||||
getUsedDataViews?: (state?: T) => string[];
|
||||
|
||||
getMainPalette?: (state: T) => undefined | PaletteOutput;
|
||||
|
||||
/**
|
||||
|
@ -926,15 +935,18 @@ export interface Visualization<T = unknown> {
|
|||
switchVisualizationType?: (visualizationTypeId: string, state: T) => T;
|
||||
/** Description is displayed as the clickable text in the chart switcher */
|
||||
getDescription: (state: T) => { icon?: IconType; label: string };
|
||||
|
||||
/** Visualizations can have references as well */
|
||||
getPersistableState?: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] };
|
||||
/** Hydrate from persistable state and references to final state */
|
||||
fromPersistableState?: (state: P, references?: SavedObjectReference[]) => T;
|
||||
/** Frame needs to know which layers the visualization is currently using */
|
||||
getLayerIds: (state: T) => string[];
|
||||
/** Reset button on each layer triggers this */
|
||||
clearLayer: (state: T, layerId: string) => T;
|
||||
clearLayer: (state: T, layerId: string, indexPatternId: string) => T;
|
||||
/** Optional, if the visualization supports multiple layers */
|
||||
removeLayer?: (state: T, layerId: string) => T;
|
||||
/** Track added layers in internal state */
|
||||
appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId?: string) => T;
|
||||
appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId: string) => T;
|
||||
|
||||
/** Retrieve a list of supported layer types with initialization data */
|
||||
getSupportedLayers: (
|
||||
|
@ -1088,7 +1100,7 @@ export interface Visualization<T = unknown> {
|
|||
*/
|
||||
getErrorMessages: (
|
||||
state: T,
|
||||
datasourceLayers?: DatasourceLayers
|
||||
frame?: Pick<FramePublicAPI, 'datasourceLayers' | 'dataViews'>
|
||||
) =>
|
||||
| Array<{
|
||||
shortMessage: string;
|
||||
|
@ -1096,6 +1108,14 @@ export interface Visualization<T = unknown> {
|
|||
}>
|
||||
| undefined;
|
||||
|
||||
validateColumn?: (
|
||||
state: T,
|
||||
frame: Pick<FramePublicAPI, 'dataViews'>,
|
||||
layerId: string,
|
||||
columnId: string,
|
||||
group?: VisualizationDimensionGroupConfig
|
||||
) => { invalid: boolean; invalidMessage?: string };
|
||||
|
||||
/**
|
||||
* The frame calls this function to display warnings about visualization
|
||||
*/
|
||||
|
|
|
@ -22,7 +22,7 @@ import type {
|
|||
IndexPatternRef,
|
||||
} from './types';
|
||||
import type { DatasourceStates, VisualizationState } from './state_management';
|
||||
import { IndexPatternServiceAPI } from './indexpattern_service/service';
|
||||
import { IndexPatternServiceAPI } from './data_views_service/service';
|
||||
|
||||
export function getVisualizeGeoFieldMessage(fieldType: string) {
|
||||
return i18n.translate('xpack.lens.visualizeGeoFieldMessage', {
|
||||
|
@ -113,9 +113,13 @@ export async function refreshIndexPatternsList({
|
|||
export function getIndexPatternsIds({
|
||||
activeDatasources,
|
||||
datasourceStates,
|
||||
visualizationState,
|
||||
activeVisualization,
|
||||
}: {
|
||||
activeDatasources: Record<string, Datasource>;
|
||||
datasourceStates: DatasourceStates;
|
||||
visualizationState: unknown;
|
||||
activeVisualization?: Visualization;
|
||||
}): string[] {
|
||||
let currentIndexPatternId: string | undefined;
|
||||
const references: SavedObjectReference[] = [];
|
||||
|
@ -125,6 +129,11 @@ export function getIndexPatternsIds({
|
|||
currentIndexPatternId = indexPatternId;
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
if (activeVisualization?.getPersistableState) {
|
||||
const { savedObjectReferences } = activeVisualization.getPersistableState(visualizationState);
|
||||
references.push(...savedObjectReferences);
|
||||
}
|
||||
const referencesIds = references
|
||||
.filter(({ type }) => type === 'index-pattern')
|
||||
.map(({ id }) => id);
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('Datatable Visualization', () => {
|
|||
layerType: layerTypes.DATA,
|
||||
columns: [{ columnId: 'a' }, { columnId: 'b' }, { columnId: 'c' }],
|
||||
};
|
||||
expect(datatableVisualization.clearLayer(state, 'baz')).toMatchObject({
|
||||
expect(datatableVisualization.clearLayer(state, 'baz', 'indexPattern1')).toMatchObject({
|
||||
layerId: 'baz',
|
||||
layerType: layerTypes.DATA,
|
||||
columns: [],
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('metric_visualization', () => {
|
|||
describe('#clearLayer', () => {
|
||||
it('returns a clean layer', () => {
|
||||
(generateId as jest.Mock).mockReturnValueOnce('test-id1');
|
||||
expect(metricVisualization.clearLayer(exampleState(), 'l1')).toEqual({
|
||||
expect(metricVisualization.clearLayer(exampleState(), 'l1', 'indexPattern1')).toEqual({
|
||||
accessor: undefined,
|
||||
layerId: 'l1',
|
||||
layerType: layerTypes.DATA,
|
||||
|
|
|
@ -515,7 +515,7 @@ describe('metric visualization', () => {
|
|||
});
|
||||
|
||||
it('clears a layer', () => {
|
||||
expect(visualization.clearLayer(fullState, 'some-id')).toMatchInlineSnapshot(`
|
||||
expect(visualization.clearLayer(fullState, 'some-id', 'indexPattern1')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"layerId": "first",
|
||||
"layerType": "data",
|
||||
|
|
|
@ -8,6 +8,7 @@ Object {
|
|||
"addTimeMarker": Array [
|
||||
false,
|
||||
],
|
||||
"annotations": Array [],
|
||||
"emphasizeFitting": Array [
|
||||
true,
|
||||
],
|
||||
|
|
|
@ -5,24 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createMockFramePublicAPI } from '../../../mocks';
|
||||
import { FramePublicAPI } from '../../../types';
|
||||
import { getStaticDate } from './helpers';
|
||||
|
||||
const frame: FramePublicAPI = {
|
||||
datasourceLayers: {},
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
dataViews: {
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
existingFields: {},
|
||||
isFirstExistenceFetch: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe('annotations helpers', () => {
|
||||
describe('getStaticDate', () => {
|
||||
it('should return the middle of the date range on when nothing is configured', () => {
|
||||
expect(getStaticDate([], frame)).toBe('2022-03-12T00:00:00.000Z');
|
||||
expect(
|
||||
getStaticDate(
|
||||
[],
|
||||
createMockFramePublicAPI({
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
})
|
||||
)
|
||||
).toBe('2022-03-12T00:00:00.000Z');
|
||||
});
|
||||
it('should return the middle of the date range value on when there is no active data', () => {
|
||||
expect(
|
||||
|
@ -36,7 +33,9 @@ describe('annotations helpers', () => {
|
|||
xAccessor: 'a',
|
||||
},
|
||||
],
|
||||
frame
|
||||
createMockFramePublicAPI({
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
})
|
||||
)
|
||||
).toBe('2022-03-12T00:00:00.000Z');
|
||||
});
|
||||
|
@ -64,7 +63,7 @@ describe('annotations helpers', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} as FramePublicAPI['activeData'];
|
||||
expect(
|
||||
getStaticDate(
|
||||
[
|
||||
|
@ -76,10 +75,10 @@ describe('annotations helpers', () => {
|
|||
xAccessor: 'a',
|
||||
},
|
||||
],
|
||||
{
|
||||
...frame,
|
||||
activeData: activeData as FramePublicAPI['activeData'],
|
||||
}
|
||||
createMockFramePublicAPI({
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
activeData,
|
||||
})
|
||||
)
|
||||
).toBe('2022-02-27T23:00:00.000Z');
|
||||
});
|
||||
|
@ -107,7 +106,7 @@ describe('annotations helpers', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} as FramePublicAPI['activeData'];
|
||||
expect(
|
||||
getStaticDate(
|
||||
[
|
||||
|
@ -119,10 +118,10 @@ describe('annotations helpers', () => {
|
|||
xAccessor: 'a',
|
||||
},
|
||||
],
|
||||
{
|
||||
...frame,
|
||||
activeData: activeData as FramePublicAPI['activeData'],
|
||||
}
|
||||
createMockFramePublicAPI({
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
activeData,
|
||||
})
|
||||
)
|
||||
).toBe('2022-03-12T00:00:00.000Z');
|
||||
});
|
||||
|
@ -162,7 +161,7 @@ describe('annotations helpers', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} as FramePublicAPI['activeData'];
|
||||
expect(
|
||||
getStaticDate(
|
||||
[
|
||||
|
@ -174,10 +173,10 @@ describe('annotations helpers', () => {
|
|||
xAccessor: 'a',
|
||||
},
|
||||
],
|
||||
{
|
||||
...frame,
|
||||
activeData: activeData as FramePublicAPI['activeData'],
|
||||
}
|
||||
createMockFramePublicAPI({
|
||||
dateRange: { fromDate: '2022-02-01T00:00:00.000Z', toDate: '2022-04-20T00:00:00.000Z' },
|
||||
activeData,
|
||||
})
|
||||
)
|
||||
).toBe('2022-03-26T05:00:00.000Z');
|
||||
});
|
||||
|
@ -242,7 +241,7 @@ describe('annotations helpers', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} as FramePublicAPI['activeData'];
|
||||
expect(
|
||||
getStaticDate(
|
||||
[
|
||||
|
@ -261,11 +260,11 @@ describe('annotations helpers', () => {
|
|||
xAccessor: 'd',
|
||||
},
|
||||
],
|
||||
{
|
||||
...frame,
|
||||
|
||||
createMockFramePublicAPI({
|
||||
activeData,
|
||||
dateRange: { fromDate: '2020-02-01T00:00:00.000Z', toDate: '2022-09-20T00:00:00.000Z' },
|
||||
activeData: activeData as FramePublicAPI['activeData'],
|
||||
}
|
||||
})
|
||||
)
|
||||
).toBe('2020-08-24T12:06:40.000Z');
|
||||
});
|
||||
|
|
|
@ -120,6 +120,7 @@ export const getAnnotationsSupportedLayer = (
|
|||
|
||||
const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotationConfig => ({
|
||||
label: defaultAnnotationLabel,
|
||||
type: 'manual',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp,
|
||||
|
|
|
@ -32,8 +32,10 @@ export class XyVisualization {
|
|||
const { getXyVisualization } = await import('../../async_services');
|
||||
const [coreStart, { charts, data, fieldFormats, eventAnnotation }] =
|
||||
await core.getStartServices();
|
||||
const palettes = await charts.palettes.getPalettes();
|
||||
const eventAnnotationService = await eventAnnotation.getService();
|
||||
const [palettes, eventAnnotationService] = await Promise.all([
|
||||
charts.palettes.getPalettes(),
|
||||
eventAnnotation.getService(),
|
||||
]);
|
||||
const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS);
|
||||
return getXyVisualization({
|
||||
core: coreStart,
|
||||
|
|
|
@ -5,8 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DistributiveOmit } from '@elastic/eui';
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import { isQueryAnnotationConfig } from '@kbn/event-annotation-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { validateQuery } from '../../shared_components';
|
||||
import type {
|
||||
FramePublicAPI,
|
||||
DatasourcePublicAPI,
|
||||
VisualizationDimensionGroupConfig,
|
||||
} from '../../types';
|
||||
import {
|
||||
visualizationTypes,
|
||||
XYLayerConfig,
|
||||
|
@ -14,6 +23,9 @@ import {
|
|||
XYReferenceLineLayerConfig,
|
||||
SeriesType,
|
||||
YConfig,
|
||||
XYState,
|
||||
XYPersistedState,
|
||||
State,
|
||||
} from './types';
|
||||
import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers';
|
||||
|
||||
|
@ -102,3 +114,129 @@ export function hasHistogramSeries(
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
return `xy-visualization-layer-${layerId}`;
|
||||
}
|
||||
|
||||
export function extractReferences(state: XYState) {
|
||||
const savedObjectReferences: SavedObjectReference[] = [];
|
||||
const persistableLayers: Array<DistributiveOmit<XYLayerConfig, 'indexPatternId'>> = [];
|
||||
state.layers.forEach((layer) => {
|
||||
if (isAnnotationsLayer(layer)) {
|
||||
const { indexPatternId, ...persistableLayer } = layer;
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: indexPatternId,
|
||||
name: getLayerReferenceName(layer.layerId),
|
||||
});
|
||||
persistableLayers.push(persistableLayer);
|
||||
} else {
|
||||
persistableLayers.push(layer);
|
||||
}
|
||||
});
|
||||
return { savedObjectReferences, state: { ...state, layers: persistableLayers } };
|
||||
}
|
||||
|
||||
export function injectReferences(
|
||||
state: XYPersistedState,
|
||||
references?: SavedObjectReference[]
|
||||
): XYState {
|
||||
if (!references || !references.length) {
|
||||
return state as XYState;
|
||||
}
|
||||
const fallbackIndexPatternId = references.find(({ type }) => type === 'index-pattern')!.id;
|
||||
return {
|
||||
...state,
|
||||
layers: state.layers.map((layer) => {
|
||||
if (!isAnnotationsLayer(layer)) {
|
||||
return layer as XYLayerConfig;
|
||||
}
|
||||
return {
|
||||
...layer,
|
||||
indexPatternId:
|
||||
references.find(({ name }) => name === getLayerReferenceName(layer.layerId))?.id ||
|
||||
fallbackIndexPatternId,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateColumn(
|
||||
state: State,
|
||||
frame: Pick<FramePublicAPI, 'dataViews'>,
|
||||
layerId: string,
|
||||
columnId: string,
|
||||
group?: VisualizationDimensionGroupConfig
|
||||
): { invalid: boolean; invalidMessages?: string[] } {
|
||||
if (group?.invalid) {
|
||||
return {
|
||||
invalid: true,
|
||||
invalidMessages: group.invalidMessage ? [group.invalidMessage] : undefined,
|
||||
};
|
||||
}
|
||||
const validColumn = { invalid: false };
|
||||
const layer = state.layers.find((l) => l.layerId === layerId);
|
||||
if (!layer || !isAnnotationsLayer(layer)) {
|
||||
return validColumn;
|
||||
}
|
||||
const annotation = layer.annotations.find(({ id }) => id === columnId);
|
||||
if (!annotation || !isQueryAnnotationConfig(annotation)) {
|
||||
return validColumn;
|
||||
}
|
||||
const { dataViews } = frame || {};
|
||||
const layerDataView = dataViews.indexPatterns[layer.indexPatternId];
|
||||
|
||||
const invalidMessages: string[] = [];
|
||||
|
||||
if (annotation.timeField && !Boolean(layerDataView.getFieldByName(annotation.timeField))) {
|
||||
invalidMessages.push(
|
||||
i18n.translate('xpack.lens.xyChart.annotationError.timeFieldNotFound', {
|
||||
defaultMessage: 'Time field {timeField} not found in data view {dataView}',
|
||||
values: { timeField: annotation.timeField, dataView: layerDataView.title },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { isValid, error } = validateQuery(annotation?.filter, layerDataView);
|
||||
if (!isValid && error) {
|
||||
invalidMessages.push(error);
|
||||
}
|
||||
if (annotation.textField && !Boolean(layerDataView.getFieldByName(annotation.textField))) {
|
||||
invalidMessages.push(
|
||||
i18n.translate('xpack.lens.xyChart.annotationError.textFieldNotFound', {
|
||||
defaultMessage: 'Text field {textField} not found in data view {dataView}',
|
||||
values: { textField: annotation.textField, dataView: layerDataView.title },
|
||||
})
|
||||
);
|
||||
}
|
||||
if (annotation.extraFields?.length) {
|
||||
const missingTooltipFields = [];
|
||||
for (const field of annotation.extraFields) {
|
||||
if (!Boolean(layerDataView.getFieldByName(field))) {
|
||||
missingTooltipFields.push(field);
|
||||
}
|
||||
}
|
||||
if (missingTooltipFields.length) {
|
||||
invalidMessages.push(
|
||||
i18n.translate('xpack.lens.xyChart.annotationError.tooltipFieldNotFound', {
|
||||
defaultMessage:
|
||||
'Tooltip {missingFields, plural, one {field} other {fields}} {missingTooltipFields} not found in data view {dataView}',
|
||||
values: {
|
||||
missingTooltipFields: missingTooltipFields.join(', '),
|
||||
missingFields: missingTooltipFields.length,
|
||||
dataView: layerDataView.title,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!invalidMessages.length) {
|
||||
return validColumn;
|
||||
}
|
||||
return {
|
||||
invalid: true,
|
||||
invalidMessages,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -539,6 +539,38 @@ describe('#toExpression', () => {
|
|||
expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultReferenceLineColor]);
|
||||
});
|
||||
|
||||
it('should ignore annotation layers with no event configured', () => {
|
||||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'show',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
seriesType: 'area',
|
||||
splitAccessor: 'd',
|
||||
xAccessor: 'a',
|
||||
accessors: ['b', 'c'],
|
||||
yConfig: [{ forAccessor: 'a' }],
|
||||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [],
|
||||
indexPatternId: 'my-indexPattern',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock },
|
||||
undefined,
|
||||
datasourceExpressionsByLayers
|
||||
) as Ast;
|
||||
|
||||
expect(expression.chain[0].arguments.layers).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should correctly set the current time marker visibility settings', () => {
|
||||
const state: XYState = {
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
|
|
|
@ -8,9 +8,14 @@
|
|||
import { Ast, AstFunction } from '@kbn/interpreter';
|
||||
import { Position, ScaleType } from '@elastic/charts';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import {
|
||||
EventAnnotationServiceType,
|
||||
isManualPointAnnotationConfig,
|
||||
isRangeAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import { LegendSize } from '@kbn/visualizations-plugin/public';
|
||||
import { XYCurveType } from '@kbn/expression-xy-plugin/common';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import {
|
||||
State,
|
||||
YConfig,
|
||||
|
@ -241,6 +246,11 @@ export const buildExpression = (
|
|||
});
|
||||
}
|
||||
|
||||
const isValidAnnotation = (a: EventAnnotationConfig) =>
|
||||
isManualPointAnnotationConfig(a) ||
|
||||
isRangeAnnotationConfig(a) ||
|
||||
(a.filter && a.filter?.query !== '');
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
|
@ -343,10 +353,39 @@ export const buildExpression = (
|
|||
datasourceExpressionsByLayers[layer.layerId]
|
||||
)
|
||||
),
|
||||
...validAnnotationsLayers.map((layer) =>
|
||||
annotationLayerToExpression(layer, eventAnnotationService)
|
||||
),
|
||||
],
|
||||
annotations:
|
||||
validAnnotationsLayers.length &&
|
||||
validAnnotationsLayers.flatMap((l) => l.annotations.filter(isValidAnnotation)).length
|
||||
? [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'event_annotations_result',
|
||||
arguments: {
|
||||
layers: validAnnotationsLayers.map((layer) =>
|
||||
annotationLayerToExpression(layer, eventAnnotationService)
|
||||
),
|
||||
datatable: eventAnnotationService.toFetchExpression({
|
||||
interval:
|
||||
(validDataLayers[0]?.xAccessor &&
|
||||
metadata[validDataLayers[0]?.layerId]?.[
|
||||
validDataLayers[0]?.xAccessor
|
||||
]?.interval) ||
|
||||
'auto',
|
||||
groups: validAnnotationsLayers.map((layer) => ({
|
||||
indexPatternId: layer.indexPatternId,
|
||||
annotations: layer.annotations.filter(isValidAnnotation),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -416,9 +455,7 @@ const annotationLayerToExpression = (
|
|||
arguments: {
|
||||
simpleView: [Boolean(layer.simpleView)],
|
||||
layerId: [layer.layerId],
|
||||
annotations: layer.annotations
|
||||
? layer.annotations.map((ann): Ast => eventAnnotationService.toExpression(ann))
|
||||
: [],
|
||||
annotations: eventAnnotationService.toExpression(layer.annotations || []),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
IconChartBarHorizontal,
|
||||
} from '@kbn/chart-icons';
|
||||
|
||||
import { DistributiveOmit } from '@elastic/eui';
|
||||
import type { VisualizationType, Suggestion } from '../../types';
|
||||
import type { ValueLabelConfig } from '../../../common/types';
|
||||
|
||||
|
@ -115,6 +116,8 @@ export interface XYAnnotationLayerConfig {
|
|||
layerId: string;
|
||||
layerType: 'annotations';
|
||||
annotations: EventAnnotationConfig[];
|
||||
hide?: boolean;
|
||||
indexPatternId: string;
|
||||
simpleView?: boolean;
|
||||
}
|
||||
|
||||
|
@ -160,6 +163,12 @@ export interface XYState {
|
|||
|
||||
export type State = XYState;
|
||||
|
||||
export type XYPersistedState = Omit<XYState, 'layers'> & {
|
||||
layers: Array<DistributiveOmit<XYLayerConfig, 'indexPatternId'>>;
|
||||
};
|
||||
|
||||
export type PersistedState = XYPersistedState;
|
||||
|
||||
const groupLabelForBar = i18n.translate('xpack.lens.xyVisualization.barGroupLabel', {
|
||||
defaultMessage: 'Bar',
|
||||
});
|
||||
|
|
|
@ -35,9 +35,13 @@ import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/
|
|||
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DataViewsState } from '../../state_management';
|
||||
import { createMockedIndexPattern } from '../../indexpattern_datasource/mocks';
|
||||
import { createMockDataViewsState } from '../../data_views_service/mocks';
|
||||
|
||||
const exampleAnnotation: EventAnnotationConfig = {
|
||||
id: 'an1',
|
||||
type: 'manual',
|
||||
label: 'Event 1',
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
|
@ -47,6 +51,7 @@ const exampleAnnotation: EventAnnotationConfig = {
|
|||
};
|
||||
const exampleAnnotation2: EventAnnotationConfig = {
|
||||
icon: 'circle',
|
||||
type: 'manual',
|
||||
id: 'an2',
|
||||
key: {
|
||||
timestamp: '2022-04-18T11:01:59.135Z',
|
||||
|
@ -237,7 +242,12 @@ describe('xy_visualization', () => {
|
|||
|
||||
describe('#appendLayer', () => {
|
||||
it('adds a layer', () => {
|
||||
const layers = xyVisualization.appendLayer!(exampleState(), 'foo', layerTypes.DATA).layers;
|
||||
const layers = xyVisualization.appendLayer!(
|
||||
exampleState(),
|
||||
'foo',
|
||||
layerTypes.DATA,
|
||||
'indexPattern1'
|
||||
).layers;
|
||||
expect(layers.length).toEqual(exampleState().layers.length + 1);
|
||||
expect(layers[layers.length - 1]).toMatchObject({ layerId: 'foo' });
|
||||
});
|
||||
|
@ -245,7 +255,7 @@ describe('xy_visualization', () => {
|
|||
|
||||
describe('#clearLayer', () => {
|
||||
it('clears the specified layer', () => {
|
||||
const layer = xyVisualization.clearLayer(exampleState(), 'first').layers[0];
|
||||
const layer = xyVisualization.clearLayer(exampleState(), 'first', 'indexPattern1').layers[0];
|
||||
expect(layer).toMatchObject({
|
||||
accessors: [],
|
||||
layerId: 'first',
|
||||
|
@ -460,6 +470,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
],
|
||||
|
@ -471,10 +482,12 @@ describe('xy_visualization', () => {
|
|||
).toEqual({
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [
|
||||
exampleAnnotation,
|
||||
{
|
||||
icon: 'triangle',
|
||||
type: 'manual',
|
||||
id: 'newCol',
|
||||
key: {
|
||||
timestamp: '2022-04-15T00:00:00.000Z',
|
||||
|
@ -495,6 +508,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
|
@ -517,6 +531,7 @@ describe('xy_visualization', () => {
|
|||
).toEqual({
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'newColId' }],
|
||||
});
|
||||
});
|
||||
|
@ -530,6 +545,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation, exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
|
@ -553,6 +569,7 @@ describe('xy_visualization', () => {
|
|||
).toEqual({
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2, exampleAnnotation],
|
||||
});
|
||||
});
|
||||
|
@ -566,12 +583,14 @@ describe('xy_visualization', () => {
|
|||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
|
@ -596,11 +615,13 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [{ ...exampleAnnotation, id: 'an2' }],
|
||||
},
|
||||
]);
|
||||
|
@ -614,12 +635,14 @@ describe('xy_visualization', () => {
|
|||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
|
@ -644,11 +667,13 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
]);
|
||||
|
@ -662,12 +687,14 @@ describe('xy_visualization', () => {
|
|||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
|
@ -692,11 +719,13 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
]);
|
||||
|
@ -710,12 +739,14 @@ describe('xy_visualization', () => {
|
|||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [],
|
||||
},
|
||||
],
|
||||
|
@ -740,11 +771,13 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [],
|
||||
},
|
||||
{
|
||||
layerId: 'second',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
]);
|
||||
|
@ -1106,6 +1139,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'ann',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation, { ...exampleAnnotation, id: 'an2' }],
|
||||
},
|
||||
],
|
||||
|
@ -1124,6 +1158,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'ann',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
]);
|
||||
|
@ -1843,6 +1878,7 @@ describe('xy_visualization', () => {
|
|||
{
|
||||
layerId: 'annotations',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation],
|
||||
},
|
||||
],
|
||||
|
@ -2278,7 +2314,7 @@ describe('xy_visualization', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
{ datasourceLayers: frame.datasourceLayers, dataViews: {} as DataViewsState }
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -2334,7 +2370,7 @@ describe('xy_visualization', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
datasourceLayers
|
||||
{ datasourceLayers, dataViews: {} as DataViewsState }
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -2390,7 +2426,7 @@ describe('xy_visualization', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
datasourceLayers
|
||||
{ datasourceLayers, dataViews: {} as DataViewsState }
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
|
@ -2399,6 +2435,98 @@ describe('xy_visualization', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Annotation layers', () => {
|
||||
function createStateWithAnnotationProps(annotation: Partial<EventAnnotationConfig>) {
|
||||
return {
|
||||
layers: [
|
||||
{
|
||||
layerId: 'layerId',
|
||||
layerType: 'annotations',
|
||||
indexPatternId: 'first',
|
||||
annotations: [
|
||||
{
|
||||
label: 'Event',
|
||||
id: '1',
|
||||
type: 'query',
|
||||
timeField: 'start_date',
|
||||
...annotation,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as XYState;
|
||||
}
|
||||
|
||||
function getFrameMock() {
|
||||
return createMockFramePublicAPI({
|
||||
datasourceLayers: { first: mockDatasource.publicAPIMock },
|
||||
dataViews: createMockDataViewsState({
|
||||
indexPatterns: { first: createMockedIndexPattern() },
|
||||
}),
|
||||
});
|
||||
}
|
||||
it('should return error if current annotation contains non-existent field as timeField', () => {
|
||||
const xyState = createStateWithAnnotationProps({
|
||||
timeField: 'non-existent',
|
||||
});
|
||||
const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
shortMessage: 'Time field non-existent not found in data view my-fake-index-pattern',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should return error if current annotation contains non existent field as textField', () => {
|
||||
const xyState = createStateWithAnnotationProps({
|
||||
textField: 'non-existent',
|
||||
});
|
||||
const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
shortMessage: 'Text field non-existent not found in data view my-fake-index-pattern',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should contain error if current annotation contains at least one non-existent field as tooltip field', () => {
|
||||
const xyState = createStateWithAnnotationProps({
|
||||
extraFields: ['bytes', 'memory', 'non-existent'],
|
||||
});
|
||||
const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
shortMessage: 'Tooltip field non-existent not found in data view my-fake-index-pattern',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should contain error if current annotation contains invalid query', () => {
|
||||
const xyState = createStateWithAnnotationProps({
|
||||
filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
|
||||
});
|
||||
const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
shortMessage: expect.stringContaining(
|
||||
'Expected "(", "{", value, whitespace but """ found.'
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should contain multiple errors if current annotation contains multiple non-existent fields', () => {
|
||||
const xyState = createStateWithAnnotationProps({
|
||||
timeField: 'non-existent',
|
||||
textField: 'non-existent',
|
||||
extraFields: ['bytes', 'memory', 'non-existent'],
|
||||
filter: { type: 'kibana_query', query: 'invalid: "', language: 'kuery' },
|
||||
});
|
||||
const errors = xyVisualization.getErrorMessages(xyState, getFrameMock());
|
||||
expect(errors).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWarningMessages', () => {
|
||||
|
@ -2555,4 +2683,80 @@ describe('xy_visualization', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fromPersistableState', () => {
|
||||
it('should inject references on annotation layers', () => {
|
||||
const baseState = exampleState();
|
||||
expect(
|
||||
xyVisualization.fromPersistableState!(
|
||||
{
|
||||
...baseState,
|
||||
layers: [
|
||||
...baseState.layers,
|
||||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
{
|
||||
type: 'index-pattern',
|
||||
name: `xy-visualization-layer-annotation`,
|
||||
id: 'indexPattern1',
|
||||
},
|
||||
]
|
||||
)
|
||||
).toEqual({
|
||||
...baseState,
|
||||
layers: [
|
||||
...baseState.layers,
|
||||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to the first dataView reference in case there are missing annotation references', () => {
|
||||
const baseState = exampleState();
|
||||
expect(
|
||||
xyVisualization.fromPersistableState!(
|
||||
{
|
||||
...baseState,
|
||||
layers: [
|
||||
...baseState.layers,
|
||||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
},
|
||||
[
|
||||
{
|
||||
type: 'index-pattern',
|
||||
name: 'something-else',
|
||||
id: 'indexPattern1',
|
||||
},
|
||||
]
|
||||
)
|
||||
).toEqual({
|
||||
...baseState,
|
||||
layers: [
|
||||
...baseState.layers,
|
||||
{
|
||||
layerId: 'annotation',
|
||||
layerType: layerTypes.ANNOTATIONS,
|
||||
indexPatternId: 'indexPattern1',
|
||||
annotations: [exampleAnnotation2],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { PaletteRegistry } from '@kbn/coloring';
|
|||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { CoreStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
||||
import { FillStyle } from '@kbn/expression-xy-plugin/common';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
@ -22,7 +22,7 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
|||
import { getSuggestions } from './xy_suggestions';
|
||||
import { XyToolbar } from './xy_config_panel';
|
||||
import { DimensionEditor } from './xy_config_panel/dimension_editor';
|
||||
import { LayerHeader } from './xy_config_panel/layer_header';
|
||||
import { LayerHeader, LayerHeaderContent } from './xy_config_panel/layer_header';
|
||||
import type { Visualization, AccessorConfig, FramePublicAPI } from '../../types';
|
||||
import {
|
||||
State,
|
||||
|
@ -33,9 +33,15 @@ import {
|
|||
YConfig,
|
||||
YAxisMode,
|
||||
SeriesType,
|
||||
PersistedState,
|
||||
} from './types';
|
||||
import { layerTypes } from '../../../common';
|
||||
import { isHorizontalChart } from './state_helpers';
|
||||
import {
|
||||
extractReferences,
|
||||
injectReferences,
|
||||
isHorizontalChart,
|
||||
validateColumn,
|
||||
} from './state_helpers';
|
||||
import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression';
|
||||
import { getAccessorColorConfigs, getColorAssignments } from './color_assignment';
|
||||
import { getColumnToLabelMap } from './state_helpers';
|
||||
|
@ -55,6 +61,7 @@ import {
|
|||
import {
|
||||
checkXAccessorCompatibility,
|
||||
defaultSeriesType,
|
||||
getAnnotationsLayers,
|
||||
getAxisName,
|
||||
getDataLayers,
|
||||
getDescription,
|
||||
|
@ -79,6 +86,7 @@ import { DimensionTrigger } from '../../shared_components/dimension_trigger';
|
|||
import { defaultAnnotationLabel } from './annotations/helpers';
|
||||
import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils';
|
||||
|
||||
const XY_ID = 'lnsXY';
|
||||
export const getXyVisualization = ({
|
||||
core,
|
||||
storage,
|
||||
|
@ -97,8 +105,8 @@ export const getXyVisualization = ({
|
|||
fieldFormats: FieldFormatsStart;
|
||||
useLegacyTimeAxis: boolean;
|
||||
kibanaTheme: ThemeServiceStart;
|
||||
}): Visualization<State> => ({
|
||||
id: 'lnsXY',
|
||||
}): Visualization<State, PersistedState> => ({
|
||||
id: XY_ID,
|
||||
visualizationTypes,
|
||||
getVisualizationTypeId(state) {
|
||||
const type = getVisualizationType(state);
|
||||
|
@ -121,7 +129,7 @@ export const getXyVisualization = ({
|
|||
};
|
||||
},
|
||||
|
||||
appendLayer(state, layerId, layerType) {
|
||||
appendLayer(state, layerId, layerType, indexPatternId) {
|
||||
const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType;
|
||||
return {
|
||||
...state,
|
||||
|
@ -131,12 +139,13 @@ export const getXyVisualization = ({
|
|||
seriesType: firstUsedSeriesType || state.preferredSeriesType,
|
||||
layerId,
|
||||
layerType,
|
||||
indexPatternId,
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
clearLayer(state, layerId) {
|
||||
clearLayer(state, layerId, indexPatternId) {
|
||||
return {
|
||||
...state,
|
||||
layers: state.layers.map((l) =>
|
||||
|
@ -145,11 +154,20 @@ export const getXyVisualization = ({
|
|||
: newLayerState({
|
||||
seriesType: state.preferredSeriesType,
|
||||
layerId,
|
||||
indexPatternId,
|
||||
})
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
getPersistableState(state) {
|
||||
return extractReferences(state);
|
||||
},
|
||||
|
||||
fromPersistableState(state, references) {
|
||||
return injectReferences(state, references);
|
||||
},
|
||||
|
||||
getDescription,
|
||||
|
||||
switchVisualizationType(seriesType: string, state: State) {
|
||||
|
@ -204,7 +222,7 @@ export const getXyVisualization = ({
|
|||
return state;
|
||||
}
|
||||
const newLayers = [...state.layers];
|
||||
newLayers[layerIndex] = { ...layer };
|
||||
newLayers[layerIndex] = { ...layer, indexPatternId };
|
||||
return {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
|
@ -468,8 +486,10 @@ export const getXyVisualization = ({
|
|||
return prevState;
|
||||
}
|
||||
if (isAnnotationsLayer(foundLayer)) {
|
||||
const newLayer = { ...foundLayer };
|
||||
newLayer.annotations = newLayer.annotations.filter(({ id }) => id !== columnId);
|
||||
const newLayer = {
|
||||
...foundLayer,
|
||||
annotations: foundLayer.annotations.filter(({ id }) => id !== columnId),
|
||||
};
|
||||
|
||||
const newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l));
|
||||
return {
|
||||
|
@ -520,6 +540,24 @@ export const getXyVisualization = ({
|
|||
};
|
||||
},
|
||||
|
||||
renderLayerPanel(domElement, props) {
|
||||
const { onChangeIndexPattern, ...otherProps } = props;
|
||||
render(
|
||||
<KibanaThemeProvider theme$={kibanaTheme.theme$}>
|
||||
<I18nProvider>
|
||||
<LayerHeaderContent
|
||||
{...otherProps}
|
||||
onChangeIndexPattern={(indexPatternId) => {
|
||||
// TODO: should it trigger an action as in the datasource?
|
||||
onChangeIndexPattern(indexPatternId);
|
||||
}}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
renderLayerHeader(domElement, props) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={kibanaTheme.theme$}>
|
||||
|
@ -560,7 +598,22 @@ export const getXyVisualization = ({
|
|||
|
||||
render(
|
||||
<KibanaThemeProvider theme$={kibanaTheme.theme$}>
|
||||
<I18nProvider>{dimensionEditor}</I18nProvider>
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
appName: 'lens',
|
||||
storage,
|
||||
uiSettings: core.uiSettings,
|
||||
data,
|
||||
fieldFormats,
|
||||
savedObjects: core.savedObjects,
|
||||
docLinks: core.docLinks,
|
||||
http: core.http,
|
||||
}}
|
||||
>
|
||||
{dimensionEditor}
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
domElement
|
||||
);
|
||||
|
@ -585,7 +638,53 @@ export const getXyVisualization = ({
|
|||
eventAnnotationService
|
||||
),
|
||||
|
||||
getErrorMessages(state, datasourceLayers) {
|
||||
validateColumn(state, frame, layerId, columnId, group) {
|
||||
const { invalid, invalidMessages } = validateColumn(state, frame, layerId, columnId, group);
|
||||
if (!invalid) {
|
||||
return { invalid };
|
||||
}
|
||||
return { invalid, invalidMessage: invalidMessages![0] };
|
||||
},
|
||||
|
||||
getErrorMessages(state, frame) {
|
||||
const { datasourceLayers, dataViews } = frame || {};
|
||||
const errors: Array<{
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
}> = [];
|
||||
|
||||
const annotationLayers = getAnnotationsLayers(state.layers);
|
||||
|
||||
if (dataViews) {
|
||||
annotationLayers.forEach((layer) => {
|
||||
layer.annotations.forEach((annotation) => {
|
||||
const validatedColumn = validateColumn(
|
||||
state,
|
||||
{ dataViews },
|
||||
layer.layerId,
|
||||
annotation.id
|
||||
);
|
||||
if (validatedColumn?.invalid && validatedColumn.invalidMessages?.length) {
|
||||
errors.push(
|
||||
...validatedColumn.invalidMessages.map((invalidMessage) => ({
|
||||
shortMessage: invalidMessage,
|
||||
longMessage: (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.xyChart.annotationError"
|
||||
defaultMessage="Annotation {annotationName} has an error: {errorMessage}"
|
||||
values={{
|
||||
annotationName: annotation.label,
|
||||
errorMessage: invalidMessage,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Data error handling below here
|
||||
const hasNoAccessors = ({ accessors }: XYDataLayerConfig) =>
|
||||
accessors == null || accessors.length === 0;
|
||||
|
@ -594,11 +693,6 @@ export const getXyVisualization = ({
|
|||
const hasNoSplitAccessor = ({ splitAccessor, seriesType }: XYDataLayerConfig) =>
|
||||
seriesType.includes('percentage') && splitAccessor == null;
|
||||
|
||||
const errors: Array<{
|
||||
shortMessage: string;
|
||||
longMessage: React.ReactNode;
|
||||
}> = [];
|
||||
|
||||
// check if the layers in the state are compatible with this type of chart
|
||||
if (state && state.layers.length > 1) {
|
||||
// Order is important here: Y Axis is fundamental to exist to make it valid
|
||||
|
@ -696,6 +790,11 @@ export const getXyVisualization = ({
|
|||
getUniqueLabels(state) {
|
||||
return getUniqueLabels(state.layers);
|
||||
},
|
||||
getUsedDataViews(state) {
|
||||
return (
|
||||
state?.layers.filter(isAnnotationsLayer).map(({ indexPatternId }) => indexPatternId) ?? []
|
||||
);
|
||||
},
|
||||
renderDimensionTrigger({
|
||||
columnId,
|
||||
label,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue