From 3cb5bea0a942b77cef187d81a3458bc71c26a721 Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 20 Jun 2022 18:40:47 +0200 Subject: [PATCH 01/98] :alembic: Initial code for query based annotations --- .../expression_functions/annotation_layer.ts | 6 +- .../extended_annotation_layer.ts | 6 +- .../common/event_annotation_group/index.ts | 25 +- .../common/fetch_event_annotation/index.ts | 51 +++ src/plugins/event_annotation/common/index.ts | 4 +- .../common/manual_event_annotation/types.ts | 5 - .../common/query_event_annotation/index.ts | 99 +++++ .../common/query_event_annotation/types.ts | 21 ++ src/plugins/event_annotation/common/types.ts | 59 ++- .../event_annotation_service/helpers.ts | 12 +- .../event_annotation_service/service.tsx | 116 ++++-- .../public/event_annotation_service/types.ts | 2 +- src/plugins/event_annotation/public/index.ts | 1 + .../indexpattern_datasource/datapanel.tsx | 2 +- .../dimension_panel/field_select.tsx | 139 ++----- .../indexpattern_datasource/field_item.tsx | 2 +- .../indexpattern_datasource/query_input.tsx | 7 +- .../field_picker/field_picker.scss | 7 + .../field_picker/field_picker.tsx | 120 ++++++ .../shared_components/field_picker/index.ts | 11 + .../field_picker}/lens_field_icon.test.tsx | 0 .../field_picker}/lens_field_icon.tsx | 4 +- .../field_picker}/truncated_label.test.tsx | 0 .../field_picker}/truncated_label.tsx | 0 .../shared_components/field_picker/types.ts | 22 ++ .../lens/public/shared_components/index.ts | 7 + .../xy_visualization/annotations/helpers.tsx | 1 + .../lens/public/xy_visualization/index.ts | 7 +- .../public/xy_visualization/to_expression.ts | 4 +- .../public/xy_visualization/visualization.tsx | 29 +- .../annotations_panel.tsx | 354 +++++++++++++----- 31 files changed, 843 insertions(+), 280 deletions(-) create mode 100644 src/plugins/event_annotation/common/fetch_event_annotation/index.ts create mode 100644 src/plugins/event_annotation/common/query_event_annotation/index.ts create mode 100644 src/plugins/event_annotation/common/query_event_annotation/types.ts create mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/field_picker.scss create mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/index.ts rename x-pack/plugins/lens/public/{indexpattern_datasource => shared_components/field_picker}/lens_field_icon.test.tsx (100%) rename x-pack/plugins/lens/public/{indexpattern_datasource => shared_components/field_picker}/lens_field_icon.tsx (82%) rename x-pack/plugins/lens/public/{indexpattern_datasource/dimension_panel => shared_components/field_picker}/truncated_label.test.tsx (100%) rename x-pack/plugins/lens/public/{indexpattern_datasource/dimension_panel => shared_components/field_picker}/truncated_label.tsx (100%) create mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/types.ts diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts index 6174b9d40e452a..d367fe7550038a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts @@ -30,7 +30,11 @@ export function annotationLayerFunction(): ExpressionFunctionDefinition< help: strings.getAnnotationLayerHideHelp(), }, annotations: { - types: ['manual_point_event_annotation', 'manual_range_event_annotation'], + types: [ + 'manual_point_event_annotation', + 'manual_range_event_annotation', + 'query_point_event_annotation', + ], help: strings.getAnnotationLayerAnnotationsHelp(), multi: true, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts index 539c11854355c1..6d2f22b373f5f1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts @@ -30,7 +30,11 @@ export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition< help: strings.getAnnotationLayerHideHelp(), }, annotations: { - types: ['manual_point_event_annotation', 'manual_range_event_annotation'], + types: [ + 'manual_point_event_annotation', + 'manual_range_event_annotation', + 'query_point_event_annotation', + ], help: strings.getAnnotationLayerAnnotationsHelp(), multi: true, }, diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts index a3a36505b1c2db..844e45f742ef32 100644 --- a/src/plugins/event_annotation/common/event_annotation_group/index.ts +++ b/src/plugins/event_annotation/common/event_annotation_group/index.ts @@ -8,17 +8,17 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; -import type { EventAnnotationOutput } from '../manual_event_annotation/types'; - -export interface EventAnnotationGroupOutput { - type: 'event_annotation_group'; - annotations: EventAnnotationOutput[]; -} +import { EventAnnotationOutput } from '../types'; export interface EventAnnotationGroupArgs { + index: string; annotations: EventAnnotationOutput[]; } +export type EventAnnotationGroupOutput = EventAnnotationGroupArgs & { + type: 'event_annotation_group'; +}; + export function eventAnnotationGroup(): ExpressionFunctionDefinition< 'event_annotation_group', null, @@ -34,8 +34,18 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition< defaultMessage: 'Event annotation group', }), args: { + index: { + types: ['string'], + help: i18n.translate('eventAnnotation.group.args.annotationDataview', { + defaultMessage: 'Annotation data view', + }), + }, annotations: { - types: ['manual_point_event_annotation', 'manual_range_event_annotation'], + types: [ + 'manual_point_event_annotation', + 'manual_range_event_annotation', + 'query_point_event_annotation', + ], help: i18n.translate('eventAnnotation.group.args.annotationConfigs', { defaultMessage: 'Annotation configs', }), @@ -45,6 +55,7 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition< fn: (input, args) => { return { type: 'event_annotation_group', + index: args.index, annotations: args.annotations, }; }, diff --git a/src/plugins/event_annotation/common/fetch_event_annotation/index.ts b/src/plugins/event_annotation/common/fetch_event_annotation/index.ts new file mode 100644 index 00000000000000..3bcf3480f76789 --- /dev/null +++ b/src/plugins/event_annotation/common/fetch_event_annotation/index.ts @@ -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, + }; + }, + }; +} diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 50d7c8b851776f..fe69f865e7b45e 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -7,8 +7,6 @@ */ export type { - EventAnnotationArgs, - EventAnnotationOutput, ManualPointEventAnnotationArgs, ManualPointEventAnnotationOutput, ManualRangeEventAnnotationArgs, @@ -20,5 +18,7 @@ export type { EventAnnotationGroupArgs } from './event_annotation_group'; export type { EventAnnotationConfig, RangeEventAnnotationConfig, + PointInTimeEventAnnotationConfig, + PointInTimeQueryEventAnnotationConfig, AvailableAnnotationIcon, } from './types'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index 208383734924c2..f1dbd4d4c460ee 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -24,8 +24,3 @@ export type ManualRangeEventAnnotationArgs = { export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs & { type: 'manual_range_event_annotation'; }; - -export type EventAnnotationArgs = ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs; -export type EventAnnotationOutput = - | ManualPointEventAnnotationOutput - | ManualRangeEventAnnotationOutput; diff --git a/src/plugins/event_annotation/common/query_event_annotation/index.ts b/src/plugins/event_annotation/common/query_event_annotation/index.ts new file mode 100644 index 00000000000000..fec11723872b57 --- /dev/null +++ b/src/plugins/event_annotation/common/query_event_annotation/index.ts @@ -0,0 +1,99 @@ +/* + * 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 { AvailableAnnotationIcons } from '../constants'; +import type { QueryPointEventAnnotationArgs, QueryPointEventAnnotationOutput } from './types'; + +export const queryPointEventAnnotation: ExpressionFunctionDefinition< + 'query_point_event_annotation', + null, + QueryPointEventAnnotationArgs, + QueryPointEventAnnotationOutput +> = { + name: 'query_point_event_annotation', + aliases: [], + type: 'query_point_event_annotation', + help: i18n.translate('eventAnnotation.queryAnnotation.description', { + defaultMessage: `Configure query annotation`, + }), + inputTypes: ['null'], + args: { + field: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.field', { + defaultMessage: `The time field chosen for annotation`, + }), + }, + label: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.label', { + defaultMessage: `The name of the annotation`, + }), + }, + color: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.color', { + defaultMessage: 'The color of the line', + }), + }, + lineStyle: { + types: ['string'], + options: ['solid', 'dotted', 'dashed'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.lineStyle', { + defaultMessage: 'The style of the annotation line', + }), + }, + lineWidth: { + types: ['number'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.lineWidth', { + defaultMessage: 'The width of the annotation line', + }), + }, + icon: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.icon', { + defaultMessage: 'An optional icon used for annotation lines', + }), + options: [...Object.values(AvailableAnnotationIcons)], + strict: true, + }, + textVisibility: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.textVisibility', { + defaultMessage: 'Visibility of the label on the annotation line', + }), + }, + isHidden: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.isHidden', { + defaultMessage: `Switch to hide annotation`, + }), + }, + query: { + types: ['kibana_query'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.query', { + defaultMessage: ``, + }), + }, + additionalFields: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.query', { + defaultMessage: ``, + }), + multi: true, + }, + }, + fn: function fn(input: unknown, args: QueryPointEventAnnotationArgs) { + return { + type: 'query_point_event_annotation', + ...args, + }; + }, +}; diff --git a/src/plugins/event_annotation/common/query_event_annotation/types.ts b/src/plugins/event_annotation/common/query_event_annotation/types.ts new file mode 100644 index 00000000000000..14fc33ed1d56b4 --- /dev/null +++ b/src/plugins/event_annotation/common/query_event_annotation/types.ts @@ -0,0 +1,21 @@ +/* + * 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 { Query } from '@kbn/es-query'; +import { ExpressionValueBoxed } from '@kbn/expressions-plugin/common'; +import { PointStyleProps } from '../types'; + +export type QueryPointEventAnnotationArgs = { + field: string; + query: ExpressionValueBoxed<'kibana_query', Query>; + additionalFields?: string[]; +} & PointStyleProps; + +export type QueryPointEventAnnotationOutput = QueryPointEventAnnotationArgs & { + type: 'query_point_event_annotation'; +}; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index e0b0de3c85c9eb..832ccdfd68ed1f 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -6,40 +6,72 @@ * Side Public License, v 1. */ +import { Query } from '@kbn/es-query'; import { $Values } from '@kbn/utility-types'; import { AvailableAnnotationIcons } from './constants'; +import { + ManualPointEventAnnotationArgs, + ManualRangeEventAnnotationArgs, + ManualPointEventAnnotationOutput, + ManualRangeEventAnnotationOutput, +} from './manual_event_annotation/types'; +import { QueryPointEventAnnotationOutput } from './query_event_annotation/types'; 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; -export interface PointStyleProps { + +export type EventAnnotationArgs = + | ManualPointEventAnnotationArgs + | ManualRangeEventAnnotationArgs + | QueryPointEventAnnotationOutput; +export type EventAnnotationOutput = + | ManualPointEventAnnotationOutput + | ManualRangeEventAnnotationOutput + | QueryPointEventAnnotationOutput; + +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 PointInTimeQueryEventAnnotationConfig = { + id: string; + type: QueryAnnotationType; + key: { + type: 'point_in_time'; + field: string; + }; + additionalFields?: string[]; + query: Query; +} & PointStyleProps; + +export type RangeStyleProps = StyleSharedProps & { outside?: boolean; - isHidden?: boolean; -} +}; export type RangeEventAnnotationConfig = { + type: ManualAnnotationType; id: string; key: { type: 'range'; @@ -50,4 +82,11 @@ export type RangeEventAnnotationConfig = { export type StyleProps = PointStyleProps & RangeStyleProps; -export type EventAnnotationConfig = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig; +export type EventAnnotationConfig = + | PointInTimeEventAnnotationConfig + | RangeEventAnnotationConfig + | PointInTimeQueryEventAnnotationConfig; + +export type PointInTimeAnnotationTypes = + | PointInTimeEventAnnotationConfig + | PointInTimeQueryEventAnnotationConfig; diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index 8eb3d05309ec14..858673c1a14932 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -7,7 +7,11 @@ */ import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; -import { EventAnnotationConfig, RangeEventAnnotationConfig } from '../../common'; +import { + EventAnnotationConfig, + RangeEventAnnotationConfig, + PointInTimeQueryEventAnnotationConfig, +} from '../../common'; export const defaultAnnotationColor = euiLightVars.euiColorAccent; export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 @@ -23,3 +27,9 @@ export const isRangeAnnotation = ( ): annotation is RangeEventAnnotationConfig => { return Boolean(annotation && annotation?.key.type === 'range'); }; + +export const isQueryAnnotation = ( + annotation?: EventAnnotationConfig +): annotation is PointInTimeQueryEventAnnotationConfig => { + return annotation?.type === 'query'; +}; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 4770c1c182af66..507ad19360f209 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -6,59 +6,98 @@ * Side Public License, v 1. */ +import { partition } from 'lodash'; import { EventAnnotationServiceType } from './types'; import { defaultAnnotationColor, defaultAnnotationRangeColor, defaultAnnotationLabel, + isRangeAnnotation, + isQueryAnnotation, } 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 } = annotation; - const { timestamp: time, endTimestamp: endTime } = key; - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'manual_range_event_annotation', - arguments: { - time: [time], - endTime: [endTime], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationRangeColor], - outside: [Boolean(outside)], - isHidden: [Boolean(isHidden)], + toExpression: (annotations) => { + const visibleAnnotations = annotations.filter(({ isHidden }) => !isHidden); + const [queryBasedAnnotations, manualBasedAnnotations] = partition( + visibleAnnotations, + isQueryAnnotation + ); + + const expressions = []; + + for (const annotation of manualBasedAnnotations) { + if (isRangeAnnotation(annotation)) { + const { label, isHidden, color, key, outside } = annotation; + const { timestamp: time, endTimestamp: endTime } = key; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'manual_range_event_annotation', + arguments: { + time: [time], + endTime: [endTime], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationRangeColor], + outside: [Boolean(outside)], + isHidden: [Boolean(isHidden)], + }, }, - }, - ], - }; - } else { - const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility } = - annotation; - return { - type: 'expression', + ], + }); + } else { + const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility } = + annotation; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'manual_point_event_annotation', + arguments: { + time: [key.timestamp], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + isHidden: [Boolean(isHidden)], + }, + }, + ], + }); + } + } + + for (const annotation of queryBasedAnnotations) { + const { + label, + isHidden, + color, + lineStyle, + lineWidth, + icon, + key, + textVisibility, + query, + additionalFields, + } = annotation; + expressions.push({ + type: 'expression' as const, chain: [ { - type: 'function', - function: 'manual_point_event_annotation', + type: 'function' as const, + function: 'query_point_event_annotation', arguments: { - time: [key.timestamp], + field: [key.field], label: [label || defaultAnnotationLabel], color: [color || defaultAnnotationColor], lineWidth: [lineWidth || 1], @@ -66,11 +105,14 @@ export function getEventAnnotationService(): EventAnnotationServiceType { icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], isHidden: [Boolean(isHidden)], + query: query ? [queryToAst(query)] : [], + additionalFields: additionalFields || [], }, }, ], - }; + }); } + return expressions; }, }; } diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index d5fcaa23107c82..7cecd2b418af9e 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -10,5 +10,5 @@ import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast'; import { EventAnnotationConfig } from '../../common'; export interface EventAnnotationServiceType { - toExpression: (props: EventAnnotationConfig) => ExpressionAstExpression; + toExpression: (props: EventAnnotationConfig[]) => ExpressionAstExpression[]; } diff --git a/src/plugins/event_annotation/public/index.ts b/src/plugins/event_annotation/public/index.ts index 56ddc4b8a60e15..4f21adf026e201 100644 --- a/src/plugins/event_annotation/public/index.ts +++ b/src/plugins/event_annotation/public/index.ts @@ -18,4 +18,5 @@ export { defaultAnnotationColor, defaultAnnotationRangeColor, isRangeAnnotation, + isQueryAnnotation, } from './event_annotation_service/helpers'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 385f92b0dc05d7..551e3f85a64fbd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -46,7 +46,7 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { loadIndexPatterns, syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; -import { LensFieldIcon } from './lens_field_icon'; +import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { FieldGroups, FieldList } from './field_list'; export type Props = Omit, 'core'> & { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 6bbe335d663078..afaa24d3d34b1d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -7,30 +7,19 @@ import './field_select.scss'; import { partition } from 'lodash'; -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiComboBoxOptionOption, - EuiComboBoxProps, -} from '@elastic/eui'; -import classNames from 'classnames'; -import { LensFieldIcon } from '../lens_field_icon'; +import { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { fieldExists } from '../pure_helpers'; -import { TruncatedLabel } from './truncated_label'; import type { OperationType } from '../indexpattern'; -import type { DataType } from '../../types'; import type { OperationSupportMatrix } from './operation_support'; import type { IndexPattern, IndexPatternPrivateState } from '../types'; -export interface FieldChoice { - type: 'field'; - field: string; +import { FieldOption, FieldOptionValue, FieldPicker } from '../../shared_components/field_picker'; + +export type FieldChoiceWithOperationType = FieldOptionValue & { operationType: OperationType; -} +}; export interface FieldSelectProps extends EuiComboBoxProps { currentIndexPattern: IndexPattern; @@ -38,7 +27,7 @@ export interface FieldSelectProps extends EuiComboBoxProps void; + onChoose: (choice: FieldChoiceWithOperationType) => void; onDeleteColumn?: () => void; existingFields: IndexPatternPrivateState['existingFields']; fieldIsInvalid: boolean; @@ -46,10 +35,6 @@ export interface FieldSelectProps extends EuiComboBoxProps(null); - const [labelProps, setLabelProps] = React.useState<{ - width: number; - font: string; - }>({ - width: DEFAULT_COMBOBOX_WIDTH - COMBOBOX_PADDINGS, - font: DEFAULT_FONT, - }); - - const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { - if (comboBoxRef.current) { - const current = { - ...labelProps, - width: comboBoxRef.current?.clientWidth - COMBOBOX_PADDINGS, - }; - if (shouldRecomputeAll) { - current.font = window.getComputedStyle(comboBoxRef.current).font; - } - setLabelProps(current); - } - }; - - useEffectOnce(() => { - if (comboBoxRef.current) { - computeStyles(undefined, true); - } - window.addEventListener('resize', computeStyles); - }); return ( -
- + selectedOptions={ + (selectedOperationType && selectedField + ? [ + { + label: fieldIsInvalid + ? selectedField + : currentIndexPattern.getFieldByName(selectedField)?.displayName ?? selectedField, + value: { type: 'field', field: selectedField }, + }, + ] + : []) as unknown as Array> + } + options={memoizedFieldOptions as Array>} + onChoose={(choice) => { + if (choice && choice.field !== selectedField) { + trackUiEvent('indexpattern_dimension_field_changed'); + onChoose(choice); } - singleSelection={{ asPlainText: true }} - onChange={(choices) => { - if (choices.length === 0) { - onDeleteColumn?.(); - return; - } - - const choice = choices[0].value as unknown as FieldChoice; - - if (choice.field !== selectedField) { - trackUiEvent('indexpattern_dimension_field_changed'); - onChoose(choice); - } - }} - renderOption={(option, searchValue) => { - return ( - - - - - - - - - ); - }} - {...rest} - /> -
+ }} + onDelete={onDeleteColumn} + fieldIsInvalid={Boolean(incompleteOperation || fieldIsInvalid)} + data-test-subj={dataTestSub ?? 'indexPattern-dimension-field'} + /> ); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index e3d10c61c5caaf..d7cde6ec7f755d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -47,7 +47,7 @@ import { DragDrop, DragDropIdentifier } from '../drag_drop'; import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, DOCUMENT_FIELD_NAME, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField, DraggedField } from './types'; -import { LensFieldIcon } from './lens_field_icon'; +import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; import { getVisualizeGeoFieldMessage } from '../utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx index 850f9d096d2327..439b12cccbe60d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx @@ -20,6 +20,7 @@ export const QueryInput = ({ onSubmit, disableAutoFocus, ['data-test-subj']: dataTestSubj, + placeholder, }: { value: Query; onChange: (input: Query) => void; @@ -28,6 +29,7 @@ export const QueryInput = ({ onSubmit: () => void; disableAutoFocus?: boolean; 'data-test-subj'?: string; + placeholder?: string; }) => { const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange }); @@ -51,7 +53,8 @@ export const QueryInput = ({ } }} placeholder={ - inputValue.language === 'kuery' + placeholder ?? + (inputValue.language === 'kuery' ? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', { defaultMessage: '{example}', values: { example: 'method : "GET" or status : "404"' }, @@ -59,7 +62,7 @@ export const QueryInput = ({ : i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', { defaultMessage: '{example}', values: { example: 'method:GET OR status:404' }, - }) + })) } languageSwitcherPopoverAnchorPosition="rightDown" /> diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.scss b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.scss new file mode 100644 index 00000000000000..d8e4e9b8f72079 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.scss @@ -0,0 +1,7 @@ +.lnFieldPicker__option--incompatible { + color: $euiColorLightShade; +} + +.lnFieldPicker__option--nonExistant { + background-color: $euiColorLightestShade; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx new file mode 100644 index 00000000000000..1a298c27c6b4ea --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx @@ -0,0 +1,120 @@ +/* + * 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 './field_picker.scss'; +import React, { useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { EuiComboBox, EuiComboBoxProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import { DataType } from '../../types'; +import { LensFieldIcon } from './lens_field_icon'; +import { TruncatedLabel } from './truncated_label'; +import type { FieldOptionValue, FieldOption } from './types'; + +export interface FieldPickerProps + extends EuiComboBoxProps['value']> { + options: Array>; + selectedField?: string; + onChoose: (choice: T | undefined) => void; + onDelete?: () => void; + fieldIsInvalid: boolean; + 'data-test-subj'?: string; +} + +const DEFAULT_COMBOBOX_WIDTH = 305; +const COMBOBOX_PADDINGS = 90; +const DEFAULT_FONT = '14px Inter'; + +export function FieldPicker({ + selectedOptions, + options, + onChoose, + onDelete, + fieldIsInvalid, + ['data-test-subj']: dataTestSub, + ...rest +}: FieldPickerProps) { + const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => ({ + ...otherAttr, + compatible, + exists, + className: classNames({ + 'lnFieldPicker__option--incompatible': !compatible, + 'lnFieldPicker__option--nonExistant': !exists, + }), + })); + const comboBoxRef = useRef(null); + const [labelProps, setLabelProps] = React.useState<{ + width: number; + font: string; + }>({ + width: DEFAULT_COMBOBOX_WIDTH - COMBOBOX_PADDINGS, + font: DEFAULT_FONT, + }); + + const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { + if (comboBoxRef.current) { + const current = { + ...labelProps, + width: comboBoxRef.current?.clientWidth - COMBOBOX_PADDINGS, + }; + if (shouldRecomputeAll) { + current.font = window.getComputedStyle(comboBoxRef.current).font; + } + setLabelProps(current); + } + }; + + useEffectOnce(() => { + if (comboBoxRef.current) { + computeStyles(undefined, true); + } + window.addEventListener('resize', computeStyles); + }); + + return ( +
+ { + if (choices.length === 0) { + onDelete?.(); + return; + } + onChoose(choices[0].value); + }} + renderOption={(option, searchValue) => { + return ( + + + + + + + + + ); + }} + {...rest} + /> +
+ ); +} diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/index.ts b/x-pack/plugins/lens/public/shared_components/field_picker/index.ts new file mode 100644 index 00000000000000..d9e18182dd0d88 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/field_picker/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { LensFieldIcon } from './lens_field_icon'; +export { FieldPicker } from './field_picker'; +export { TruncatedLabel } from './truncated_label'; +export type { FieldOptionValue, FieldOption } from './types'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx rename to x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx similarity index 82% rename from x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx rename to x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx index be30712a9de015..fabb8cab6fd0e3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx +++ b/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; -import { DataType } from '../types'; -import { normalizeOperationDataType } from './pure_utils'; +import { DataType } from '../../types'; +import { normalizeOperationDataType } from '../../indexpattern_datasource/pure_utils'; export function LensFieldIcon({ type, ...rest }: FieldIconProps & { type: DataType }) { return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/truncated_label.test.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/truncated_label.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/truncated_label.test.tsx rename to x-pack/plugins/lens/public/shared_components/field_picker/truncated_label.test.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/truncated_label.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/truncated_label.tsx similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/truncated_label.tsx rename to x-pack/plugins/lens/public/shared_components/field_picker/truncated_label.tsx diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/types.ts b/x-pack/plugins/lens/public/shared_components/field_picker/types.ts new file mode 100644 index 00000000000000..fdbc532f7b13e8 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/field_picker/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { DataType } from '../../types'; + +export interface FieldOptionValue { + type: 'field'; + field: string; + dataType?: DataType; +} + +export interface FieldOption extends EuiComboBoxOptionOption { + label: string; + value: T; + exists: boolean; + compatible: number | boolean; + 'data-test-subj'?: string; +} diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 7da63a69472115..a8bc3499fc1a10 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -9,6 +9,13 @@ export type { ToolbarPopoverProps } from './toolbar_popover'; export { ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; +export { + FieldPicker, + LensFieldIcon, + TruncatedLabel, + FieldOption, + FieldOptionValue, +} from './field_picker'; export { RangeInputField } from './range_input_field'; export { BucketAxisBoundsControl, diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 35a40623b72aad..bb5e48c7364693 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -139,6 +139,7 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( if (!currentConfig) { resultAnnotations.push({ + type: 'manual', label: defaultAnnotationLabel, key: { type: 'point_in_time', diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index f9f1558468cfc6..30c58d2a728b74 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -6,6 +6,7 @@ */ import type { CoreSetup } from '@kbn/core/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public'; import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; @@ -29,11 +30,15 @@ export class XyVisualization { ) { editorFrame.registerVisualization(async () => { const { getXyVisualization } = await import('../async_services'); - const [, { charts, fieldFormats, eventAnnotation }] = await core.getStartServices(); + const [coreStart, { data, charts, fieldFormats, eventAnnotation }] = + await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); const eventAnnotationService = await eventAnnotation.getService(); const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); return getXyVisualization({ + core: coreStart, + data, + storage: new Storage(localStorage), paletteService: palettes, eventAnnotationService, fieldFormats, diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index a6a4766661b176..89da7b56ecd218 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -391,9 +391,7 @@ const annotationLayerToExpression = ( arguments: { hide: [Boolean(layer.hide)], layerId: [layer.layerId], - annotations: layer.annotations - ? layer.annotations.map((ann): Ast => eventAnnotationService.toExpression(ann)) - : [], + annotations: eventAnnotationService.toExpression(layer.annotations || []), }, }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 908a97d5c1c2d3..516654cef4130c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -12,9 +12,9 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { PaletteRegistry } from '@kbn/coloring'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { ThemeServiceStart } from '@kbn/core/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, @@ -22,6 +22,8 @@ import { YAxisMode, ExtendedYConfig, } from '@kbn/expression-xy-plugin/common'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +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'; @@ -72,12 +74,18 @@ import { DimensionTrigger } from '../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; export const getXyVisualization = ({ + core, + storage, + data, paletteService, fieldFormats, useLegacyTimeAxis, kibanaTheme, eventAnnotationService, }: { + core: CoreStart; + storage: IStorageWrapper; + data: DataPublicPluginStart; paletteService: PaletteRegistry; eventAnnotationService: EventAnnotationServiceType; fieldFormats: FieldFormatsStart; @@ -510,7 +518,22 @@ export const getXyVisualization = ({ render( - {dimensionEditor} + + + {dimensionEditor} + + , domElement ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 7ce8eba19f1a91..b927232920cec5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -24,16 +24,19 @@ import moment from 'moment'; import { EventAnnotationConfig, PointInTimeEventAnnotationConfig, + PointInTimeQueryEventAnnotationConfig, RangeEventAnnotationConfig, } from '@kbn/event-annotation-plugin/common/types'; import { pick } from 'lodash'; -import { search } from '@kbn/data-plugin/public'; +import { Query, search } from '@kbn/data-plugin/public'; import { defaultAnnotationColor, defaultAnnotationRangeColor, isRangeAnnotation, + isQueryAnnotation, } from '@kbn/event-annotation-plugin/public'; import Color from 'color'; +import { FieldOptionValue, FieldPicker } from '../../../shared_components/field_picker'; import { getDataLayers } from '../../visualization_helpers'; import { FormatFactory } from '../../../../common'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; @@ -46,6 +49,9 @@ import { updateLayer } from '..'; import { annotationsIconSet } from './icon_set'; import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types'; import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types'; +import { QueryInput } from '../../../indexpattern_datasource/query_input'; + +type ManualEventAnnotationType = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig; export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { return new Color(transparentize(color, 0.1)).hexa(); @@ -91,6 +97,7 @@ export const getEndTimestamp = ( const sanitizeProperties = (annotation: EventAnnotationConfig) => { if (isRangeAnnotation(annotation)) { const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ + 'type', 'label', 'key', 'id', @@ -99,8 +106,10 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => { 'outside', ]); return rangeAnnotation; - } else { - const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + } + if (isQueryAnnotation(annotation)) { + const lineAnnotation: PointInTimeQueryEventAnnotationConfig = pick(annotation, [ + 'type', 'id', 'label', 'key', @@ -110,9 +119,24 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => { 'color', 'icon', 'textVisibility', + 'query', + 'additionalFields', ]); return lineAnnotation; } + const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + 'type', + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + ]); + return lineAnnotation; }; export const AnnotationsPanel = ( @@ -137,6 +161,7 @@ export const AnnotationsPanel = ( const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor); const isRange = isRangeAnnotation(currentAnnotation); + const isQueryBased = isQueryAnnotation(currentAnnotation); const setAnnotations = useCallback( (annotation) => { @@ -167,98 +192,62 @@ export const AnnotationsPanel = ( defaultMessage: 'Placement', })} > - {isRange ? ( - <> - { - if (date) { - const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); - if (currentEndTime < date.valueOf()) { - const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); - const dif = currentEndTime - currentStartTime; - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'range' }), - timestamp: date.toISOString(), - endTimestamp: moment(date.valueOf() + dif).toISOString(), - }, - }); - } else { - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'range' }), - timestamp: date.toISOString(), - }, - }); - } - } - }} - label={i18n.translate('xpack.lens.xyChart.annotationDate', { - defaultMessage: 'Annotation date', - })} - /> - { - if (date) { - const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); - if (currentStartTime > date.valueOf()) { - const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); - const dif = currentEndTime - currentStartTime; - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'range' }), - endTimestamp: date.toISOString(), - timestamp: moment(date.valueOf() - dif).toISOString(), - }, - }); - } else { - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'range' }), - endTimestamp: date.toISOString(), - }, - }); - } - } - }} - /> - - ) : ( - + { - if (date) { - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'point_in_time' }), - timestamp: date.toISOString(), - }, - }); - } + data-test-subj="lns-xyAnnotation-placementType" + name="placementType" + buttonSize="compressed" + options={[ + { + id: `lens_xyChart_annotation_staticDate`, + label: i18n.translate('xpack.lens.xyChart.annotation.staticDate', { + defaultMessage: 'Static Date', + }), + 'data-test-subj': 'lnsXY_annotation_staticDate', + }, + { + id: `lens_xyChart_annotation_query`, + label: i18n.translate('xpack.lens.xyChart.annotation.query', { + defaultMessage: 'Query', + }), + 'data-test-subj': 'lnsXY_annotation_query', + }, + ]} + idSelected={`lens_xyChart_annotation_${ + currentAnnotation?.type === 'query' ? 'query' : 'staticDate' + }`} + onChange={(id) => { + setAnnotations({ + type: id === `lens_xyChart_annotation_query` ? 'query' : 'manual', + }); }} + isFullWidth + /> + + {isQueryBased ? ( + + ) : ( + )} - - | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const inputQuery = annotation?.query ?? defaultQuery; + return ( + <> + + + + + {}} + placeholder={ + inputQuery.language === 'kuery' + ? i18n.translate('xpack.lens.annotations.query.queryPlaceholderKql', { + defaultMessage: '{example}', + values: { example: 'method : "GET"' }, + }) + : i18n.translate('xpack.lens.annotations.query.queryPlaceholderLucene', { + defaultMessage: '{example}', + values: { example: 'method:GET' }, + }) + } + /> + + + ); +}; + +const ConfigPanelManualAnnotation = ({ + annotation, + frame, + state, + onChange, +}: { + annotation?: ManualEventAnnotationType | undefined; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + <> + {isRange ? ( + <> + { + if (date) { + const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); + if (currentEndTime < date.valueOf()) { + const currentStartTime = moment(annotation?.key.timestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + endTimestamp: moment(date.valueOf() + dif).toISOString(), + }, + }); + } else { + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + }, + }); + } + } + }} + label={i18n.translate('xpack.lens.xyChart.annotationDate', { + defaultMessage: 'Annotation date', + })} + /> + { + if (date) { + const currentStartTime = moment(annotation?.key.timestamp).valueOf(); + if (currentStartTime > date.valueOf()) { + const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + timestamp: moment(date.valueOf() - dif).toISOString(), + }, + }); + } else { + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + }, + }); + } + } + }} + /> + + ) : ( + { + if (date) { + onChange({ + key: { + ...(annotation?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + /> + )} + + + ); +}; + const ConfigPanelApplyAsRangeSwitch = ({ annotation, onChange, frame, state, }: { - annotation?: EventAnnotationConfig; - onChange: (annotations: Partial | undefined) => void; + annotation?: ManualEventAnnotationType; + onChange: (annotations: Partial | undefined) => void; frame: FramePublicAPI; state: XYState; }) => { @@ -391,6 +557,7 @@ const ConfigPanelApplyAsRangeSwitch = ({ onChange={() => { if (isRange) { const newPointAnnotation: PointInTimeEventAnnotationConfig = { + type: 'manual', key: { type: 'point_in_time', timestamp: annotation.key.timestamp, @@ -408,6 +575,7 @@ const ConfigPanelApplyAsRangeSwitch = ({ const fromTimestamp = moment(annotation?.key.timestamp); const dataLayers = getDataLayers(state.layers); const newRangeAnnotation: RangeEventAnnotationConfig = { + type: 'manual', key: { type: 'range', timestamp: annotation.key.timestamp, From d1d62ddd2e95dc5b7e55d6d54a50c76a0ed96e60 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 6 Jul 2022 11:28:14 +0200 Subject: [PATCH 02/98] :bug: Solved more conflicts --- .../lens/public/xy_visualization/annotations/helpers.tsx | 1 + x-pack/plugins/lens/public/xy_visualization/visualization.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 692d0f02725b20..650dab9be6ca73 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -120,6 +120,7 @@ export const getAnnotationsSupportedLayer = ( const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotationConfig => ({ label: defaultAnnotationLabel, + type: 'manual', key: { type: 'point_in_time', timestamp, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 07cdc74665b8ee..cad5530181b532 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -16,7 +16,7 @@ import { CoreStart, ThemeServiceStart } from '@kbn/core/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; -import { FillStyle, SeriesType, YAxisMode } from '@kbn/expression-xy-plugin/common'; +import { FillStyle } from '@kbn/expression-xy-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { getSuggestions } from './xy_suggestions'; From 53a618bd5c239e5dd835281446a8fd41bd1bca67 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 6 Jul 2022 15:28:47 +0200 Subject: [PATCH 03/98] :alembic: More scaffolding layout --- src/plugins/event_annotation/common/types.ts | 1 + .../annotations_panel.tsx | 76 ++++++++------ .../shared/marker_decoration_settings.tsx | 99 +++++++++++++------ 3 files changed, 119 insertions(+), 57 deletions(-) diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 832ccdfd68ed1f..a64e94db7966cf 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -64,6 +64,7 @@ export type PointInTimeQueryEventAnnotationConfig = { }; additionalFields?: string[]; query: Query; + textSource?: 'name' | 'field'; } & PointStyleProps; export type RangeStyleProps = StyleSharedProps & { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 2bb1206a23f244..3058306fa238c9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -18,6 +18,7 @@ import { EuiFormControlLayout, EuiText, transparentize, + EuiSpacer, } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; import moment from 'moment'; @@ -28,8 +29,8 @@ import { RangeEventAnnotationConfig, } from '@kbn/event-annotation-plugin/common/types'; import { pick } from 'lodash'; -import { Query, search } from '@kbn/data-plugin/public'; -import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; +import { search } from '@kbn/data-plugin/public'; +import type { DatatableUtilitiesService, Query } from '@kbn/data-plugin/common'; import { defaultAnnotationColor, defaultAnnotationRangeColor, @@ -121,6 +122,7 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => { 'color', 'icon', 'textVisibility', + 'textSource', 'query', 'additionalFields', ]); @@ -266,33 +268,49 @@ export const AnnotationsPanel = ( }} /> {!isRange && ( - - )} - {!isRange && ( - - )} - {!isRange && ( - + <> + + + {(textDecorationSelected) => { + if (textDecorationSelected !== 'field') { + return null; + } + return ( + <> + + + + ); + }} + + + )} - {isRange && ( { const inputQuery = annotation?.query ?? defaultQuery; + // list only supported field by operation, remove the rest + const options = []; return ( <> { icon?: T; iconPosition?: IconPosition; textVisibility?: boolean; + textSource?: 'name' | 'field'; +} + +function getSelectedOption( + { textSource, textVisibility }: MarkerDecorationConfig = {}, + isQueryBased: boolean +) { + if (!textVisibility) { + return 'none'; + } + if (!isQueryBased && textSource === 'field') { + return 'name'; + } + return textSource ?? 'name'; } export function TextDecorationSetting({ currentConfig, setConfig, customIconSet, + isQueryBased, + children, }: { currentConfig?: MarkerDecorationConfig; setConfig: (config: MarkerDecorationConfig) => void; customIconSet?: IconSet; + isQueryBased: boolean; + children: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; }) { + const options = [ + { + id: `${idPrefix}none`, + label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lnsXY_textVisibility_none', + }, + { + id: `${idPrefix}name`, + label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.name', { + defaultMessage: 'Name', + }), + 'data-test-subj': 'lnsXY_textVisibility_name', + }, + ]; + if (isQueryBased) { + options.push({ + id: `${idPrefix}field`, + label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.field', { + defaultMessage: 'Field', + }), + 'data-test-subj': 'lnsXY_textVisibility_field', + }); + } + + // Override the only conflictual scenario + const selectedVisibleOption = getSelectedOption(currentConfig, isQueryBased); return ( ({ display="columnCompressed" fullWidth > - { - setConfig({ textVisibility: id === `${idPrefix}name` }); - }} - isFullWidth - /> +
+ { + setConfig({ + textVisibility: id !== `${idPrefix}none`, + textSource: id.replace(idPrefix, '') as 'name' | 'field', + }); + }} + isFullWidth + /> + {children(selectedVisibleOption)} +
); } From 776380aeaa49cc8f32119529a17d51fb812c1924 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 8 Jul 2022 15:55:25 +0200 Subject: [PATCH 04/98] :alembic: Initial indexpatetrn move into frame --- .../editor_frame/config_panel/layer_panel.tsx | 81 +++++++++++-------- .../editor_frame/state_helpers.ts | 22 +++++ .../indexpattern_datasource/datapanel.tsx | 8 +- .../indexpattern_datasource/indexpattern.tsx | 3 + .../layerpanel.test.tsx | 2 +- .../indexpattern_datasource/layerpanel.tsx | 2 +- .../public/indexpattern_datasource/loader.ts | 33 ++++---- .../public/indexpattern_datasource/types.ts | 7 +- .../dataview_picker/dataview_picker.tsx} | 2 +- .../dataview_picker/index.ts | 9 +++ .../dataview_picker/types.ts | 12 +++ .../lens/public/shared_components/index.ts | 1 + .../init_middleware/load_initial.ts | 4 +- .../public/state_management/lens_slice.ts | 6 ++ .../lens/public/state_management/selectors.ts | 8 +- .../lens/public/state_management/types.ts | 4 + x-pack/plugins/lens/public/types.ts | 31 ++++++- .../lens/public/xy_visualization/types.ts | 1 + .../public/xy_visualization/visualization.tsx | 25 +++++- .../visualization_helpers.tsx | 13 ++- .../annotations_panel.tsx | 36 ++++++++- .../xy_config_panel/layer_header.tsx | 72 ++++++++++++++--- 22 files changed, 296 insertions(+), 86 deletions(-) rename x-pack/plugins/lens/public/{indexpattern_datasource/change_indexpattern.tsx => shared_components/dataview_picker/dataview_picker.tsx} (97%) create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 0c54ca0df5c710..d9627f3ac26455 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -318,45 +318,56 @@ export function LayerPanel( /> + {(layerDatasource || activeVisualization.renderLayerPanel) && } {layerDatasource && ( - <> - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, + frame: framePublicAPI, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame: framePublicAPI, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + )} + {activeVisualization.renderLayerPanel && ( + { + props.updateVisualization(newState); + }, + }} + /> )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 1790a18ad12486..14e188f7a252e6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -30,6 +30,27 @@ import { getUnknownVisualizationTypeError, } from '../error_helper'; import { DatasourceStates } from '../../state_management'; +import { IndexPattern } from '../../indexpattern_datasource/types'; + +export function mergeIndexPatterns( + datasourceMap: DatasourceMap, + datasourceStates: DatasourceStates +) { + const dataViewWithDuplicates = Object.entries(datasourceMap).map(([datasourceId, datasource]) => + datasource.getIndexPatterns(datasourceStates[datasourceId].state) + ); + // it's always the same, so just pick the first one + const indexPatternRefs = dataViewWithDuplicates[0].indexPatternRefs; + const indexPatternDedup: Record = {}; + for (const { indexPatterns } of dataViewWithDuplicates) { + for (const [indexPatternId, indexPattern] of Object.entries(indexPatterns)) { + if (!(indexPatternId in indexPatternDedup)) { + indexPatternDedup[indexPatternId] = indexPattern; + } + } + } + return { indexPatternRefs, indexPatterns: indexPatternDedup }; +} export async function initializeDatasources( datasourceMap: DatasourceMap, @@ -39,6 +60,7 @@ export async function initializeDatasources( options?: InitializationOptions ) { const states: DatasourceStates = {}; + await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { if (datasourceStates[datasourceId]) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 551e3f85a64fbd..afdec325e70d9c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -36,18 +36,14 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; -import type { - IndexPattern, - IndexPatternPrivateState, - IndexPatternField, - IndexPatternRef, -} from './types'; +import type { IndexPattern, IndexPatternPrivateState, IndexPatternField } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; import { loadIndexPatterns, syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { FieldGroups, FieldList } from './field_list'; +import { IndexPatternRef } from '../shared_components'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index fe6d6adef39f41..f970088d50e924 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -39,6 +39,7 @@ import { extractReferences, injectReferences, loadIndexPatterns, + getIndexPatterns, } from './loader'; import { toExpression } from './to_expression'; import { @@ -181,6 +182,8 @@ export function getIndexPatternDatasource({ }); }, + getIndexPatterns, + getPersistableState(state: IndexPatternPrivateState) { return extractReferences(state); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index d0c8e1ffafd693..3d366408595b0f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -12,7 +12,7 @@ import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; -import { ChangeIndexPattern } from './change_indexpattern'; +import { ChangeIndexPattern } from '../shared_components/dataview_picker/dataview_picker'; import { getFieldByNameFactory } from './pure_helpers'; import { TermsIndexPatternColumn } from './operations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index 4fd7b920e124b2..63099f363ecf4a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; -import { ChangeIndexPattern } from './change_indexpattern'; +import { ChangeIndexPattern } from '../shared_components/dataview_picker/dataview_picker'; export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index d6acf5958bd2ba..ee90adc420e652 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -18,7 +18,6 @@ import type { } from '../types'; import { IndexPattern, - IndexPatternRef, IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField, @@ -32,6 +31,7 @@ import { documentField } from './document_field'; import { readFromStorage, writeToStorage } from '../settings_storage'; import { getFieldByNameFactory } from './pure_helpers'; import { memoizedGetAvailableOperationsByMetadata } from './operations'; +import type { IndexPatternRef } from '../shared_components'; type SetState = DatasourceDataPanelProps['setState']; type IndexPatternsService = Pick; @@ -200,6 +200,13 @@ export function injectReferences( }; } +export function getIndexPatterns({ + indexPatternRefs, + indexPatterns, +}: Partial = {}) { + return { indexPatternRefs: indexPatternRefs ?? [], indexPatterns: indexPatterns ?? {} }; +} + export async function loadInitialState({ persistedState, references, @@ -226,13 +233,14 @@ export async function loadInitialState({ const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; const indexPatternIds = []; - if (initialContext && 'isVisualizeAction' in initialContext) { - for (let layerIdx = 0; layerIdx < initialContext.layers.length; layerIdx++) { - const layerContext = initialContext.layers[layerIdx]; - indexPatternIds.push(layerContext.indexPatternId); + if (initialContext) { + if ('isVisualizeAction' in initialContext) { + for (const { indexPatternId } of initialContext.layers) { + indexPatternIds.push(indexPatternId); + } + } else { + indexPatternIds.push(initialContext.indexPatternId); } - } else if (initialContext) { - indexPatternIds.push(initialContext.indexPatternId); } const state = persistedState && references ? injectReferences(persistedState, references) : undefined; @@ -244,13 +252,10 @@ export async function loadInitialState({ // take out the undefined from the list .filter(Boolean); - const notUsedPatterns: string[] = difference( - uniq(indexPatternRefs.map(({ id }) => id)), - usedPatterns - ); - const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); + const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedPatterns); + const indexPatterns = await loadIndexPatterns({ indexPatternsService, cache: {}, @@ -262,12 +267,10 @@ export async function loadInitialState({ // * start with the indexPattern in context // * then fallback to the used ones // * then as last resort use a first one from not used refs - const availableIndexPatternIds = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].filter( + const currentIndexPatternId = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].find( (id) => id != null && availableIndexPatterns.has(id) && indexPatterns[id] ); - const currentIndexPatternId = availableIndexPatternIds[0]; - if (currentIndexPatternId) { setLastUsedIndexPatternId(storage, currentIndexPatternId); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index cbf02bddb8814a..c47e474189599c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -11,6 +11,7 @@ import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; import type { DragDropIdentifier } from '../drag_drop/providers'; import type { IncompleteColumn, GenericIndexPatternColumn } from './operations'; import { DragDropOperation } from '../types'; +import { IndexPatternRef } from '../shared_components'; export type { GenericIndexPatternColumn, @@ -105,12 +106,6 @@ export interface IndexPatternPrivateState { isDimensionClosePrevented?: boolean; } -export interface IndexPatternRef { - id: string; - title: string; - name?: string; -} - export interface DataViewDragDropOperation extends DragDropOperation { dataView: IndexPattern; column?: GenericIndexPatternColumn; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx similarity index 97% rename from x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx rename to x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index ae087221fd49ae..9ad4f4a154ce4f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -10,8 +10,8 @@ import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { trackUiEvent } from '../../lens_ui_telemetry'; import { IndexPatternRef } from './types'; -import { trackUiEvent } from '../lens_ui_telemetry'; export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { label: string; diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts new file mode 100644 index 00000000000000..4454d791489231 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts @@ -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 { ChangeIndexPattern } from './dataview_picker'; +export type { IndexPatternRef } from './types'; diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts new file mode 100644 index 00000000000000..2a61512bbab06d --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts @@ -0,0 +1,12 @@ +/* + * 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 interface IndexPatternRef { + id: string; + title: string; + name?: string; +} diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index a8bc3499fc1a10..2aed3948efc04c 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -16,6 +16,7 @@ export { FieldOption, FieldOptionValue, } from './field_picker'; +export { ChangeIndexPattern, IndexPatternRef } from './dataview_picker'; export { RangeInputField } from './range_input_field'; export { BucketAxisBoundsControl, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index e25c57ac129c9b..0b2ec59c1ce222 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -13,7 +13,7 @@ import { disableAutoApply, getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; -import { initializeDatasources } from '../../editor_frame_service/editor_frame'; +import { initializeDatasources, mergeIndexPatterns } from '../../editor_frame_service/editor_frame'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document } from '../../persistence'; @@ -102,6 +102,7 @@ export function loadInitial( getPreloadedState(storeDeps); const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; const { lens } = store.getState(); + if ( !initialInput || (attributeService.inputIsRefType(initialInput) && @@ -115,6 +116,7 @@ export function loadInitial( initEmpty({ newState: { ...emptyState, + ...mergeIndexPatterns(datasourceMap, result), searchSessionId: data.search.session.getSessionId() || data.search.session.start(), datasourceStates: Object.entries(result).reduce( (state, [datasourceId, datasourceState]) => ({ diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 7ea8053caa5884..bdb03523a72e79 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -38,6 +38,8 @@ export const initialState: LensAppState = { state: null, activeId: null, }, + indexPatternRefs: [], + indexPatterns: {}, }; export const getPreloadedState = ({ @@ -634,6 +636,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), + indexPatternRefs: current(state.indexPatternRefs), + indexPatterns: current(state.indexPatterns), }; const activeDatasource = datasourceMap[state.activeDatasourceId]; @@ -693,6 +697,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), + indexPatternRefs: current(state.indexPatternRefs), + indexPatterns: current(state.indexPatterns), }, activeVisualization, activeDatasource, diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 9217c66863c5c9..e4334270b6c59b 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -27,6 +27,8 @@ export const selectChangesApplied = (state: LensState) => export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; export const selectActiveData = (state: LensState) => state.lens.activeData; +export const selectIndexPatternRefs = (state: LensState) => state.lens.indexPatternRefs; +export const selectIndexPatterns = (state: LensState) => state.lens.indexPatterns; export const selectIsFullscreenDatasource = (state: LensState) => Boolean(state.lens.isFullscreenDatasource); @@ -165,12 +167,16 @@ export const selectFramePublicAPI = createSelector( selectActiveData, selectInjectedDependencies as SelectInjectedDependenciesFunction, selectResolvedDateRange, + selectIndexPatternRefs, + selectIndexPatterns, ], - (datasourceStates, activeData, datasourceMap, dateRange) => { + (datasourceStates, activeData, datasourceMap, dateRange, indexPatternRefs, indexPatterns) => { return { datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), activeData, dateRange, + indexPatternRefs, + indexPatterns, }; } ); diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 4c6f4adc59ff5d..53d53ff29649bf 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -20,6 +20,8 @@ import { SharingSavedObjectProps, VisualizeEditorContext, } from '../types'; +import type { IndexPatternRef } from '../shared_components'; +import type { IndexPattern } from '../indexpattern_datasource'; export interface VisualizationState { activeId: string | null; state: unknown; @@ -53,6 +55,8 @@ export interface LensAppState extends EditorFrameState { searchSessionId: string; resolvedDateRange: DateRange; sharingSavedObjectProps?: Omit; + indexPatternRefs: IndexPatternRef[]; + indexPatterns: Record; } export type DispatchSetState = (state: Partial) => { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 770f4bee7eecdd..1dae6647330e16 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -45,6 +45,8 @@ import { LENS_EDIT_PAGESIZE_ACTION, } from './datatable_visualization/components/constants'; import type { LensInspector } from './lens_inspector_service'; +import type { IndexPattern } from './indexpattern_datasource'; +import type { IndexPatternRef } from './shared_components'; export type ErrorCallback = (e: { message: string }) => void; @@ -228,6 +230,11 @@ export interface Datasource { options?: InitializationOptions ) => Promise; + getIndexPatterns: (state?: T) => { + indexPatterns: Record; + indexPatternRefs: IndexPatternRef[]; + }; + // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; getCurrentIndexPatternId: (state: T) => string; @@ -550,6 +557,10 @@ export type VisualizationLayerWidgetProps = VisualizationConfigProp setState: (newState: T) => void; }; +export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps & { + defaultIndexPatternId: string; +}; + export interface VisualizationToolbarProps { setState: (newState: T) => void; frame: FramePublicAPI; @@ -712,6 +723,8 @@ export interface FramePublicAPI { * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; + indexPatternRefs: IndexPatternRef[]; + indexPatterns: Record; } export interface FrameDatasourceAPI extends FramePublicAPI { query: Query; @@ -795,7 +808,7 @@ export interface Visualization { /** 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) => T; + appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId?: string) => T; /** Retrieve a list of supported layer types with initialization data */ getSupportedLayers: ( @@ -826,13 +839,22 @@ export interface Visualization { }; /** - * Header rendered as layer title This can be used for both static and dynamic content lioke + * Header rendered as layer title. This can be used for both static and dynamic content like * for extra configurability, such as for switch chart type */ renderLayerHeader?: ( domElement: Element, props: VisualizationLayerWidgetProps ) => ((cleanupElement: Element) => void) | void; + + /** + * Layer panel content rendered. This can be used to render a custom content below the title, + * like a custom dataview switch + */ + renderLayerPanel?: ( + domElement: Element, + props: VisualizationLayerWidgetProps + ) => ((cleanupElement: Element) => void) | void; /** * Toolbar rendered above the visualization. This is meant to be used to provide chart-level * settings for the visualization. @@ -949,6 +971,11 @@ export interface Visualization { * On Edit events the frame will call this to know what's going to be the next visualization state */ onEditAction?: (state: T, event: LensEditEvent) => T; + + getInnerDatasource?: ( + state: T, + frame: FramePublicAPI + ) => { datasource: Datasource; state: unknown }; } // Use same technique as TriggerContext diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 0beda9f4740aca..49320b0f0da7c7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -114,6 +114,7 @@ export interface XYAnnotationLayerConfig { layerType: 'annotations'; annotations: EventAnnotationConfig[]; hide?: boolean; + indexPatternId: string; } export type XYLayerConfig = diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index cad5530181b532..c29ce605d8eff1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -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, @@ -121,7 +121,7 @@ export const getXyVisualization = ({ }; }, - appendLayer(state, layerId, layerType) { + appendLayer(state, layerId, layerType, indexPatternId) { const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType; return { ...state, @@ -131,6 +131,7 @@ export const getXyVisualization = ({ seriesType: firstUsedSeriesType || state.preferredSeriesType, layerId, layerType, + indexPatternId: indexPatternId ?? core.uiSettings.get('defaultIndex'), }), ], }; @@ -142,7 +143,11 @@ export const getXyVisualization = ({ layers: state.layers.map((l) => l.layerId !== layerId ? l - : newLayerState({ seriesType: state.preferredSeriesType, layerId }) + : newLayerState({ + seriesType: state.preferredSeriesType, + layerId, + indexPatternId: core.uiSettings.get('defaultIndex'), + }) ), }; }, @@ -500,6 +505,20 @@ export const getXyVisualization = ({ }; }, + renderLayerPanel(domElement, props) { + render( + + + + + , + domElement + ); + }, + renderLayerHeader(domElement, props) { render( diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index e89edab464bfdf..fb364d7bd09f77 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -277,10 +277,17 @@ const newLayerFn = { layerType: layerTypes.REFERENCELINE, accessors: [], }), - [layerTypes.ANNOTATIONS]: ({ layerId }: { layerId: string }): XYAnnotationLayerConfig => ({ + [layerTypes.ANNOTATIONS]: ({ + layerId, + indexPatternId, + }: { + layerId: string; + indexPatternId: string; + }): XYAnnotationLayerConfig => ({ layerId, layerType: layerTypes.ANNOTATIONS, annotations: [], + indexPatternId, }), }; @@ -288,12 +295,14 @@ export function newLayerState({ layerId, layerType = layerTypes.DATA, seriesType, + indexPatternId, }: { layerId: string; layerType?: LayerType; seriesType: SeriesType; + indexPatternId: string; }) { - return newLayerFn[layerType]({ layerId, seriesType }); + return newLayerFn[layerType]({ layerId, seriesType, indexPatternId }); } export function getLayersByType(state: State, byType?: string) { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 3058306fa238c9..fb37db58ad3be7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -244,6 +244,7 @@ export const AnnotationsPanel = ( onChange={setAnnotations} frame={frame} state={state} + layer={localLayer} /> ) : ( | undefined) => void; frame: FramePublicAPI; state: XYState; + layer: XYAnnotationLayerConfig; }) => { const inputQuery = annotation?.query ?? defaultQuery; + const currentIndexPattern = frame.indexPatterns[layer.indexPatternId]; // list only supported field by operation, remove the rest - const options = []; + const options = currentIndexPattern.fields + .filter((field) => field.type === 'date' && field.displayName) + .map((field) => { + return { + label: field.displayName, + value: { + type: 'field', + field: field.name, + dataType: field.type, + }, + exists: true, + compatible: true, + 'data-test-subj': `lns-fieldOption-${field.name}`, + }; + }); + + const selectedField = annotation?.key.field; return ( <> {}} placeholder={ diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index d3a7552954d137..e70fc26ffbf984 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -9,11 +9,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; -import type { VisualizationLayerWidgetProps, VisualizationType } from '../../types'; -import { State, visualizationTypes, SeriesType } from '../types'; +import type { + VisualizationLayerHeaderContentProps, + VisualizationLayerWidgetProps, + VisualizationType, +} from '../../types'; +import { State, visualizationTypes, SeriesType, XYAnnotationLayerConfig } from '../types'; import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { StaticHeader } from '../../shared_components'; +import { ChangeIndexPattern, StaticHeader } from '../../shared_components'; import { LensIconChartBarReferenceLine } from '../../assets/chart_bar_reference_line'; import { LensIconChartBarAnnotations } from '../../assets/chart_bar_annotations'; import { updateLayer } from '.'; @@ -26,12 +30,21 @@ export function LayerHeader(props: VisualizationLayerWidgetProps) { } if (isReferenceLayer(layer)) { return ; - } else if (isAnnotationsLayer(layer)) { + } + if (isAnnotationsLayer(layer)) { return ; } return ; } +export function LayerHeaderContent(props: VisualizationLayerHeaderContentProps) { + const layer = props.state.layers.find((l) => l.layerId === props.layerId); + if (layer && isAnnotationsLayer(layer)) { + return ; + } + return null; +} + function ReferenceLayerHeader() { return ( + + + ); +} + +function AnnotationLayerHeaderContent({ + frame, + state, + layerId, + setState, + defaultIndexPatternId, +}: VisualizationLayerHeaderContentProps) { + const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { + defaultMessage: 'Data view not found', + }); + const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[layerIndex] as XYAnnotationLayerConfig; + + // what if no indexPatternId has been set? Fallback to uiSettings default one for now + const indexPatternId = layer.indexPatternId ?? defaultIndexPatternId; + + return ( + { + const newLayer = { ...layer, indexPatternId: newIndexPatternId }; + const newLayers = [...state.layers]; + newLayers[layerIndex] = newLayer; + setState({ ...state, layers: newLayers }); + }} /> ); } From 00ad7679c94214d6814bf6039b6aae15714a95ba Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 8 Jul 2022 19:50:17 +0200 Subject: [PATCH 05/98] :alembic: Make field selection work --- .../common/query_event_annotation/index.ts | 13 ++ .../common/query_event_annotation/types.ts | 2 + src/plugins/event_annotation/common/types.ts | 1 + src/plugins/event_annotation/kibana.json | 3 +- .../event_annotation_service/service.tsx | 5 + .../editor_frame/dataviews/loader.ts | 174 ++++++++++++++++++ .../lens/public/editor_frame_service/types.ts | 29 ++- .../public/indexpattern_datasource/loader.ts | 108 +++++++++++ .../field_picker/field_picker.tsx | 2 +- .../init_middleware/load_initial.ts | 2 + .../annotations_panel.tsx | 82 ++++++++- .../xy_config_panel/layer_header.tsx | 4 +- 12 files changed, 414 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts diff --git a/src/plugins/event_annotation/common/query_event_annotation/index.ts b/src/plugins/event_annotation/common/query_event_annotation/index.ts index fec11723872b57..835d70f4d94771 100644 --- a/src/plugins/event_annotation/common/query_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_event_annotation/index.ts @@ -70,6 +70,19 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition< defaultMessage: 'Visibility of the label on the annotation line', }), }, + textSource: { + types: ['string'], + options: ['name', 'field'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.textSource', { + defaultMessage: `Source of annotation label`, + }), + }, + textField: { + types: ['string'], + help: i18n.translate('eventAnnotation.queryAnnotation.args.textField', { + defaultMessage: `Field name used for the annotation label`, + }), + }, isHidden: { types: ['boolean'], help: i18n.translate('eventAnnotation.queryAnnotation.args.isHidden', { diff --git a/src/plugins/event_annotation/common/query_event_annotation/types.ts b/src/plugins/event_annotation/common/query_event_annotation/types.ts index 14fc33ed1d56b4..39b0fdd28dbe30 100644 --- a/src/plugins/event_annotation/common/query_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/query_event_annotation/types.ts @@ -12,6 +12,8 @@ import { PointStyleProps } from '../types'; export type QueryPointEventAnnotationArgs = { field: string; + textSource?: string; + textField?: string; query: ExpressionValueBoxed<'kibana_query', Query>; additionalFields?: string[]; } & PointStyleProps; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index a64e94db7966cf..49b82f52a615a9 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -65,6 +65,7 @@ export type PointInTimeQueryEventAnnotationConfig = { additionalFields?: string[]; query: Query; textSource?: 'name' | 'field'; + textField?: string; } & PointStyleProps; export type RangeStyleProps = StyleSharedProps & { diff --git a/src/plugins/event_annotation/kibana.json b/src/plugins/event_annotation/kibana.json index 5a0c49be09ba3e..d981af8ab165b4 100644 --- a/src/plugins/event_annotation/kibana.json +++ b/src/plugins/event_annotation/kibana.json @@ -8,7 +8,8 @@ "common" ], "requiredPlugins": [ - "expressions" + "expressions", + "data" ], "owner": { "name": "Vis Editors", diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 507ad19360f209..b37596b753d4d4 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -7,6 +7,7 @@ */ import { partition } from 'lodash'; +import { queryToAst } from '@kbn/data-plugin/common'; import { EventAnnotationServiceType } from './types'; import { defaultAnnotationColor, @@ -87,6 +88,8 @@ export function getEventAnnotationService(): EventAnnotationServiceType { icon, key, textVisibility, + textField, + textSource, query, additionalFields, } = annotation; @@ -104,6 +107,8 @@ export function getEventAnnotationService(): EventAnnotationServiceType { lineStyle: [lineStyle || 'solid'], icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], + textSource: textVisibility ? [textSource || 'name'] : [], + textField: textVisibility && textSource === 'field' && textField ? [textField] : [], isHidden: [Boolean(isHidden)], query: query ? [queryToAst(query)] : [], additionalFields: additionalFields || [], diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts new file mode 100644 index 00000000000000..6bbe9a73c60a82 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts @@ -0,0 +1,174 @@ +/* + * 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 { isNestedField } from '@kbn/data-views-plugin/common'; +import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { keyBy } from 'lodash'; +import { documentField } from '../../../indexpattern_datasource/document_field'; +import type { IndexPattern, IndexPatternField } from '../../types'; + +export function getFieldByNameFactory(newFields: IndexPatternField[]) { + const fieldsLookup = keyBy(newFields, 'name'); + return (name: string) => fieldsLookup[name]; +} + +export function convertDataViewIntoLensIndexPattern( + dataView: DataView, + restrictionRemapper: (name: string) => string +): IndexPattern { + const newFields = dataView.fields + .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) + .map((field): IndexPatternField => { + // Convert the getters on the index pattern service into plain JSON + const base = { + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + meta: dataView.metaFields.includes(field.name), + esTypes: field.esTypes, + scripted: field.scripted, + runtime: Boolean(field.runtimeField), + }; + + // Simplifies tests by hiding optional properties instead of undefined + return base.scripted + ? { + ...base, + lang: field.lang, + script: field.script, + } + : base; + }) + .concat(documentField); + + const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; + if (typeMeta?.aggs) { + const aggs = Object.keys(typeMeta.aggs); + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; + aggs.forEach((agg) => { + const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; + if (restriction) { + restrictionsObj[restrictionRemapper(agg)] = restriction; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; + } + }); + } + + return { + id: dataView.id!, // id exists for sure because we got index patterns by id + title, + name: name ? name : title, + timeFieldName, + fieldFormatMap: + fieldFormatMap && + Object.fromEntries( + Object.entries(fieldFormatMap).map(([id, format]) => [ + id, + 'toJSON' in format ? format.toJSON() : format, + ]) + ), + fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), + hasRestrictions: !!typeMeta?.aggs, + }; +} + +export async function loadIndexPatterns({ + indexPatternsService, + patterns, + notUsedPatterns, + cache, + onIndexPatternRefresh, + restrictionRemapper, +}: { + indexPatternsService: DataViewsContract; + patterns: string[]; + notUsedPatterns?: string[]; + cache: Record; + onIndexPatternRefresh?: () => void; + restrictionRemapper: (name: string) => string; +}) { + const missingIds = patterns.filter((id) => !cache[id]); + + if (missingIds.length === 0) { + return cache; + } + + onIndexPatternRefresh?.(); + + const allIndexPatterns = await Promise.allSettled( + missingIds.map((id) => indexPatternsService.get(id)) + ); + // ignore rejected indexpatterns here, they're already handled at the app level + let indexPatterns = allIndexPatterns + .filter( + (response): response is PromiseFulfilledResult => response.status === 'fulfilled' + ) + .map((response) => response.value); + + // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds + for (let i = 0; notUsedPatterns && i < notUsedPatterns?.length && !indexPatterns.length; i++) { + const resp = await indexPatternsService.get(notUsedPatterns[i]).catch((e) => { + // do nothing + }); + if (resp) { + indexPatterns = [resp]; + } + } + + const indexPatternsObject = indexPatterns.reduce( + (acc, indexPattern) => ({ + [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, restrictionRemapper), + ...acc, + }), + { ...cache } + ); + + return indexPatternsObject; +} + +// export async function changeIndexPattern({ +// id, +// state, +// setState, +// onError, +// indexPatternsService, +// }: { +// id: string; +// state: IndexPatternPrivateState; +// setState: SetState; +// onError: ErrorHandler; +// indexPatternsService: DataViewsContract; +// }) { +// const indexPatterns = await loadIndexPatterns({ +// indexPatternsService, +// cache: state.indexPatterns, +// patterns: [id], +// }); + +// if (indexPatterns[id] == null) { +// return onError(Error('Missing indexpatterns')); +// } + +// try { +// setState((s) => ({ +// ...s, +// indexPatterns: { +// ...s.indexPatterns, +// [id]: indexPatterns[id], +// }, +// })); +// } catch (err) { +// onError(err); +// } +// } diff --git a/x-pack/plugins/lens/public/editor_frame_service/types.ts b/x-pack/plugins/lens/public/editor_frame_service/types.ts index 2d50e96ec280e1..1a4641ac216a15 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/types.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { Datatable } from '@kbn/expressions-plugin'; +import type { IndexPatternAggRestrictions } from '@kbn/data-plugin/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; +import type { Datatable } from '@kbn/expressions-plugin'; +import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; export type TableInspectorAdapter = Record; @@ -14,3 +17,27 @@ export interface ErrorMessage { longMessage: React.ReactNode; type?: 'fixable' | 'critical'; } + +export interface IndexPattern { + id: string; + fields: IndexPatternField[]; + getFieldByName(name: string): IndexPatternField | undefined; + title: string; + name?: string; + timeFieldName?: string; + fieldFormatMap?: Record< + string, + { + id: string; + params: FieldFormatParams; + } + >; + hasRestrictions: boolean; +} + +export type IndexPatternField = FieldSpec & { + displayName: string; + aggregationRestrictions?: Partial; + meta?: boolean; + runtime?: boolean; +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index ee90adc420e652..5ccba73dd7e422 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -207,6 +207,114 @@ export function getIndexPatterns({ return { indexPatternRefs: indexPatternRefs ?? [], indexPatterns: indexPatterns ?? {} }; } +export function createStateFromPersisted({ + persistedState, + references, +}: { + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; +}) { + return persistedState && references ? injectReferences(persistedState, references) : undefined; +} + +export function getUsedIndexPatterns({ + state, + indexPatternRefs, + storage, + initialContext, + defaultIndexPatternId, +}: { + state?: { + layers: Record; + }; + defaultIndexPatternId?: string; + storage: IStorageWrapper; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + indexPatternRefs: IndexPatternRef[]; +}) { + const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; + const indexPatternIds = []; + if (initialContext) { + if ('isVisualizeAction' in initialContext) { + for (const { indexPatternId } of initialContext.layers) { + indexPatternIds.push(indexPatternId); + } + } else { + indexPatternIds.push(initialContext.indexPatternId); + } + } + const usedPatterns = ( + initialContext + ? indexPatternIds + : uniq(state ? Object.values(state.layers).map((l) => l.indexPatternId) : [fallbackId]) + ) + // take out the undefined from the list + .filter(Boolean); + + return { usedPatterns, indexPatternIds }; +} + +// @TODO: rewire the new logic to this +export async function loadInitialStateMini({ + persistedState, + references, + defaultIndexPatternId, + storage, + initialContext, + indexPatternRefs, + getOrLoadIndexPatterns, +}: { + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; + defaultIndexPatternId?: string; + storage: IStorageWrapper; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + indexPatternRefs: IndexPatternRef[]; + getOrLoadIndexPatterns: (args: { + patterns: string[]; + notUsedPatterns?: string[]; + }) => Record; +}): Promise> { + const state = createStateFromPersisted({ persistedState, references }); + const { usedPatterns, indexPatternIds } = getUsedIndexPatterns({ + state, + defaultIndexPatternId, + storage, + initialContext, + indexPatternRefs, + }); + + const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); + + const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedPatterns); + + const indexPatterns = await getOrLoadIndexPatterns({ + patterns: usedPatterns, + notUsedPatterns, + }); + + // Priority list: + // * start with the indexPattern in context + // * then fallback to the used ones + // * then as last resort use a first one from not used refs + const currentIndexPatternId = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].find( + (id) => id != null && availableIndexPatterns.has(id) && indexPatterns[id] + ); + + if (currentIndexPatternId) { + setLastUsedIndexPatternId(storage, currentIndexPatternId); + } + + return { + layers: {}, + ...state, + currentIndexPatternId, + existingFields: {}, + isFirstExistenceFetch: true, + }; +} + export async function loadInitialState({ persistedState, references, diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx index bde920446d4684..eb33bb41e03730 100644 --- a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx @@ -30,7 +30,7 @@ const DEFAULT_COMBOBOX_WIDTH = 305; const COMBOBOX_PADDINGS = 90; const DEFAULT_FONT = '14px Inter'; -export function FieldPicker({ +export function FieldPicker({ selectedOptions, options, onChoose, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 0b2ec59c1ce222..3a9356571c1e30 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -108,6 +108,8 @@ export function loadInitial( (attributeService.inputIsRefType(initialInput) && initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { + // @TODO: preload indexpattern refs + // @TODO: collect all used dataviews/indexpatterns from datasources & visualizations return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { isFullEditor: true, }) diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index fb37db58ad3be7..29fa2b8875ba1c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -19,6 +19,7 @@ import { EuiText, transparentize, EuiSpacer, + EuiButtonEmpty, } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; import moment from 'moment'; @@ -38,7 +39,11 @@ import { isQueryAnnotation, } from '@kbn/event-annotation-plugin/public'; import Color from 'color'; -import { FieldOptionValue, FieldPicker } from '../../../shared_components/field_picker'; +import { + FieldOption, + FieldOptionValue, + FieldPicker, +} from '../../../shared_components/field_picker'; import { getDataLayers } from '../../visualization_helpers'; import { FormatFactory } from '../../../../common'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; @@ -123,6 +128,7 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => { 'icon', 'textVisibility', 'textSource', + 'textField', 'query', 'additionalFields', ]); @@ -291,13 +297,46 @@ export const AnnotationsPanel = ( if (textDecorationSelected !== 'field') { return null; } + const currentIndexPattern = frame.indexPatterns[localLayer.indexPatternId]; + // @TODO: refactor this to group fields by type + const options = currentIndexPattern.fields + .filter(({ displayName }) => displayName) + .map( + (field) => + ({ + label: field.displayName, + value: { + type: 'field', + field: field.name, + dataType: field.type, + }, + // @TODO: add the existing check here + exists: true, + compatible: true, + 'data-test-subj': `lns-fieldOption-${field.name}`, + } as FieldOption) + ); + const selectedField = (currentAnnotation as PointInTimeQueryEventAnnotationConfig) + .textField; return ( <> @@ -372,6 +411,34 @@ export const AnnotationsPanel = ( onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })} />
+ {isQueryBased && ( + + + {}} + isDisabled={false} + > + {i18n.translate('xpack.lens.xyChart.annotation.addField', { + defaultMessage: 'Add field', + })} + + + + )} ); }; @@ -407,10 +474,11 @@ const ConfigPanelQueryAnnotation = ({ field: field.name, dataType: field.type, }, + // @TODO: add the existing check here exists: true, compatible: true, 'data-test-subj': `lns-fieldOption-${field.name}`, - }; + } as FieldOption; }); const selectedField = annotation?.key.field; @@ -437,7 +505,9 @@ const ConfigPanelQueryAnnotation = ({ : [] } onChoose={function (choice: FieldOptionValue | undefined): void { - throw new Error('Function not implemented.'); + if (choice) { + onChange({ key: { type: 'point_in_time', field: choice.field } }); + } }} fieldIsInvalid={false} /> @@ -453,7 +523,7 @@ const ConfigPanelQueryAnnotation = ({ Date: Fri, 22 Jul 2022 18:58:22 +0200 Subject: [PATCH 06/98] :construction: Fixed almost all dataViews occurrencies, but changeIndexPattern --- x-pack/plugins/lens/public/app_plugin/app.tsx | 29 +- .../lens/public/app_plugin/lens_top_nav.tsx | 19 +- .../public/app_plugin/show_underlying_data.ts | 4 +- .../plugins/lens/public/app_plugin/types.ts | 2 + .../lens/public/data_views_service/loader.ts | 321 +++++++++++ .../lens/public/data_views_service/service.ts | 122 +++++ .../buttons/draggable_dimension_button.tsx | 4 + .../buttons/empty_dimension_button.tsx | 4 + .../config_panel/config_panel.tsx | 3 +- .../editor_frame/config_panel/layer_panel.tsx | 25 +- .../editor_frame/config_panel/types.ts | 2 + .../editor_frame/data_panel_wrapper.tsx | 39 +- .../editor_frame/dataviews/loader.ts | 174 ------ .../editor_frame/editor_frame.tsx | 8 +- .../editor_frame/expression_helpers.ts | 12 +- .../editor_frame/state_helpers.ts | 304 +++++++++-- .../editor_frame/suggestion_helpers.ts | 20 +- .../editor_frame/suggestion_panel.tsx | 9 +- .../workspace_panel/chart_switch.tsx | 1 + .../workspace_panel/workspace_panel.tsx | 7 +- .../public/editor_frame_service/service.tsx | 28 +- .../lens/public/editor_frame_service/types.ts | 27 - .../lens/public/embeddable/embeddable.tsx | 4 +- .../public/embeddable/embeddable_factory.ts | 6 +- .../indexpattern_datasource/datapanel.tsx | 126 +++-- .../bucket_nesting_editor.test.tsx | 2 +- .../dimension_panel/dimension_editor.tsx | 21 +- .../dimension_panel/dimension_panel.tsx | 5 +- .../droppable/get_drop_props.ts | 27 +- .../droppable/on_drop_handler.ts | 33 +- .../dimension_panel/field_select.tsx | 12 +- .../dimension_panel/operation_support.ts | 6 +- .../indexpattern_datasource/field_item.tsx | 4 +- .../indexpattern_datasource/field_list.tsx | 167 +++--- .../fields_accordion.tsx | 4 +- .../public/indexpattern_datasource/index.ts | 2 +- .../indexpattern_datasource/indexpattern.tsx | 144 ++--- .../indexpattern_suggestions.ts | 111 ++-- .../indexpattern_datasource/layerpanel.tsx | 11 +- .../public/indexpattern_datasource/loader.ts | 424 ++------------- .../definitions/formula/formula.test.tsx | 3 +- .../definitions/ranges/ranges.test.tsx | 5 +- .../definitions/shared_components/index.tsx | 1 - .../definitions/terms/field_inputs.tsx | 11 +- .../definitions/terms/terms.test.tsx | 3 +- .../operations/layer_helpers.test.ts | 3 +- .../operations/layer_helpers.ts | 4 +- .../operations/operations.ts | 3 +- .../indexpattern_datasource/pure_helpers.ts | 10 +- .../time_shift_utils.tsx | 9 +- .../indexpattern_datasource/to_expression.ts | 6 +- .../public/indexpattern_datasource/types.ts | 49 +- .../public/indexpattern_datasource/utils.tsx | 8 +- x-pack/plugins/lens/public/plugin.ts | 10 +- .../dataview_picker/dataview_picker.tsx | 2 +- .../dataview_picker/helpers.ts | 29 + .../dataview_picker/index.ts | 2 +- .../drag_drop_bucket}/buckets.test.tsx | 2 +- .../drag_drop_bucket}/buckets.tsx | 0 .../lens/public/shared_components/index.ts | 7 +- .../context_middleware/index.ts | 7 +- .../lens/public/state_management/index.ts | 1 + .../init_middleware/load_initial.ts | 97 +++- .../public/state_management/lens_slice.ts | 41 +- .../lens/public/state_management/selectors.ts | 11 +- .../lens/public/state_management/types.ts | 20 +- x-pack/plugins/lens/public/types.ts | 126 ++++- x-pack/plugins/lens/public/utils.ts | 78 ++- .../annotations_panel.tsx | 507 ++---------------- .../annotations_config_panel/helpers.ts | 116 ++++ .../manual_annotation_panel.tsx | 131 +++++ .../query_annotation_panel.tsx | 122 +++++ .../range_annotation_panel.tsx | 151 ++++++ .../tooltip_annotation_panel.tsx | 229 ++++++++ .../annotations_config_panel}/types.ts | 13 +- .../xy_config_panel/layer_header.tsx | 6 +- .../shared/marker_decoration_settings.tsx | 2 +- 77 files changed, 2437 insertions(+), 1661 deletions(-) create mode 100644 x-pack/plugins/lens/public/data_views_service/loader.ts create mode 100644 x-pack/plugins/lens/public/data_views_service/service.ts delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts rename x-pack/plugins/lens/public/{indexpattern_datasource/operations/definitions/shared_components => shared_components/drag_drop_bucket}/buckets.test.tsx (97%) rename x-pack/plugins/lens/public/{indexpattern_datasource/operations/definitions/shared_components => shared_components/drag_drop_bucket}/buckets.tsx (100%) create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/helpers.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx rename x-pack/plugins/lens/public/{shared_components/dataview_picker => xy_visualization/xy_config_panel/annotations_config_panel}/types.ts (52%) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4f5027c908b09e..93d14d4306fb04 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -25,11 +25,13 @@ import { LensAppState, DispatchSetState, selectSavedObjectFormat, + updateIndexPatterns, } from '../state_management'; import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container'; import { LensInspector } from '../lens_inspector_service'; import { getEditPath } from '../../common'; import { isLensEqual } from './lens_document_equality'; +import { IndexPatternServiceAPI, createIndexPatternService } from '../data_views_service/service'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -66,6 +68,7 @@ export function App({ getOriginatingAppName, spaces, http, + notifications, executionContext, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, @@ -360,6 +363,22 @@ export function App({ ); }, [initialContext]); + const indexPatternService = useMemo( + () => + createIndexPatternService({ + dataViews: lensAppServices.dataViews, + uiSettings: lensAppServices.uiSettings, + core: { http, notifications }, + updateIndexPatterns: (newIndexPatternsState, options) => { + dispatch(updateIndexPatterns(newIndexPatternsState)); + if (options?.applyImmediately) { + dispatch(applyChanges()); + } + }, + }), + [dispatch, http, notifications, lensAppServices] + ); + return ( <>
@@ -381,6 +400,7 @@ export function App({ topNavMenuEntryGenerators={topNavMenuEntryGenerators} initialContext={initialContext} theme$={theme$} + indexPatternService={indexPatternService} /> {getLegacyUrlConflictCallout()} {(!isLoading || persistedDoc) && ( @@ -388,6 +408,7 @@ export function App({ editorFrame={editorFrame} showNoDataPopover={showNoDataPopover} lensInspector={lensInspector} + indexPatternService={indexPatternService} /> )}
@@ -449,13 +470,19 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({ editorFrame, showNoDataPopover, lensInspector, + indexPatternService, }: { editorFrame: EditorFrameInstance; lensInspector: LensInspector; showNoDataPopover: () => void; + indexPatternService: IndexPatternServiceAPI; }) { const { EditorFrameContainer } = editorFrame; return ( - + ); }); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index f8dba39f20dd9e..7ce25f7cc60ead 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -220,6 +220,7 @@ export const LensTopNavMenu = ({ topNavMenuEntryGenerators, initialContext, theme$, + indexPatternService, }: LensTopNavMenuProps) => { const { data, @@ -232,7 +233,7 @@ export const LensTopNavMenu = ({ dashboardFeatureFlag, dataViewFieldEditor, dataViewEditor, - dataViews, + dataViews: dataViewsService, } = useKibana().services; const dispatch = useLensDispatch(); @@ -258,6 +259,7 @@ export const LensTopNavMenu = ({ datasourceStates, visualization, filters, + dataViews, } = useLensSelector((state) => state.lens); const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); @@ -291,7 +293,7 @@ export const LensTopNavMenu = ({ // Update the cached index patterns if the user made a change to any of them if (hasIndexPatternsChanged) { - getIndexPatternsObjects(indexPatternIds, dataViews).then( + getIndexPatternsObjects(indexPatternIds, dataViewsService).then( ({ indexPatterns: indexPatternObjects, rejectedIds }) => { setIndexPatterns(indexPatternObjects); setRejectedIndexPatterns(rejectedIds); @@ -304,7 +306,7 @@ export const LensTopNavMenu = ({ rejectedIndexPatterns, datasourceMap, indexPatterns, - dataViews, + dataViewsService, ]); useEffect(() => { @@ -367,6 +369,7 @@ export const LensTopNavMenu = ({ datasourceMap[activeDatasourceId], datasourceStates[activeDatasourceId].state, activeData, + dataViews.indexPatterns, data.query.timefilter.timefilter.getTime(), application.capabilities ); @@ -376,6 +379,7 @@ export const LensTopNavMenu = ({ datasourceMap, datasourceStates, activeData, + dataViews.indexPatterns, data.query.timefilter.timefilter, application.capabilities, ]); @@ -630,7 +634,8 @@ export const LensTopNavMenu = ({ {} ), indexPatternId: currentIndexPattern.id, - setDatasourceState, + indexPatternService, + indexPatternsCache: dataViews.indexPatterns, }); } // start a new session so all charts are refreshed @@ -640,7 +645,8 @@ export const LensTopNavMenu = ({ data.search.session, datasourceMap, datasourceStates, - setDatasourceState, + indexPatternService, + dataViews.indexPatterns, ]); const editField = useMemo( @@ -762,7 +768,8 @@ export const LensTopNavMenu = ({ allLoaded && activeDatasourceId && datasourceMap[activeDatasourceId].isTimeBased( - datasourceStates[activeDatasourceId].state + datasourceStates[activeDatasourceId].state, + dataViews.indexPatterns ) ) } diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index 310e91d3caaad6..39d7cda3677c07 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -20,7 +20,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities } from '@kbn/core/public'; import { partition } from 'lodash'; import { TableInspectorAdapter } from '../editor_frame_service/types'; -import { Datasource } from '../types'; +import { Datasource, IndexPatternMap } from '../types'; /** * Joins a series of queries. @@ -61,6 +61,7 @@ export function getLayerMetaInfo( currentDatasource: Datasource | undefined, datasourceState: unknown, activeData: TableInspectorAdapter | undefined, + indexPatterns: IndexPatternMap, timeRange: TimeRange | undefined, capabilities: RecursiveReadonly<{ navLinks: Capabilities['navLinks']; @@ -93,6 +94,7 @@ export function getLayerMetaInfo( const datasourceAPI = currentDatasource.getPublicAPI({ layerId: firstLayerId, state: datasourceState, + indexPatterns, }); // maybe add also datasourceId validation here? if (datasourceAPI.datasourceId !== 'indexpattern') { diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index abb6cfa6a06a6e..fe4df5f26593d1 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -47,6 +47,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 '../data_views_service/service'; export interface RedirectToOriginProps { input?: LensEmbeddableInput; @@ -107,6 +108,7 @@ export interface LensTopNavMenuProps { topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; initialContext?: VisualizeFieldContext | VisualizeEditorContext; theme$: Observable; + indexPatternService: IndexPatternServiceAPI; } export interface HistoryLocationState { diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts new file mode 100644 index 00000000000000..5b2a33d66e1334 --- /dev/null +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -0,0 +1,321 @@ +/* + * 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 { isNestedField } from '@kbn/data-views-plugin/common'; +import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { keyBy } from 'lodash'; +import { HttpSetup } from '@kbn/core/public'; +import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types'; +import { documentField } from '../indexpattern_datasource/document_field'; +import { BASE_API_URL, DateRange, ExistingFields } from '../../common'; +import { DataViewsState } from '../state_management'; + +type ErrorHandler = (err: Error) => void; + +/** + * All these functions will be used by the Embeddable instance too, + * therefore keep all these functions pretty raw here and do not use the IndexPatternService + */ + +export function getFieldByNameFactory(newFields: IndexPatternField[]) { + const fieldsLookup = keyBy(newFields, 'name'); + return (name: string) => fieldsLookup[name]; +} + +export function convertDataViewIntoLensIndexPattern( + dataView: DataView, + restrictionRemapper: (name: string) => string +): IndexPattern { + const newFields = dataView.fields + .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) + .map((field): IndexPatternField => { + // Convert the getters on the index pattern service into plain JSON + const base = { + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + meta: dataView.metaFields.includes(field.name), + esTypes: field.esTypes, + scripted: field.scripted, + runtime: Boolean(field.runtimeField), + }; + + // Simplifies tests by hiding optional properties instead of undefined + return base.scripted + ? { + ...base, + lang: field.lang, + script: field.script, + } + : base; + }) + .concat(documentField); + + const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; + if (typeMeta?.aggs) { + const aggs = Object.keys(typeMeta.aggs); + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; + aggs.forEach((agg) => { + const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; + if (restriction) { + restrictionsObj[restrictionRemapper(agg)] = restriction; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; + } + }); + } + + return { + id: dataView.id!, // id exists for sure because we got index patterns by id + title, + name: name ? name : title, + timeFieldName, + fieldFormatMap: + fieldFormatMap && + Object.fromEntries( + Object.entries(fieldFormatMap).map(([id, format]) => [ + id, + 'toJSON' in format ? format.toJSON() : format, + ]) + ), + fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), + hasRestrictions: !!typeMeta?.aggs, + }; +} + +export async function loadIndexPatternRefs( + indexPatternsService: DataViewsContract +): Promise { + const indexPatterns = await indexPatternsService.getIdsWithTitle(); + + return indexPatterns.sort((a, b) => { + return a.title.localeCompare(b.title); + }); +} + +/** + * Map ES agg names with Lens ones + */ +const renameOperationsMapping: Record = { + avg: 'average', + cardinality: 'unique_count', +}; + +function onRestrictionMapping(agg: string): string { + return agg in renameOperationsMapping ? renameOperationsMapping[agg] : agg; +} + +export async function loadIndexPatterns({ + dataViews, + patterns, + notUsedPatterns, + cache, + onIndexPatternRefresh, +}: { + dataViews: DataViewsContract; + patterns: string[]; + notUsedPatterns?: string[]; + cache: Record; + onIndexPatternRefresh?: () => void; +}) { + const missingIds = patterns.filter((id) => !cache[id]); + + if (missingIds.length === 0) { + return cache; + } + + onIndexPatternRefresh?.(); + + const allIndexPatterns = await Promise.allSettled(missingIds.map((id) => dataViews.get(id))); + // ignore rejected indexpatterns here, they're already handled at the app level + let indexPatterns = allIndexPatterns + .filter( + (response): response is PromiseFulfilledResult => response.status === 'fulfilled' + ) + .map((response) => response.value); + + // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds + if (!indexPatterns.length && notUsedPatterns) { + for (const notUsedPattern of notUsedPatterns) { + const resp = await dataViews.get(notUsedPattern).catch((e) => { + // do nothing + }); + if (resp) { + indexPatterns = [resp]; + } + } + } + + const indexPatternsObject = indexPatterns.reduce( + (acc, indexPattern) => ({ + [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, onRestrictionMapping), + ...acc, + }), + { ...cache } + ); + + return indexPatternsObject; +} + +export async function changeIndexPattern({ + id, + updateIndexPatterns, + onError, + dataViews, + cache = {}, +}: { + id: string; + updateIndexPatterns: ( + newFieldState: Pick, + options?: { applyImmediately: boolean } + ) => void; + onError: ErrorHandler; + dataViews: DataViewsContract; + cache?: IndexPatternMap; +}) { + const indexPatterns = await loadIndexPatterns({ + dataViews, + cache, + patterns: [id], + }); + + if (indexPatterns[id] == null) { + onError(Error('Missing indexpatterns')); + return; + } + + const newIndexPatterns = { + ...cache, + [id]: indexPatterns[id], + }; + + updateIndexPatterns( + { + indexPatterns: newIndexPatterns, + }, + { applyImmediately: true } + ); + return newIndexPatterns; +} + +async function refreshExistingFields({ + dateRange, + fetchJson, + indexPatternList, + dslQuery, +}: { + dateRange: DateRange; + indexPatternList: IndexPattern[]; + fetchJson: HttpSetup['post']; + dslQuery: object; +}) { + try { + const emptinessInfo = await Promise.all( + indexPatternList.map((pattern) => { + if (pattern.hasRestrictions) { + return { + indexPatternTitle: pattern.title, + existingFieldNames: pattern.fields.map((field) => field.name), + }; + } + const body: Record = { + dslQuery, + fromDate: dateRange.fromDate, + toDate: dateRange.toDate, + }; + + if (pattern.timeFieldName) { + body.timeFieldName = pattern.timeFieldName; + } + + return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { + body: JSON.stringify(body), + }) as Promise; + }) + ); + return { result: emptinessInfo, status: 200 }; + } catch (e) { + return { result: undefined, status: e.res?.status as number }; + } +} + +type FieldsPropsFromDataViewsState = Pick< + DataViewsState, + 'existingFields' | 'isFirstExistenceFetch' | 'existenceFetchTimeout' | 'existenceFetchFailed' +>; +export async function syncExistingFields({ + updateIndexPatterns, + isFirstExistenceFetch, + currentIndexPatternTitle, + onNoData, + existingFields, + ...requestOptions +}: { + dateRange: DateRange; + indexPatternList: IndexPattern[]; + existingFields: Record>; + fetchJson: HttpSetup['post']; + updateIndexPatterns: ( + newFieldState: FieldsPropsFromDataViewsState, + options: { applyImmediately: boolean } + ) => void; + isFirstExistenceFetch: boolean; + currentIndexPatternTitle: string; + dslQuery: object; + onNoData?: () => void; +}) { + const { indexPatternList } = requestOptions; + const newExistingFields = { ...existingFields }; + + const { result, status } = await refreshExistingFields(requestOptions); + + if (result) { + if (isFirstExistenceFetch) { + const fieldsCurrentIndexPattern = result.find( + (info) => info.indexPatternTitle === currentIndexPatternTitle + ); + if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { + onNoData?.(); + } + } + + for (const { indexPatternTitle, existingFieldNames } of result) { + newExistingFields[indexPatternTitle] = booleanMap(existingFieldNames); + } + } else { + for (const { title, fields } of indexPatternList) { + newExistingFields[title] = booleanMap(fields.map((field) => field.name)); + } + } + + updateIndexPatterns( + { + isFirstExistenceFetch: status !== 200, + existingFields: newExistingFields, + ...(result + ? {} + : { + existenceFetchFailed: status !== 418, + existenceFetchTimeout: status === 418, + }), + }, + { applyImmediately: true } + ); +} + +function booleanMap(keys: string[]) { + return keys.reduce((acc, key) => { + acc[key] = true; + return acc; + }, {} as Record); +} diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/data_views_service/service.ts new file mode 100644 index 00000000000000..43a77e919fae6b --- /dev/null +++ b/x-pack/plugins/lens/public/data_views_service/service.ts @@ -0,0 +1,122 @@ +/* + * 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 { DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import type { DateRange } from '../../common'; +import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; +import { + changeIndexPattern, + loadIndexPatternRefs, + loadIndexPatterns, + syncExistingFields, +} from './loader'; +import type { DataViewsState } from '../state_management'; + +interface IndexPatternServiceProps { + core: Pick; + dataViews: DataViewsContract; + uiSettings: IUiSettingsClient; + updateIndexPatterns: ( + newState: Partial, + options?: { applyImmediately: boolean } + ) => void; +} + +/** + * This service is only available for the full editor version + * and it encapsulate all the indexpattern methods and state + * in a single object. + * NOTE: this is not intended to be used with the Embeddable branch + */ +export interface IndexPatternServiceAPI { + /** + * Loads a list of indexPatterns from a list of id (patterns) + * leveraging existing cache. Eventually fallbacks to unused indexPatterns ( notUsedPatterns ) + * @returns IndexPatternMap + */ + loadIndexPatterns: (args: { + patterns: string[]; + notUsedPatterns?: string[]; + cache: IndexPatternMap; + onIndexPatternRefresh?: () => void; + }) => Promise; + /** + * Load indexPatternRefs with title and ids + */ + loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise; + /** + * Load an indexPattern to the cache, usually used in conjuction with a indexPattern change action. + * + * **Note**: + * this function has sideEffects, updating the Lens state with the new indexPattern loaded, or showing + * a notification error toast in case of loading issues + */ + addIndexPattern: (args: { + id: string; + cache: IndexPatternMap; + }) => Promise; + /** + * Loads the existingFields map given the current context + */ + refreshExistingFields: (args: { + dateRange: DateRange; + currentIndexPatternTitle: string; + dslQuery: object; + onNoData?: () => void; + existingFields: Record>; + indexPatternList: IndexPattern[]; + isFirstExistenceFetch: boolean; + }) => Promise; + /** + * Retrieves the default indexPattern from the uiSettings + */ + getDefaultIndex: () => string; + + /** + * Update the Lens state cache of indexPatterns + */ + updateIndexPatternsCache: ( + newState: Partial, + options?: { applyImmediately: boolean } + ) => void; +} + +export function createIndexPatternService({ + core, + dataViews, + uiSettings, + updateIndexPatterns, +}: IndexPatternServiceProps): IndexPatternServiceAPI { + const onChangeError = (err: Error) => + core.notifications.toasts.addError(err, { + title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { + defaultMessage: 'Error loading data view', + }), + }); + return { + updateIndexPatternsCache: updateIndexPatterns, + loadIndexPatterns: (args) => { + return loadIndexPatterns({ + dataViews, + ...args, + }); + }, + addIndexPattern: (args) => + changeIndexPattern({ updateIndexPatterns, onError: onChangeError, dataViews, ...args }), + refreshExistingFields: (args) => + syncExistingFields({ + updateIndexPatterns, + fetchJson: core.http.post, + ...args, + }), + loadIndexPatternRefs: async ({ isFullEditor }) => + isFullEditor ? loadIndexPatternRefs(dataViews) : [], + getDefaultIndex: () => uiSettings.get('defaultIndex'), + }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index 32aba270e846b1..cb95c7e9e2c26d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -13,6 +13,7 @@ import { isOperation, DropType, DatasourceLayers, + IndexPatternMap, } from '../../../../types'; import { getCustomDropTarget, @@ -37,6 +38,7 @@ export function DraggableDimensionButton({ layerDatasource, datasourceLayers, registerNewButtonRef, + indexPatterns, }: { layerId: string; groupIndex: number; @@ -53,6 +55,7 @@ export function DraggableDimensionButton({ accessorIndex: number; columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; + indexPatterns: IndexPatternMap; }) { const { dragging } = useContext(DragContext); @@ -73,6 +76,7 @@ export function DraggableDimensionButton({ filterOperations: group.filterOperations, prioritizedOperation: group.prioritizedOperation, }, + indexPatterns, }, sharedDatasource ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index a35366611ae180..30b543bb5f0ae4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -18,6 +18,7 @@ import { DropType, DatasourceLayers, isOperation, + IndexPatternMap, } from '../../../../types'; import { getCustomDropTarget, @@ -111,6 +112,7 @@ export function EmptyDimensionButton({ onClick, onDrop, datasourceLayers, + indexPatterns, }: { layerId: string; groupIndex: number; @@ -121,6 +123,7 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; datasourceLayers: DatasourceLayers; state: unknown; + indexPatterns: IndexPatternMap; }) { const { dragging } = useContext(DragContext); const sharedDatasource = @@ -148,6 +151,7 @@ export function EmptyDimensionButton({ prioritizedOperation: group.prioritizedOperation, isNewColumn: true, }, + indexPatterns, }, sharedDatasource ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 163d1b8ce8e616..9aa459d20b9376 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -44,7 +44,7 @@ export function LayerPanels( activeVisualization: Visualization; } ) { - const { activeVisualization, datasourceMap } = props; + const { activeVisualization, datasourceMap, indexPatternService } = props; const { activeDatasourceId, visualization } = useLensSelector((state) => state.lens); const dispatchLens = useLensDispatch(); @@ -184,6 +184,7 @@ export function LayerPanels( removeLayerRef(layerId); }} toggleFullscreen={toggleFullscreen} + indexPatternService={indexPatternService} /> ))} void; toggleFullscreen: () => void; onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void; + indexPatternService: IndexPatternServiceAPI; } ) { const [activeDimension, setActiveDimension] = useState( @@ -87,6 +89,7 @@ export function LayerPanel( updateAll, updateDatasourceAsync, visualizationState, + indexPatternService, } = props; const datasourceStates = useLensSelector(selectDatasourceStates); @@ -187,6 +190,7 @@ export function LayerPanel( }, dimensionGroups: groups, dropType, + indexPatterns: framePublicAPI.dataViews.indexPatterns, }) ); } @@ -210,15 +214,15 @@ export function LayerPanel( }; }, [ layerDatasource, - layerDatasourceState, setNextFocusedButtonId, + layerDatasourceState, groups, + updateDatasource, + datasourceId, activeVisualization, + updateVisualization, props.visualizationState, framePublicAPI, - updateVisualization, - datasourceId, - updateDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -294,6 +298,8 @@ export function LayerPanel( ] ); + const { dataViews } = props.framePublicAPI; + return ( <>
@@ -327,6 +333,8 @@ export function LayerPanel( layerId, state: layerDatasourceState, activeData: props.framePublicAPI.activeData, + dataViews, + indexPatternService, setState: (updater: unknown) => { const newState = typeof updater === 'function' ? updater(layerDatasourceState) : updater; @@ -334,6 +342,7 @@ export function LayerPanel( const nextPublicAPI = layerDatasource.getPublicAPI({ state: newState, layerId, + indexPatterns: dataViews.indexPatterns, }); const nextTable = new Set( nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) @@ -453,6 +462,7 @@ export function LayerPanel( onDragStart={() => setHideTooltip(true)} onDragEnd={() => setHideTooltip(false)} onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} >
) : ( @@ -554,6 +568,7 @@ export function LayerPanel( }); }} onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} /> ) : null} @@ -614,6 +629,8 @@ export function LayerPanel( paramEditorCustomProps: activeGroup.paramEditorCustomProps, supportFieldFormat: activeGroup.supportFieldFormat !== false, layerType: activeVisualization.getLayerType(layerId, visualizationState), + indexPatterns: dataViews.indexPatterns, + existingFields: dataViews.existingFields, }} /> )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 172e0702f56e87..26278e187a57af 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IndexPatternServiceAPI } from '../../../data_views_service/service'; import { Visualization, FramePublicAPI, @@ -18,6 +19,7 @@ export interface ConfigPanelWrapperProps { datasourceMap: DatasourceMap; visualizationMap: VisualizationMap; core: DatasourceDimensionEditorProps['core']; + indexPatternService: IndexPatternServiceAPI; } export interface LayerPanelProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index a12578c66b0c9c..b2d5f2aeb383da 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -10,10 +10,12 @@ import './data_panel_wrapper.scss'; import React, { useMemo, memo, useContext, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { NativeRenderer } from '../../native_renderer'; import { DragContext, DragDropIdentifier } from '../../drag_drop'; -import { StateSetter, DatasourceDataPanelProps, DatasourceMap } from '../../types'; +import { StateSetter, DatasourceDataPanelProps, DatasourceMap, FramePublicAPI } from '../../types'; import { switchDatasource, useLensDispatch, @@ -25,7 +27,8 @@ import { selectActiveDatasourceId, selectDatasourceStates, } from '../../state_management'; -import { initializeDatasources } from './state_helpers'; +import { initializeSources } from './state_helpers'; +import type { IndexPatternServiceAPI } from '../../data_views_service/service'; interface DataPanelWrapperProps { datasourceMap: DatasourceMap; @@ -33,7 +36,9 @@ interface DataPanelWrapperProps { core: DatasourceDataPanelProps['core']; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; - plugins: { uiActions: UiActionsStart }; + plugins: { uiActions: UiActionsStart; dataViews: DataViewsPublicPluginStart }; + indexPatternService: IndexPatternServiceAPI; + frame: FramePublicAPI; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -63,9 +68,20 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { useEffect(() => { if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) { - initializeDatasources(props.datasourceMap, datasourceStates, undefined, undefined, { - isFullEditor: true, - }).then((result) => { + initializeSources( + { + datasourceMap: props.datasourceMap, + datasourceStates, + dataViews: props.plugins.dataViews, + references: undefined, + initialContext: undefined, + storage: new Storage(localStorage), + defaultIndexPatternId: props.core.uiSettings.get('defaultIndex'), + }, + { + isFullEditor: true, + } + ).then((result) => { const newDatasourceStates = Object.entries(result).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, @@ -79,7 +95,14 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dispatchLens(setState({ datasourceStates: newDatasourceStates })); }); } - }, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]); + }, [ + datasourceStates, + activeDatasourceId, + props.datasourceMap, + dispatchLens, + props.plugins.dataViews, + props.core.uiSettings, + ]); const datasourceProps: DatasourceDataPanelProps = { ...externalContext, @@ -91,6 +114,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, uiActions: props.plugins.uiActions, + indexPatternService: props.indexPatternService, + frame: props.frame, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts deleted file mode 100644 index 6bbe9a73c60a82..00000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/dataviews/loader.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isNestedField } from '@kbn/data-views-plugin/common'; -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; -import { keyBy } from 'lodash'; -import { documentField } from '../../../indexpattern_datasource/document_field'; -import type { IndexPattern, IndexPatternField } from '../../types'; - -export function getFieldByNameFactory(newFields: IndexPatternField[]) { - const fieldsLookup = keyBy(newFields, 'name'); - return (name: string) => fieldsLookup[name]; -} - -export function convertDataViewIntoLensIndexPattern( - dataView: DataView, - restrictionRemapper: (name: string) => string -): IndexPattern { - const newFields = dataView.fields - .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) - .map((field): IndexPatternField => { - // Convert the getters on the index pattern service into plain JSON - const base = { - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - meta: dataView.metaFields.includes(field.name), - esTypes: field.esTypes, - scripted: field.scripted, - runtime: Boolean(field.runtimeField), - }; - - // Simplifies tests by hiding optional properties instead of undefined - return base.scripted - ? { - ...base, - lang: field.lang, - script: field.script, - } - : base; - }) - .concat(documentField); - - const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; - if (typeMeta?.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[restrictionRemapper(agg)] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - return { - id: dataView.id!, // id exists for sure because we got index patterns by id - title, - name: name ? name : title, - timeFieldName, - fieldFormatMap: - fieldFormatMap && - Object.fromEntries( - Object.entries(fieldFormatMap).map(([id, format]) => [ - id, - 'toJSON' in format ? format.toJSON() : format, - ]) - ), - fields: newFields, - getFieldByName: getFieldByNameFactory(newFields), - hasRestrictions: !!typeMeta?.aggs, - }; -} - -export async function loadIndexPatterns({ - indexPatternsService, - patterns, - notUsedPatterns, - cache, - onIndexPatternRefresh, - restrictionRemapper, -}: { - indexPatternsService: DataViewsContract; - patterns: string[]; - notUsedPatterns?: string[]; - cache: Record; - onIndexPatternRefresh?: () => void; - restrictionRemapper: (name: string) => string; -}) { - const missingIds = patterns.filter((id) => !cache[id]); - - if (missingIds.length === 0) { - return cache; - } - - onIndexPatternRefresh?.(); - - const allIndexPatterns = await Promise.allSettled( - missingIds.map((id) => indexPatternsService.get(id)) - ); - // ignore rejected indexpatterns here, they're already handled at the app level - let indexPatterns = allIndexPatterns - .filter( - (response): response is PromiseFulfilledResult => response.status === 'fulfilled' - ) - .map((response) => response.value); - - // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds - for (let i = 0; notUsedPatterns && i < notUsedPatterns?.length && !indexPatterns.length; i++) { - const resp = await indexPatternsService.get(notUsedPatterns[i]).catch((e) => { - // do nothing - }); - if (resp) { - indexPatterns = [resp]; - } - } - - const indexPatternsObject = indexPatterns.reduce( - (acc, indexPattern) => ({ - [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, restrictionRemapper), - ...acc, - }), - { ...cache } - ); - - return indexPatternsObject; -} - -// export async function changeIndexPattern({ -// id, -// state, -// setState, -// onError, -// indexPatternsService, -// }: { -// id: string; -// state: IndexPatternPrivateState; -// setState: SetState; -// onError: ErrorHandler; -// indexPatternsService: DataViewsContract; -// }) { -// const indexPatterns = await loadIndexPatterns({ -// indexPatternsService, -// cache: state.indexPatterns, -// patterns: [id], -// }); - -// if (indexPatterns[id] == null) { -// return onError(Error('Missing indexpatterns')); -// } - -// try { -// setState((s) => ({ -// ...s, -// indexPatterns: { -// ...s.indexPatterns, -// [id]: indexPatterns[id], -// }, -// })); -// } catch (err) { -// onError(err); -// } -// } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 16a3a6f96827ab..6a73c7168f45b1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -28,6 +28,7 @@ import { selectVisualization, } from '../../state_management'; import type { LensInspector } from '../../lens_inspector_service'; +import { IndexPatternServiceAPI } from '../../data_views_service/service'; export interface EditorFrameProps { datasourceMap: DatasourceMap; @@ -37,6 +38,7 @@ export interface EditorFrameProps { plugins: EditorFrameStartPlugins; showNoDataPopover: () => void; lensInspector: LensInspector; + indexPatternService: IndexPatternServiceAPI; } export function EditorFrame(props: EditorFrameProps) { @@ -65,7 +67,8 @@ export function EditorFrame(props: EditorFrameProps) { datasourceStates, visualizationMap, datasourceMap[activeDatasourceId], - field + field, + framePublicAPI.dataViews ); }; @@ -96,6 +99,8 @@ export function EditorFrame(props: EditorFrameProps) { showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + indexPatternService={props.indexPatternService} + frame={framePublicAPI} /> } configPanel={ @@ -105,6 +110,7 @@ export function EditorFrame(props: EditorFrameProps) { datasourceMap={datasourceMap} visualizationMap={visualizationMap} framePublicAPI={framePublicAPI} + indexPatternService={props.indexPatternService} /> ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 367d156929714a..f1caa66ede1a2c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -7,11 +7,12 @@ import { Ast, fromExpression } from '@kbn/interpreter'; import { DatasourceStates } from '../../state_management'; -import { Visualization, DatasourceMap, DatasourceLayers } from '../../types'; +import { Visualization, DatasourceMap, DatasourceLayers, IndexPatternMap } from '../../types'; export function getDatasourceExpressionsByLayers( datasourceMap: DatasourceMap, - datasourceStates: DatasourceStates + datasourceStates: DatasourceStates, + indexPatterns: IndexPatternMap ): null | Record { const datasourceExpressions: Array<[string, Ast | string]> = []; @@ -24,7 +25,7 @@ export function getDatasourceExpressionsByLayers( const layers = datasource.getLayers(state); layers.forEach((layerId) => { - const result = datasource.toExpression(state, layerId); + const result = datasource.toExpression(state, layerId, indexPatterns); if (result) { datasourceExpressions.push([layerId, result]); } @@ -52,6 +53,7 @@ export function buildExpression({ datasourceLayers, title, description, + indexPatterns, }: { title?: string; description?: string; @@ -60,6 +62,7 @@ export function buildExpression({ datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; datasourceLayers: DatasourceLayers; + indexPatterns: IndexPatternMap; }): Ast | null { if (visualization === null) { return null; @@ -67,7 +70,8 @@ export function buildExpression({ const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers( datasourceMap, - datasourceStates + datasourceStates, + indexPatterns ); const visualizationExpression = visualization.toExpression( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 14e188f7a252e6..92992f9eac0237 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -5,15 +5,21 @@ * 2.0. */ -import { SavedObjectReference } from '@kbn/core/public'; +import { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public'; import { Ast } from '@kbn/interpreter'; import memoizeOne from 'memoize-one'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; +import { difference } from 'lodash'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { Datasource, DatasourceLayers, DatasourceMap, FramePublicAPI, + IndexPattern, + IndexPatternMap, + IndexPatternRef, InitializationOptions, Visualization, VisualizationMap, @@ -22,64 +28,217 @@ import { import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; import { getActiveDatasourceIdFromDoc } from '../../utils'; -import { ErrorMessage } from '../types'; +import type { ErrorMessage } from '../types'; import { getMissingCurrentDatasource, getMissingIndexPatterns, getMissingVisualizationTypeError, getUnknownVisualizationTypeError, } from '../error_helper'; -import { DatasourceStates } from '../../state_management'; -import { IndexPattern } from '../../indexpattern_datasource/types'; +import type { DatasourceStates, DataViewsState } from '../../state_management'; +import { readFromStorage } from '../../settings_storage'; +import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader'; -export function mergeIndexPatterns( - datasourceMap: DatasourceMap, - datasourceStates: DatasourceStates +function getIndexPatterns( + references?: SavedObjectReference[], + initialContext?: VisualizeFieldContext | VisualizeEditorContext, + initialId?: string ) { - const dataViewWithDuplicates = Object.entries(datasourceMap).map(([datasourceId, datasource]) => - datasource.getIndexPatterns(datasourceStates[datasourceId].state) - ); - // it's always the same, so just pick the first one - const indexPatternRefs = dataViewWithDuplicates[0].indexPatternRefs; - const indexPatternDedup: Record = {}; - for (const { indexPatterns } of dataViewWithDuplicates) { - for (const [indexPatternId, indexPattern] of Object.entries(indexPatterns)) { - if (!(indexPatternId in indexPatternDedup)) { - indexPatternDedup[indexPatternId] = indexPattern; + const indexPatternIds = []; + if (initialContext) { + if ('isVisualizeAction' in initialContext) { + for (const { indexPatternId } of initialContext.layers) { + indexPatternIds.push(indexPatternId); + } + } else { + indexPatternIds.push(initialContext.indexPatternId); + } + } else { + // use the initialId only when no context is passed over + if (initialId) { + indexPatternIds.push(initialId); + } + } + if (references) { + for (const reference of references) { + if (reference.type === 'index-pattern') { + indexPatternIds.push(reference.id); } } } - return { indexPatternRefs, indexPatterns: indexPatternDedup }; + return [...new Set(indexPatternIds)]; } -export async function initializeDatasources( - datasourceMap: DatasourceMap, - datasourceStates: DatasourceStates, - references?: SavedObjectReference[], - initialContext?: VisualizeFieldContext | VisualizeEditorContext, +const getLastUsedIndexPatternId = ( + storage: IStorageWrapper, + indexPatternRefs: IndexPatternRef[] +) => { + const indexPattern = readFromStorage(storage, 'indexPatternId'); + return indexPattern && indexPatternRefs.find((i) => i.id === indexPattern)?.id; +}; + +export async function initializeDataViews( + { + dataViews, + datasourceMap, + datasourceStates, + storage, + defaultIndexPatternId, + references, + initialContext, + }: { + dataViews: DataViewsContract; + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + defaultIndexPatternId: string; + storage: IStorageWrapper; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + }, options?: InitializationOptions ) { - const states: DatasourceStates = {}; + const { isFullEditor } = options ?? {}; + // make it explicit or TS will infer never[] and break few lines down + const indexPatternRefs: IndexPatternRef[] = await (isFullEditor + ? loadIndexPatternRefs(dataViews) + : []); - await Promise.all( - Object.entries(datasourceMap).map(([datasourceId, datasource]) => { - if (datasourceStates[datasourceId]) { - return datasource - .initialize( - datasourceStates[datasourceId].state || undefined, - references, - initialContext, - options - ) - .then((datasourceState) => { - states[datasourceId] = { isLoading: false, state: datasourceState }; - }); - } - }) + // if no state is available, use the fallbackId + const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; + const initialId = + !initialContext && + Object.keys(datasourceMap).every((datasourceId) => !datasourceStates[datasourceId]?.state) + ? fallbackId + : undefined; + + const usedIndexPatterns = getIndexPatterns(references, initialContext, initialId); + + // load them + const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); + + const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedIndexPatterns); + + const indexPatterns = await loadIndexPatterns({ + dataViews, + patterns: usedIndexPatterns, + notUsedPatterns, + cache: {}, + }); + + return { indexPatternRefs, indexPatterns }; +} + +/** + * This function composes both initializeDataViews & initializeDatasources into a single call + */ +export async function initializeSources( + { + dataViews, + datasourceMap, + datasourceStates, + storage, + defaultIndexPatternId, + references, + initialContext, + }: { + dataViews: DataViewsContract; + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + defaultIndexPatternId: string; + storage: IStorageWrapper; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + }, + options?: InitializationOptions +) { + const { indexPatternRefs, indexPatterns } = await initializeDataViews( + { + datasourceMap, + datasourceStates, + initialContext, + dataViews, + storage, + defaultIndexPatternId, + references, + }, + { + isFullEditor: true, + } ); + return { + indexPatterns, + indexPatternRefs, + states: initializeDatasources({ + datasourceMap, + datasourceStates, + initialContext, + indexPatternRefs, + indexPatterns, + }), + }; +} + +export function initializeDatasources({ + datasourceMap, + datasourceStates, + indexPatternRefs, + indexPatterns, + references, + initialContext, +}: { + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + indexPatterns: Record; + indexPatternRefs: IndexPatternRef[]; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; +}) { + // init datasources + const states: DatasourceStates = {}; + for (const [datasourceId, datasource] of Object.entries(datasourceMap)) { + if (datasourceStates[datasourceId]) { + const state = datasource.initialize( + datasourceStates[datasourceId] || undefined, + references, + initialContext, + indexPatternRefs, + indexPatterns + ); + states[datasourceId] = { isLoading: false, state }; + } + } return states; } +// export async function initializeDatasources( +// datasourceMap: DatasourceMap, +// datasourceStates: DatasourceStates, +// references?: SavedObjectReference[], +// initialContext?: VisualizeFieldContext | VisualizeEditorContext, +// options?: InitializationOptions +// ) { +// const states: DatasourceStates = {}; + +// await Promise.all( +// Object.entries(datasourceMap).map(([datasourceId, datasource]) => { +// if (datasourceStates[datasourceId]) { +// return datasource +// .initialize( +// datasourceStates[datasourceId].state || undefined, +// references, +// initialContext, +// options +// ) +// .then((datasourceState) => { +// states[datasourceId] = { isLoading: false, state: datasourceState }; +// }); +// } +// }) +// ); +// return states; +// } + export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceStates: DatasourceStates, datasourceMap: DatasourceMap @@ -96,6 +255,8 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ state: datasourceState, layerId: layer, + // @TODO + indexPatterns: {}, }); }); }); @@ -105,7 +266,12 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( export async function persistedStateToExpression( datasourceMap: DatasourceMap, visualizations: VisualizationMap, - doc: Document + doc: Document, + services: { + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + dataViews: DataViewsContract; + } ): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> { const { state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates }, @@ -127,18 +293,30 @@ export async function persistedStateToExpression( }; } const visualization = visualizations[visualizationType!]; - const datasourceStates = await initializeDatasources( - datasourceMap, - Object.fromEntries( - Object.entries(persistedDatasourceStates).map(([id, state]) => [ - id, - { isLoading: false, state }, - ]) - ), - references, - undefined, + const datasourceStatesFromSO = Object.fromEntries( + Object.entries(persistedDatasourceStates).map(([id, state]) => [ + id, + { isLoading: false, state }, + ]) + ); + const { indexPatterns, indexPatternRefs } = await initializeDataViews( + { + datasourceMap, + datasourceStates: datasourceStatesFromSO, + references, + dataViews: services.dataViews, + storage: services.storage, + defaultIndexPatternId: services.uiSettings.get('defaultIndex'), + }, { isFullEditor: false } ); + const datasourceStates = initializeDatasources({ + datasourceMap, + datasourceStates: datasourceStatesFromSO, + references, + indexPatterns, + indexPatternRefs, + }); const datasourceLayers = getDatasourceLayers(datasourceStates, datasourceMap); @@ -152,7 +330,8 @@ export async function persistedStateToExpression( const indexPatternValidation = validateRequiredIndexPatterns( datasourceMap[datasourceId], - datasourceStates[datasourceId] + datasourceStates[datasourceId], + indexPatterns ); if (indexPatternValidation) { @@ -167,7 +346,7 @@ export async function persistedStateToExpression( datasourceStates[datasourceId].state, visualization, visualizationState, - { datasourceLayers } + { datasourceLayers, dataViews: { indexPatterns } as DataViewsState } ); return { @@ -179,6 +358,7 @@ export async function persistedStateToExpression( datasourceMap, datasourceStates, datasourceLayers, + indexPatterns, }), errors: validationResult, }; @@ -186,12 +366,13 @@ export async function persistedStateToExpression( export function getMissingIndexPattern( currentDatasource: Datasource | null, - currentDatasourceState: { state: unknown } | null + currentDatasourceState: { state: unknown } | null, + indexPatterns: IndexPatternMap ) { if (currentDatasourceState == null || currentDatasource == null) { return []; } - const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state); + const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state, indexPatterns); if (!missingIds.length) { return []; } @@ -200,9 +381,14 @@ export function getMissingIndexPattern( const validateRequiredIndexPatterns = ( currentDatasource: Datasource, - currentDatasourceState: { state: unknown } | null + currentDatasourceState: { state: unknown } | null, + indexPatterns: IndexPatternMap ): ErrorMessage[] | undefined => { - const missingIds = getMissingIndexPattern(currentDatasource, currentDatasourceState); + const missingIds = getMissingIndexPattern( + currentDatasource, + currentDatasourceState, + indexPatterns + ); if (!missingIds.length) { return; @@ -216,14 +402,14 @@ export const validateDatasourceAndVisualization = ( currentDatasourceState: unknown | null, currentVisualization: Visualization | null, currentVisualizationState: unknown | undefined, - frameAPI: Pick + { datasourceLayers, dataViews }: Pick ): ErrorMessage[] | undefined => { const datasourceValidationErrors = currentDatasourceState - ? currentDataSource?.getErrorMessages(currentDatasourceState) + ? currentDataSource?.getErrorMessages(currentDatasourceState, dataViews.indexPatterns) : undefined; const visualizationValidationErrors = currentVisualizationState - ? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI.datasourceLayers) + ? currentVisualization?.getErrorMessages(currentVisualizationState, datasourceLayers) : undefined; if (datasourceValidationErrors?.length || visualizationValidationErrors?.length) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index d21940d4ed5618..fadc7780cc85dc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -28,6 +28,7 @@ import { DatasourceStates, VisualizationState, applyChanges, + DataViewsState, } from '../../state_management'; /** @@ -48,6 +49,7 @@ export function getSuggestions({ field, visualizeTriggerFieldContext, activeData, + dataViews, mainPalette, }: { datasourceMap: DatasourceMap; @@ -59,6 +61,7 @@ export function getSuggestions({ field?: unknown; visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; activeData?: Record; + dataViews: DataViewsState; mainPalette?: PaletteOutput; }): Suggestion[] { const datasources = Object.entries(datasourceMap).filter( @@ -91,25 +94,29 @@ export function getSuggestions({ if ('isVisualizeAction' in visualizeTriggerFieldContext) { dataSourceSuggestions = datasource.getDatasourceSuggestionsForVisualizeCharts( datasourceState, - visualizeTriggerFieldContext.layers + visualizeTriggerFieldContext.layers, + dataViews.indexPatterns ); } else { // used for navigating from Discover to Lens dataSourceSuggestions = datasource.getDatasourceSuggestionsForVisualizeField( datasourceState, visualizeTriggerFieldContext.indexPatternId, - visualizeTriggerFieldContext.fieldName + visualizeTriggerFieldContext.fieldName, + dataViews.indexPatterns ); } } else if (field) { dataSourceSuggestions = datasource.getDatasourceSuggestionsForField( datasourceState, field, - (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]) // a field dragged to workspace should added to data layer + (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]), // a field dragged to workspace should added to data layer + dataViews.indexPatterns ); } else { dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState( datasourceState, + dataViews.indexPatterns, (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]), activeData ); @@ -162,12 +169,14 @@ export function getVisualizeFieldSuggestions({ datasourceStates, visualizationMap, visualizeTriggerFieldContext, + dataViews, }: { datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; subVisualizationId?: string; visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; + dataViews: DataViewsState; }): Suggestion | undefined { const activeVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null; const suggestions = getSuggestions({ @@ -177,6 +186,7 @@ export function getVisualizeFieldSuggestions({ activeVisualization, visualizationState: undefined, visualizeTriggerFieldContext, + dataViews, }); if (visualizeTriggerFieldContext && 'isVisualizeAction' in visualizeTriggerFieldContext) { @@ -266,7 +276,8 @@ export function getTopSuggestionForField( datasourceStates: DatasourceStates, visualizationMap: Record>, datasource: Datasource, - field: DragDropIdentifier + field: DragDropIdentifier, + dataViews: DataViewsState ) { const hasData = Object.values(datasourceLayers).some( (datasourceLayer) => datasourceLayer.getTableSpec().length > 0 @@ -288,6 +299,7 @@ export function getTopSuggestionForField( visualizationState: visualization.state, field, mainPalette, + dataViews, }); return ( suggestions.find((s) => s.visualizationId === visualization.activeId) || diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index e64cdd9bd33dca..e1bd41a3b4ea83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -209,7 +209,8 @@ export function SuggestionPanel({ const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId ? datasourceStates[activeDatasourceId] : null + activeDatasourceId ? datasourceStates[activeDatasourceId] : null, + frame.dataViews.indexPatterns ); const { suggestions, currentStateExpression, currentStateError } = useMemo(() => { const newSuggestions = missingIndexPatterns.length @@ -223,6 +224,7 @@ export function SuggestionPanel({ : undefined, visualizationState: currentVisualization.state, activeData, + dataViews: frame.dataViews, }) .filter( ({ @@ -240,6 +242,7 @@ export function SuggestionPanel({ visualizationMap[visualizationId], suggestionVisualizationState, { + dataViews: frame.dataViews, datasourceLayers: getDatasourceLayers( suggestionDatasourceId ? { @@ -514,6 +517,7 @@ function getPreviewExpression( updatedLayerApis[layerId] = datasource.getPublicAPI({ layerId, state: datasourceState, + indexPatterns: frame.dataViews.indexPatterns, }); } }); @@ -521,7 +525,8 @@ function getPreviewExpression( const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers( datasources, - datasourceStates + datasourceStates, + frame.dataViews.indexPatterns ); return visualization.toPreviewExpression( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index a611d6d43638dd..0d7a07b3a762aa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -499,6 +499,7 @@ function getTopSuggestion( subVisualizationId, activeData: props.framePublicAPI.activeData, mainPalette, + dataViews: props.framePublicAPI.dataViews, }); const suggestions = unfilteredSuggestions.filter((suggestion) => { // don't use extended versions of current data table on switching between visualizations diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index e45f23941b070f..91a5689cb62b93 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -163,7 +163,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const shouldApplyExpression = autoApplyEnabled || !initialRenderComplete.current || triggerApply; - const { datasourceLayers } = framePublicAPI; + const { datasourceLayers, dataViews } = framePublicAPI; const activeVisualization = visualization.activeId ? visualizationMap[visualization.activeId] @@ -171,7 +171,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId ? datasourceStates[activeDatasourceId] : null + activeDatasourceId ? datasourceStates[activeDatasourceId] : null, + dataViews.indexPatterns ); const missingRefsErrors = missingIndexPatterns.length @@ -218,6 +219,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ datasourceMap, datasourceStates, datasourceLayers, + indexPatterns: dataViews.indexPatterns, }); if (ast) { @@ -259,6 +261,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ missingRefsErrors.length, unknownVisError, visualization.activeId, + dataViews.indexPatterns, ]); useEffect(() => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 3eb0b14ae49fb2..457a33043b79bb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -6,14 +6,23 @@ */ import React from 'react'; -import { CoreStart } from '@kbn/core/public'; +import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public'; import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + DataViewsContract, +} from '@kbn/data-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import { + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, +} from '@kbn/data-views-plugin/public'; import { Document } from '../persistence/saved_object_store'; import { Datasource, @@ -29,6 +38,7 @@ export interface EditorFrameSetupPlugins { expressions: ExpressionsSetup; charts: ChartsPluginSetup; usageCollection?: UsageCollectionSetup; + dataViews: DataViewsPublicPluginSetup; } export interface EditorFrameStartPlugins { @@ -38,6 +48,13 @@ export interface EditorFrameStartPlugins { expressions: ExpressionsStart; uiActions: UiActionsStart; charts: ChartsPluginSetup; + dataViews: DataViewsPublicPluginStart; +} + +export interface EditorFramePlugins { + dataViews: DataViewsContract; + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; } async function collectAsyncDefinitions( @@ -67,7 +84,7 @@ export class EditorFrameService { * This is an asynchronous process. * @param doc parsed Lens saved object */ - public documentToExpression = async (doc: Document) => { + public documentToExpression = async (doc: Document, services: EditorFramePlugins) => { const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ this.loadDatasources(), this.loadVisualizations(), @@ -75,7 +92,7 @@ export class EditorFrameService { const { persistedStateToExpression } = await import('../async_services'); - return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); + return persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc, services); }; public setup(): EditorFrameSetup { @@ -99,7 +116,7 @@ export class EditorFrameService { const { EditorFrame } = await import('../async_services'); return { - EditorFrameContainer: ({ showNoDataPopover, lensInspector }) => { + EditorFrameContainer: ({ showNoDataPopover, lensInspector, indexPatternService }) => { return (
; @@ -17,27 +14,3 @@ export interface ErrorMessage { longMessage: React.ReactNode; type?: 'fixable' | 'critical'; } - -export interface IndexPattern { - id: string; - fields: IndexPatternField[]; - getFieldByName(name: string): IndexPatternField | undefined; - title: string; - name?: string; - timeFieldName?: string; - fieldFormatMap?: Record< - string, - { - id: string; - params: FieldFormatParams; - } - >; - hasRestrictions: boolean; -} - -export type IndexPatternField = FieldSpec & { - displayName: string; - aggregationRestrictions?: Partial; - meta?: boolean; - runtime?: boolean; -}; diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 29022a25a5673e..c1fa6b6506cc24 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -120,7 +120,7 @@ export interface LensEmbeddableDeps { injectFilterReferences: FilterManager['inject']; visualizationMap: VisualizationMap; datasourceMap: DatasourceMap; - indexPatternService: DataViewsContract; + dataViews: DataViewsContract; expressionRenderer: ReactExpressionRendererType; timefilter: TimefilterContract; basePath: IBasePath; @@ -770,7 +770,7 @@ export class Embeddable const { indexPatterns } = await getIndexPatternsObjects( this.savedVis?.references.map(({ id }) => id) || [], - this.deps.indexPatternService + this.deps.dataViews ); this.indexPatterns = uniqBy(indexPatterns, 'id'); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 07c5a5bdf5ef7d..1b6effea993417 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -38,7 +38,7 @@ export interface LensEmbeddableStartServices { attributeService: LensAttributeService; capabilities: RecursiveReadonly; expressionRenderer: ReactExpressionRendererType; - indexPatternService: DataViewsContract; + dataViews: DataViewsContract; uiActions?: UiActionsStart; usageCollection?: UsageCollectionSetup; documentToExpression: ( @@ -102,7 +102,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { uiActions, coreHttp, attributeService, - indexPatternService, + dataViews, capabilities, usageCollection, theme, @@ -117,7 +117,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { { attributeService, data, - indexPatternService, + dataViews, timefilter, inspector, expressionRenderer, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index afdec325e70d9c..ffb1987b54ccb6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -34,16 +34,22 @@ import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; -import type { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; +import type { + DatasourceDataPanelProps, + DataType, + FramePublicAPI, + IndexPattern, + IndexPatternField, + StateSetter, +} from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; -import type { IndexPattern, IndexPatternPrivateState, IndexPatternField } from './types'; +import type { IndexPatternPrivateState } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { loadIndexPatterns, syncExistingFields } from './loader'; -import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { FieldGroups, FieldList } from './field_list'; -import { IndexPatternRef } from '../shared_components'; +import { fieldContainsData, fieldExists } from '../shared_components'; +import { IndexPatternServiceAPI } from '../data_views_service/service'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; @@ -57,6 +63,9 @@ export type Props = Omit, 'co charts: ChartsPluginSetup; core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + frame: FramePublicAPI; + indexPatternService: IndexPatternServiceAPI; + onIndexPatternRefresh: () => void; }; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { @@ -131,40 +140,26 @@ export function IndexPatternDataPanel({ dropOntoWorkspace, hasSuggestionForField, uiActions, + indexPatternService, + frame, + onIndexPatternRefresh, }: Props) { - const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; + const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } = + frame.dataViews; + const { currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( (id: string) => changeIndexPattern(id, state, setState), [state, setState, changeIndexPattern] ); - const onUpdateIndexPattern = useCallback( - (indexPattern: IndexPattern) => { - setState((prevState) => ({ - ...prevState, - indexPatterns: { - ...prevState.indexPatterns, - [indexPattern.id]: indexPattern, - }, - })); - }, - [setState] - ); - const indexPatternList = uniq( Object.values(state.layers) .map((l) => l.indexPatternId) .concat(currentIndexPatternId) ) .filter((id) => !!indexPatterns[id]) - .sort((a, b) => a.localeCompare(b)) - .map((id) => ({ - id, - title: indexPatterns[id].title, - timeFieldName: indexPatterns[id].timeFieldName, - fields: indexPatterns[id].fields, - hasRestrictions: indexPatterns[id].hasRestrictions, - })); + .sort() + .map((id) => indexPatterns[id]); const dslQuery = buildSafeEsQuery( indexPatterns[currentIndexPatternId], @@ -177,15 +172,14 @@ export function IndexPatternDataPanel({ <> - syncExistingFields({ + indexPatternService.refreshExistingFields({ dateRange, - setState, - isFirstExistenceFetch: state.isFirstExistenceFetch, currentIndexPatternTitle: indexPatterns[currentIndexPatternId]?.title || '', - showNoDataPopover, - indexPatterns: indexPatternList, - fetchJson: core.http.post, + onNoData: showNoDataPopover, dslQuery, + indexPatternList, + isFirstExistenceFetch, + existingFields, }) } loadDeps={[ @@ -194,7 +188,6 @@ export function IndexPatternDataPanel({ dateRange.fromDate, dateRange.toDate, indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','), - state.indexPatterns, ]} /> @@ -226,8 +219,6 @@ export function IndexPatternDataPanel({ ) : ( )} @@ -282,42 +272,35 @@ const fieldSearchDescriptionId = htmlId(); export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ currentIndexPatternId, - indexPatternRefs, - indexPatterns, - existenceFetchFailed, - existenceFetchTimeout, query, dateRange, filters, dragDropContext, onChangeIndexPattern, - onUpdateIndexPattern, core, data, dataViews, fieldFormats, indexPatternFieldEditor, - existingFields, charts, dropOntoWorkspace, hasSuggestionForField, uiActions, + indexPatternService, + frame, + onIndexPatternRefresh, }: Omit & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; dragDropContext: DragContextState; onChangeIndexPattern: (newId: string) => void; - onUpdateIndexPattern: (indexPattern: IndexPattern) => void; - existingFields: IndexPatternPrivateState['existingFields']; charts: ChartsPluginSetup; + frame: FramePublicAPI; indexPatternFieldEditor: IndexPatternFieldEditorStart; - existenceFetchFailed?: boolean; - existenceFetchTimeout?: boolean; + onIndexPatternRefresh: () => void; }) { const [localState, setLocalState] = useState({ nameFilter: '', @@ -327,13 +310,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ isEmptyAccordionOpen: false, isMetaAccordionOpen: false, }); + const { existenceFetchFailed, existenceFetchTimeout, indexPatterns, existingFields } = + frame.dataViews; const currentIndexPattern = indexPatterns[currentIndexPatternId]; + const existingFieldsForIndexPattern = existingFields[currentIndexPattern?.title]; const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER); const allFields = visualizeGeoFieldTrigger ? currentIndexPattern.fields : currentIndexPattern.fields.filter(({ type }) => type !== 'geo_point' && type !== 'geo_shape'); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); - const hasSyncedExistingFields = existingFields[currentIndexPattern.title]; const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( (type) => type in fieldTypeNames ); @@ -346,9 +331,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ const unfilteredFieldGroups: FieldGroups = useMemo(() => { const containsData = (field: IndexPatternField) => { const overallField = currentIndexPattern.getFieldByName(field.name); - return ( - overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name) + overallField && + existingFieldsForIndexPattern && + fieldExists(existingFieldsForIndexPattern, overallField.name) ); }; @@ -460,7 +446,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ filters.length, existenceFetchTimeout, currentIndexPattern, - existingFields, + existingFieldsForIndexPattern, ]); const fieldGroups: FieldGroups = useMemo(() => { @@ -487,10 +473,9 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]); const checkFieldExists = useCallback( - (field) => - field.type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field.name), - [existingFields, currentIndexPattern.title] + (field: IndexPatternField) => + fieldContainsData(field.name, currentIndexPattern, existingFieldsForIndexPattern), + [currentIndexPattern, existingFieldsForIndexPattern] ); const { nameFilter, typeFilter } = localState; @@ -515,15 +500,24 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }, []); const refreshFieldList = useCallback(async () => { - const newlyMappedIndexPattern = await loadIndexPatterns({ - indexPatternsService: dataViews, - cache: {}, + const newlyMappedIndexPattern = await indexPatternService.loadIndexPatterns({ patterns: [currentIndexPattern.id], + cache: {}, + onIndexPatternRefresh, + }); + indexPatternService.updateIndexPatternsCache({ + ...frame.dataViews.indexPatterns, + [currentIndexPattern.id]: newlyMappedIndexPattern[currentIndexPattern.id], }); - onUpdateIndexPattern(newlyMappedIndexPattern[currentIndexPattern.id]); // start a new session so all charts are refreshed data.search.session.start(); - }, [data, dataViews, currentIndexPattern, onUpdateIndexPattern]); + }, [ + indexPatternService, + currentIndexPattern.id, + onIndexPatternRefresh, + frame.dataViews.indexPatterns, + data.search.session, + ]); const editField = useMemo( () => @@ -712,7 +706,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ exists={checkFieldExists} fieldProps={fieldProps} fieldGroups={fieldGroups} - hasSyncedExistingFields={!!hasSyncedExistingFields} + hasSyncedExistingFields={!!existingFieldsForIndexPattern} filter={filter} currentIndexPatternId={currentIndexPatternId} existenceFetchFailed={existenceFetchFailed} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index fbecfeed0f3214..f876536ebba43b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { GenericIndexPatternColumn } from '../indexpattern'; -import { IndexPatternField } from '../types'; +import { IndexPatternField } from '../../editor_frame_service/types'; const fieldMap: Record = { a: { displayName: 'a' } as IndexPatternField, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 4e667e2aa30d62..5a76a05b574cb1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -37,7 +37,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField } from '../pure_utils'; import { fieldIsInvalid } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; -import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types'; +import type { IndexPatternLayer } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { FormatSelector } from './format_selector'; import { ReferenceEditor } from './reference_editor'; @@ -62,7 +62,8 @@ import { NameInput } from '../../shared_components'; import { ParamEditorProps } from '../operations/definitions'; import { WrappingHelpPopover } from '../help_popover'; import { isColumn } from '../operations/definitions/helpers'; -import { FieldChoiceWithOperationType } from './field_select'; +import type { FieldChoiceWithOperationType } from './field_select'; +import type { IndexPattern, IndexPatternField } from '../../types'; export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: GenericIndexPatternColumn; @@ -305,7 +306,7 @@ export function DimensionEditor(props: DimensionEditorProps) { disabledStatus: definition.getDisabledStatus && definition.getDisabledStatus( - state.indexPatterns[state.currentIndexPatternId], + props.indexPatterns[state.currentIndexPatternId], state.layers[layerId], layerType ), @@ -542,7 +543,7 @@ export function DimensionEditor(props: DimensionEditorProps) { setIsCloseable, paramEditorCustomProps, ReferenceEditor, - existingFields: state.existingFields, + existingFields: props.existingFields, ...services, }; @@ -645,7 +646,7 @@ export function DimensionEditor(props: DimensionEditorProps) { }} validation={validation} currentIndexPattern={currentIndexPattern} - existingFields={state.existingFields} + existingFields={props.existingFields} selectionStyle={selectedOperationDefinition.selectionStyle} dateRange={dateRange} labelAppend={selectedOperationDefinition?.getHelpMessage?.({ @@ -671,7 +672,7 @@ export function DimensionEditor(props: DimensionEditorProps) { selectedColumn={selectedColumn as FieldBasedIndexPatternColumn} columnId={columnId} indexPattern={currentIndexPattern} - existingFields={state.existingFields} + existingFields={props.existingFields} operationSupportMatrix={operationSupportMatrix} updateLayer={(newLayer) => { if (temporaryQuickFunction) { @@ -702,7 +703,7 @@ export function DimensionEditor(props: DimensionEditorProps) { const customParamEditor = ParamEditor ? ( <> { dropType?: DropType; source: T; target: DataViewDragDropOperation; + indexPatterns: IndexPatternMap; } export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const { target, source, dropType, state } = props; + const { target, source, dropType, state, indexPatterns } = props; if (isDraggedField(source) && isFieldDropType(dropType)) { return onFieldDrop( @@ -53,9 +55,10 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps ['field_add', 'field_replace', 'field_combine'].includes(dropType); function onFieldDrop(props: DropHandlerProps, shouldAddField?: boolean) { - const { setState, state, source, target, dimensionGroups } = props; + const { setState, state, source, target, dimensionGroups, indexPatterns } = props; const prioritizedOperation = dimensionGroups.find( (g) => g.groupId === target.groupId )?.prioritizedOperation; const layer = state.layers[target.layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const targetColumn = layer.columns[target.columnId]; const newOperation = shouldAddField ? targetColumn.operationType @@ -238,13 +242,20 @@ function onReorder({ } function onMoveIncompatible( - { setState, state, source, dimensionGroups, target }: DropHandlerProps, + { + setState, + state, + source, + dimensionGroups, + target, + indexPatterns, + }: DropHandlerProps, shouldDeleteSource?: boolean ) { const targetLayer = state.layers[target.layerId]; const targetColumn = targetLayer.columns[target.columnId] || null; const sourceLayer = state.layers[source.layerId]; - const indexPattern = state.indexPatterns[sourceLayer.indexPatternId]; + const indexPattern = indexPatterns[sourceLayer.indexPatternId]; const sourceColumn = sourceLayer.columns[source.columnId]; const sourceField = getField(sourceColumn, indexPattern); const newOperation = getNewOperation(sourceField, target.filterOperations, targetColumn); @@ -313,10 +324,11 @@ function onSwapIncompatible({ source, dimensionGroups, target, + indexPatterns, }: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; const sourceLayer = state.layers[source.layerId]; - const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const indexPattern = indexPatterns[targetLayer.indexPatternId]; const sourceColumn = sourceLayer.columns[source.columnId]; const targetColumn = targetLayer.columns[target.columnId]; @@ -466,11 +478,12 @@ function onCombine({ source, target, dimensionGroups, + indexPatterns, }: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; const targetColumn = targetLayer.columns[target.columnId]; const targetField = getField(targetColumn, target.dataView); - const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const indexPattern = indexPatterns[targetLayer.indexPatternId]; const sourceLayer = state.layers[source.layerId]; const sourceColumn = sourceLayer.columns[source.columnId]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 16e70f5657db0a..b1ce4462528701 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -11,11 +11,12 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { fieldExists } from '../pure_helpers'; import type { OperationType } from '../indexpattern'; import type { OperationSupportMatrix } from './operation_support'; -import type { IndexPattern, IndexPatternPrivateState } from '../types'; +import type { IndexPatternPrivateState } from '../types'; import { FieldOption, FieldOptionValue, FieldPicker } from '../../shared_components/field_picker'; +import type { IndexPattern } from '../../editor_frame_service/types'; +import { fieldContainsData } from '../../shared_components'; export type FieldChoiceWithOperationType = FieldOptionValue & { operationType: OperationType; @@ -62,9 +63,10 @@ export function FieldSelect({ fields, (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); - const containsData = (field: string) => - currentIndexPattern.getFieldByName(field)?.type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field); + + function containsData(field: string) { + return fieldContainsData(field, currentIndexPattern, existingFields); + } function fieldNamesToOptions(items: string[]) { return items diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 2f703547219ec3..43d9770deb2281 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -6,7 +6,7 @@ */ import memoizeOne from 'memoize-one'; -import { DatasourceDimensionDropProps, OperationMetadata } from '../../types'; +import { DatasourceDimensionDropProps, IndexPatternMap, OperationMetadata } from '../../types'; import { OperationType } from '../indexpattern'; import { memoizedGetAvailableOperationsByMetadata, OperationFieldTuple } from '../operations'; import { IndexPatternPrivateState } from '../types'; @@ -20,7 +20,7 @@ export interface OperationSupportMatrix { type Props = Pick< DatasourceDimensionDropProps['target'], 'layerId' | 'columnId' | 'filterOperations' -> & { state: IndexPatternPrivateState }; +> & { state: IndexPatternPrivateState; indexPatterns: IndexPatternMap }; function computeOperationMatrix( operationsByMetadata: Array<{ @@ -67,7 +67,7 @@ const memoizedComputeOperationsMatrix = memoizeOne(computeOperationMatrix); // TODO: the support matrix should be available outside of the dimension panel export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { const layerId = props.layerId; - const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + const currentIndexPattern = props.indexPatterns[props.state.layers[layerId].indexPatternId]; const operationsByMetadata = memoizedGetAvailableOperationsByMetadata(currentIndexPattern); return memoizedComputeOperationsMatrix(operationsByMetadata, props.filterOperations); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 27c774ed2963ec..0e45614665e2eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -44,9 +44,9 @@ import { KBN_FIELD_TYPES, ES_FIELD_TYPES, getEsQueryConfig } from '@kbn/data-plu import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; -import { DatasourceDataPanelProps, DataType } from '../types'; +import type { DatasourceDataPanelProps, DataType, IndexPattern, IndexPatternField } from '../types'; import { BucketedAggregation, DOCUMENT_FIELD_NAME, FieldStatsResponse } from '../../common'; -import { IndexPattern, IndexPatternField, DraggedField } from './types'; +import { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index efaaf86217f060..18da8488db212f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -6,15 +6,14 @@ */ import './field_list.scss'; -import { throttle } from 'lodash'; +import { partition, throttle } from 'lodash'; import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { FieldItem } from './field_item'; import { NoFieldsCallout } from './no_fields_callout'; -import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; -import { DatasourceDataPanelProps } from '../types'; +import type { DatasourceDataPanelProps, IndexPatternField } from '../types'; const PAGINATION_SIZE = 50; export type FieldGroups = Record< @@ -76,13 +75,15 @@ export const FieldList = React.memo(function FieldList({ removeField?: (name: string) => void; uiActions: UiActionsStart; }) { + const [fieldGroupsToShow, fieldFroupsToCollapse] = partition( + Object.entries(fieldGroups), + ([, { showInAccordion }]) => showInAccordion + ); const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); const [accordionState, setAccordionState] = useState>>(() => Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) + fieldGroupsToShow.map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) ) ); @@ -116,18 +117,16 @@ export const FieldList = React.memo(function FieldList({ const paginatedFields = useMemo(() => { let remainingItems = pageSize; return Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup]) => { - if (!accordionState[key] || remainingItems <= 0) { - return [key, []]; - } - const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); - remainingItems = remainingItems - slicedFieldList.length; - return [key, slicedFieldList]; - }) + fieldGroupsToShow.map(([key, fieldGroup]) => { + if (!accordionState[key] || remainingItems <= 0) { + return [key, []]; + } + const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); + remainingItems = remainingItems - slicedFieldList.length; + return [key, slicedFieldList]; + }) ); - }, [pageSize, fieldGroups, accordionState]); + }, [pageSize, fieldGroupsToShow, accordionState]); return (
    - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => !showInAccordion) - .flatMap(([, { fields }]) => - fields.map((field, index) => ( - - )) - )} -
- - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup], index) => ( - - + fields.map((field, index) => ( + { - setAccordionState((s) => ({ - ...s, - [key]: open, - })); - const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { - ...accordionState, - [key]: open, - }); - setPageSize( - Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) - ); - }} - showExistenceFetchError={existenceFetchFailed} - showExistenceFetchTimeout={existenceFetchTimeout} - renderCallout={ - - } + hideDetails={true} + key={field.name} + itemIndex={index} + groupIndex={0} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} uiActions={uiActions} /> - - - ))} + )) + )} + + + {fieldGroupsToShow.map(([key, fieldGroup], index) => ( + + { + setAccordionState((s) => ({ + ...s, + [key]: open, + })); + const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { + ...accordionState, + [key]: open, + }); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + showExistenceFetchError={existenceFetchFailed} + showExistenceFetchTimeout={existenceFetchTimeout} + renderCallout={ + + } + uiActions={uiActions} + /> + + + ))}
); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 33413bf0ba59bb..5db910e6d3effa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -22,10 +22,8 @@ import { Filter } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { IndexPatternField } from './types'; import { FieldItem } from './field_item'; -import { DatasourceDataPanelProps } from '../types'; -import { IndexPattern } from './types'; +import type { DatasourceDataPanelProps, IndexPattern, IndexPatternField } from '../types'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 41f2a4df9bcda3..4bf33013b3280a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -17,7 +17,7 @@ import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { FieldFormatsStart, FieldFormatsSetup } from '@kbn/field-formats-plugin/public'; import type { EditorFrameSetup } from '../types'; -export type { PersistedIndexPatternLayer, IndexPattern, FormulaPublicApi } from './types'; +export type { PersistedIndexPatternLayer, FormulaPublicApi } from './types'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index f970088d50e924..c468a5b95c17eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -31,15 +31,17 @@ import type { InitializationOptions, OperationDescriptor, FramePublicAPI, + IndexPatternField, + IndexPattern, + IndexPatternRef, } from '../types'; import { - loadInitialState, changeIndexPattern, changeLayerIndexPattern, extractReferences, injectReferences, - loadIndexPatterns, - getIndexPatterns, + loadInitialState, + onRefreshIndexPattern, } from './loader'; import { toExpression } from './to_expression'; import { @@ -67,14 +69,9 @@ import { TermsIndexPatternColumn, } from './operations'; import { getReferenceRoot } from './operations/layer_helpers'; -import { - IndexPatternField, - IndexPatternPrivateState, - IndexPatternPersistedState, - IndexPattern, -} from './types'; +import { IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { mergeLayer } from './state_helpers'; -import { Datasource, StateSetter, VisualizeEditorContext } from '../types'; +import { Datasource, VisualizeEditorContext } from '../types'; import { deleteColumn, isReferenced } from './operations'; import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; import { DraggingIdentifier } from '../drag_drop'; @@ -137,38 +134,17 @@ export function getIndexPatternDatasource({ uiActions: UiActionsStart; }) { const uiSettings = core.uiSettings; - const onIndexPatternLoadError = (err: Error) => - core.notifications.toasts.addError(err, { - title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { - defaultMessage: 'Error loading data view', - }), - }); - - const indexPatternsService = dataViews; - - const handleChangeIndexPattern = ( - id: string, - state: IndexPatternPrivateState, - setState: StateSetter - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }; // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { id: 'indexpattern', - async initialize( + initialize( persistedState?: IndexPatternPersistedState, references?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, + indexPatternRefs?: IndexPatternRef[], + indexPatterns?: Record, options?: InitializationOptions ) { return loadInitialState({ @@ -176,14 +152,21 @@ export function getIndexPatternDatasource({ references, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, - indexPatternsService, initialContext, - options, + indexPatternRefs, + indexPatterns, }); + // return loadInitialState({ + // persistedState, + // references, + // defaultIndexPatternId: core.uiSettings.get('defaultIndex'), + // storage, + // indexPatternsService, + // initialContext, + // options, + // }); }, - getIndexPatterns, - getPersistableState(state: IndexPatternPrivateState) { return extractReferences(state); }, @@ -226,8 +209,8 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, - removeColumn({ prevState, layerId, columnId }) { - const indexPattern = prevState.indexPatterns[prevState.layers[layerId]?.indexPatternId]; + removeColumn({ prevState, layerId, columnId, indexPatterns }) { + const indexPattern = indexPatterns[prevState.layers[layerId]?.indexPatternId]; return mergeLayer({ state: prevState, layerId, @@ -239,8 +222,8 @@ export function getIndexPatternDatasource({ }); }, - initializeDimension(state, layerId, { columnId, groupId, staticValue }) { - const indexPattern = state.indexPatterns[state.layers[layerId]?.indexPatternId]; + initializeDimension(state, layerId, indexPatterns, { columnId, groupId, staticValue }) { + const indexPattern = indexPatterns[state.layers[layerId]?.indexPatternId]; if (staticValue == null) { return state; } @@ -261,7 +244,8 @@ export function getIndexPatternDatasource({ }); }, - toExpression: (state, layerId) => toExpression(state, layerId, uiSettings), + toExpression: (state, layerId, indexPatterns) => + toExpression(state, layerId, indexPatterns, uiSettings), renderDataPanel( domElement: Element, @@ -271,7 +255,16 @@ export function getIndexPatternDatasource({ { + changeIndexPattern({ + id, + state, + setState, + storage, + indexPatterns: props.frame.dataViews.indexPatterns, + indexPatternService: props.indexPatternService, + }); + }} data={data} dataViews={dataViews} fieldFormats={fieldFormats} @@ -280,6 +273,7 @@ export function getIndexPatternDatasource({ {...props} core={core} uiActions={uiActions} + onIndexPatternRefresh={onRefreshIndexPattern} /> , @@ -319,10 +313,10 @@ export function getIndexPatternDatasource({ return columnLabelMap; }, - isValidColumn: (state: IndexPatternPrivateState, layerId: string, columnId: string) => { + isValidColumn: (state, indexPatterns, layerId, columnId) => { const layer = state.layers[layerId]; - return !isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]); + return !isColumnInvalid(layer, columnId, indexPatterns[layer.indexPatternId]); }, renderDimensionTrigger: ( @@ -408,10 +402,10 @@ export function getIndexPatternDatasource({ setState: props.setState, state: props.state, layerId: props.layerId, - onError: onIndexPatternLoadError, replaceIfPossible: true, storage, - indexPatternsService, + indexPatterns: props.dataViews.indexPatterns, + indexPatternService: props.indexPatternService, }); }} {...props} @@ -457,29 +451,14 @@ export function getIndexPatternDatasource({ }, updateCurrentIndexPatternId: ({ state, indexPatternId, setState }) => { - handleChangeIndexPattern(indexPatternId, state, setState); - }, - - refreshIndexPatternsList: async ({ indexPatternId, setState }) => { - const newlyMappedIndexPattern = await loadIndexPatterns({ - indexPatternsService: dataViews, - cache: {}, - patterns: [indexPatternId], - }); - const indexPatternRefs = await dataViews.getIdsWithTitle(); - const indexPattern = newlyMappedIndexPattern[indexPatternId]; - setState((s) => { - return { - ...s, - indexPatterns: { - ...s.indexPatterns, - [indexPattern.id]: indexPattern, - }, - indexPatternRefs, - }; + setState({ + ...state, + currentIndexPatternId: indexPatternId, }); }, + onRefreshIndexPattern, + // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId }) => { @@ -494,7 +473,7 @@ export function getIndexPatternDatasource({ }); }, - getPublicAPI({ state, layerId }: PublicAPIProps) { + getPublicAPI({ state, layerId, indexPatterns }: PublicAPIProps) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); const layer = state.layers[layerId]; const visibleColumnIds = layer.columnOrder.filter((colId) => !isReferenced(layer, colId)); @@ -530,7 +509,7 @@ export function getIndexPatternDatasource({ return columnToOperation( layer.columns[columnId], columnLabelMap[columnId], - state.indexPatterns[layer.indexPatternId] + indexPatterns[layer.indexPatternId] ); } } @@ -542,18 +521,19 @@ export function getIndexPatternDatasource({ layer, visibleColumnIds, activeData?.[layerId], - state.indexPatterns[layer.indexPatternId], + indexPatterns[layer.indexPatternId], timeRange ), getVisualDefaults: () => getVisualDefaultsForLayer(layer), }; }, - getDatasourceSuggestionsForField(state, draggedField, filterLayers) { + getDatasourceSuggestionsForField(state, draggedField, filterLayers, indexPatterns) { return isDraggedField(draggedField) ? getDatasourceSuggestionsForField( state, draggedField.indexPatternId, draggedField.field, + indexPatterns, filterLayers ) : []; @@ -562,23 +542,17 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsForVisualizeField, getDatasourceSuggestionsForVisualizeCharts, - getErrorMessages(state) { + getErrorMessages(state, indexPatterns) { if (!state) { return; } // Forward the indexpattern as well, as it is required by some operationType checks const layerErrors = Object.entries(state.layers) - .filter(([_, layer]) => !!state.indexPatterns[layer.indexPatternId]) + .filter(([_, layer]) => !!indexPatterns[layer.indexPatternId]) .map(([layerId, layer]) => ( - getErrorMessages( - layer, - state.indexPatterns[layer.indexPatternId], - state, - layerId, - core - ) ?? [] + getErrorMessages(layer, indexPatterns[layer.indexPatternId], state, layerId, core) ?? [] ).map((message) => ({ shortMessage: '', // Not displayed currently longMessage: typeof message === 'string' ? message : message.message, @@ -631,13 +605,13 @@ export function getIndexPatternDatasource({ ), ]; }, - checkIntegrity: (state) => { + checkIntegrity: (state, indexPatterns) => { const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId); - return ids.filter((id) => !state.indexPatterns[id]); + return ids.filter((id) => !indexPatterns[id]); }, - isTimeBased: (state) => { + isTimeBased: (state, indexPatterns) => { if (!state) return false; - const { layers, indexPatterns } = state; + const { layers } = state; return ( Boolean(layers) && Object.values(layers).some((layer) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 720c917dcbc53b..319c309cac0363 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -9,7 +9,13 @@ import { flatten, minBy, pick, mapValues, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/public'; import { generateId } from '../id_generator'; -import type { DatasourceSuggestion, TableChangeType } from '../types'; +import type { + DatasourceSuggestion, + IndexPattern, + IndexPatternField, + IndexPatternMap, + TableChangeType, +} from '../types'; import { columnToOperation } from './indexpattern'; import { insertNewColumn, @@ -28,12 +34,7 @@ import { hasTermsWithManyBuckets, } from './operations'; import { hasField } from './pure_utils'; -import type { - IndexPattern, - IndexPatternPrivateState, - IndexPatternLayer, - IndexPatternField, -} from './types'; +import type { IndexPatternPrivateState, IndexPatternLayer } from './types'; import { documentField } from './document_field'; export type IndexPatternSuggestion = DatasourceSuggestion; @@ -100,6 +101,7 @@ export function getDatasourceSuggestionsForField( state: IndexPatternPrivateState, indexPatternId: string, field: IndexPatternField, + indexPatterns: IndexPatternMap, filterLayers?: (layerId: string) => boolean ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); @@ -113,8 +115,20 @@ export function getDatasourceSuggestionsForField( // This generates a set of suggestions where we add a layer. // A second set of suggestions is generated for visualizations that don't work with layers const newId = generateId(); - return getEmptyLayerSuggestionsForField(state, newId, indexPatternId, field).concat( - getEmptyLayerSuggestionsForField({ ...state, layers: {} }, newId, indexPatternId, field) + return getEmptyLayerSuggestionsForField( + state, + newId, + indexPatternId, + field, + indexPatterns + ).concat( + getEmptyLayerSuggestionsForField( + { ...state, layers: {} }, + newId, + indexPatternId, + field, + indexPatterns + ) ); } else { // The field we're suggesting on matches an existing layer. In this case we find the layer with @@ -125,9 +139,15 @@ export function getDatasourceSuggestionsForField( (layerId) => state.layers[layerId].columnOrder.length ) as string; if (state.layers[mostEmptyLayerId].columnOrder.length === 0) { - return getEmptyLayerSuggestionsForField(state, mostEmptyLayerId, indexPatternId, field); + return getEmptyLayerSuggestionsForField( + state, + mostEmptyLayerId, + indexPatternId, + field, + indexPatterns + ); } else { - return getExistingLayerSuggestionsForField(state, mostEmptyLayerId, field); + return getExistingLayerSuggestionsForField(state, mostEmptyLayerId, field, indexPatterns); } } } @@ -135,24 +155,26 @@ export function getDatasourceSuggestionsForField( // Called when the user navigates from Visualize editor to Lens export function getDatasourceSuggestionsForVisualizeCharts( state: IndexPatternPrivateState, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter( (id) => state.layers[id].indexPatternId === context[0].indexPatternId ); if (layerIds.length !== 0) return []; - return getEmptyLayersSuggestionsForVisualizeCharts(state, context); + return getEmptyLayersSuggestionsForVisualizeCharts(state, context, indexPatterns); } function getEmptyLayersSuggestionsForVisualizeCharts( state: IndexPatternPrivateState, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const suggestions: IndexPatternSuggestion[] = []; for (let layerIdx = 0; layerIdx < context.length; layerIdx++) { const layer = context[layerIdx]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; if (!indexPattern) return []; const newId = generateId(); @@ -228,18 +250,31 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( export function getDatasourceSuggestionsForVisualizeField( state: IndexPatternPrivateState, indexPatternId: string, - fieldName: string + fieldName: string, + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName - const indexPattern = state.indexPatterns[indexPatternId]; + const indexPattern = indexPatterns[indexPatternId]; const field = indexPattern.getFieldByName(fieldName); if (layerIds.length !== 0 || !field) return []; const newId = generateId(); - return getEmptyLayerSuggestionsForField(state, newId, indexPatternId, field).concat( - getEmptyLayerSuggestionsForField({ ...state, layers: {} }, newId, indexPatternId, field) + return getEmptyLayerSuggestionsForField( + state, + newId, + indexPatternId, + field, + indexPatterns + ).concat( + getEmptyLayerSuggestionsForField( + { ...state, layers: {} }, + newId, + indexPatternId, + field, + indexPatterns + ) ); } @@ -255,10 +290,11 @@ function getBucketOperation(field: IndexPatternField) { function getExistingLayerSuggestionsForField( state: IndexPatternPrivateState, layerId: string, - field: IndexPatternField + field: IndexPatternField, + indexPatterns: IndexPatternMap ) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const operations = getOperationTypesForField(field); const usableAsBucketOperation = getBucketOperation(field); const fieldInUse = Object.values(layer.columns).some( @@ -369,9 +405,10 @@ function getEmptyLayerSuggestionsForField( state: IndexPatternPrivateState, layerId: string, indexPatternId: string, - field: IndexPatternField + field: IndexPatternField, + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { - const indexPattern = state.indexPatterns[indexPatternId]; + const indexPattern = indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; const bucketOperation = getBucketOperation(field); if (bucketOperation) { @@ -447,6 +484,7 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState, + indexPatterns: IndexPatternMap, filterLayers: (layerId: string) => boolean = () => true ): Array> { const layers = Object.entries(state.layers || {}).filter(([layerId]) => filterLayers(layerId)); @@ -467,7 +505,7 @@ export function getDatasourceSuggestionsFromCurrentState( }) : i18n.translate('xpack.lens.indexPatternSuggestion.removeLayerLabel', { defaultMessage: 'Show only {indexPatternTitle}', - values: { indexPatternTitle: state.indexPatterns[layer.indexPatternId].title }, + values: { indexPatternTitle: indexPatterns[layer.indexPatternId].title }, }); return buildSuggestion({ @@ -495,7 +533,7 @@ export function getDatasourceSuggestionsFromCurrentState( layers .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const [buckets, metrics, references] = getExistingColumnGroups(layer); const timeDimension = layer.columnOrder.find( (columnId) => @@ -522,7 +560,9 @@ export function getDatasourceSuggestionsFromCurrentState( if (!references.length && metrics.length && buckets.length === 0) { if (timeField && buckets.length < 1 && !hasTermsWithManyBuckets(layer)) { // suggest current metric over time if there is a default time field - suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); + suggestions.push( + createSuggestionWithDefaultDateHistogram(state, layerId, timeField, indexPatterns) + ); } if (indexPattern) { suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); @@ -541,11 +581,13 @@ export function getDatasourceSuggestionsFromCurrentState( ) { // suggest current configuration over time if there is a default time field // and no time dimension yet - suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); + suggestions.push( + createSuggestionWithDefaultDateHistogram(state, layerId, timeField, indexPatterns) + ); } if (buckets.length === 2) { - suggestions.push(createChangedNestingSuggestion(state, layerId)); + suggestions.push(createChangedNestingSuggestion(state, layerId, indexPatterns)); } } return suggestions; @@ -553,11 +595,15 @@ export function getDatasourceSuggestionsFromCurrentState( ); } -function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId: string) { +function createChangedNestingSuggestion( + state: IndexPatternPrivateState, + layerId: string, + indexPatterns: IndexPatternMap +) { const layer = state.layers[layerId]; const [firstBucket, secondBucket, ...rest] = layer.columnOrder; const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; - const indexPattern = state.indexPatterns[state.currentIndexPatternId]; + const indexPattern = indexPatterns[state.currentIndexPatternId]; const firstBucketColumn = layer.columns[firstBucket]; const firstBucketLabel = (hasField(firstBucketColumn) && @@ -670,10 +716,11 @@ function createAlternativeMetricSuggestions( function createSuggestionWithDefaultDateHistogram( state: IndexPatternPrivateState, layerId: string, - timeField: IndexPatternField + timeField: IndexPatternField, + indexPatterns: IndexPatternMap ) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; return buildSuggestion({ state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index 63099f363ecf4a..9824f70eeddfc0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -18,10 +18,15 @@ export interface IndexPatternLayerPanelProps onChangeIndexPattern: (newId: string) => void; } -export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatternLayerPanelProps) { +export function LayerPanel({ + state, + layerId, + onChangeIndexPattern, + dataViews, +}: IndexPatternLayerPanelProps) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = dataViews.indexPatterns[layer.indexPatternId]; const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', }); @@ -37,7 +42,7 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter fontWeight: 'normal', }} indexPatternId={layer.indexPatternId} - indexPatternRefs={state.indexPatternRefs} + indexPatternRefs={dataViews.indexPatternRefs} isMissingCurrent={!indexPattern} onChangeIndexPattern={onChangeIndexPattern} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 5ccba73dd7e422..047da93512951b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -7,151 +7,23 @@ import { uniq, mapValues, difference } from 'lodash'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import type { HttpSetup, SavedObjectReference } from '@kbn/core/public'; -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; -import { isNestedField } from '@kbn/data-views-plugin/common'; +import type { SavedObjectReference } from '@kbn/core/public'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; -import type { - DatasourceDataPanelProps, - InitializationOptions, - VisualizeEditorContext, -} from '../types'; -import { - IndexPattern, - IndexPatternPersistedState, - IndexPatternPrivateState, - IndexPatternField, - IndexPatternLayer, -} from './types'; +import type { DatasourceDataPanelProps, VisualizeEditorContext } from '../types'; +import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternLayer } from './types'; -import { updateLayerIndexPattern, translateToOperationName } from './operations'; -import { DateRange, ExistingFields } from '../../common/types'; -import { BASE_API_URL } from '../../common'; -import { documentField } from './document_field'; +import { memoizedGetAvailableOperationsByMetadata, updateLayerIndexPattern } from './operations'; import { readFromStorage, writeToStorage } from '../settings_storage'; -import { getFieldByNameFactory } from './pure_helpers'; -import { memoizedGetAvailableOperationsByMetadata } from './operations'; -import type { IndexPatternRef } from '../shared_components'; +import type { IndexPattern, IndexPatternRef } from '../types'; +import { IndexPatternServiceAPI } from '../data_views_service/service'; type SetState = DatasourceDataPanelProps['setState']; -type IndexPatternsService = Pick; -type ErrorHandler = (err: Error) => void; - -export function convertDataViewIntoLensIndexPattern(dataView: DataView): IndexPattern { - const newFields = dataView.fields - .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) - .map((field): IndexPatternField => { - // Convert the getters on the index pattern service into plain JSON - const base = { - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - meta: dataView.metaFields.includes(field.name), - esTypes: field.esTypes, - scripted: field.scripted, - runtime: Boolean(field.runtimeField), - }; - - // Simplifies tests by hiding optional properties instead of undefined - return base.scripted - ? { - ...base, - lang: field.lang, - script: field.script, - } - : base; - }) - .concat(documentField); - - const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; - if (typeMeta?.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[translateToOperationName(agg)] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - return { - id: dataView.id!, // id exists for sure because we got index patterns by id - title, - name: name ? name : title, - timeFieldName, - fieldFormatMap: - fieldFormatMap && - Object.fromEntries( - Object.entries(fieldFormatMap).map(([id, format]) => [ - id, - 'toJSON' in format ? format.toJSON() : format, - ]) - ), - fields: newFields, - getFieldByName: getFieldByNameFactory(newFields), - hasRestrictions: !!typeMeta?.aggs, - }; -} - -export async function loadIndexPatterns({ - indexPatternsService, - patterns, - notUsedPatterns, - cache, -}: { - indexPatternsService: IndexPatternsService; - patterns: string[]; - notUsedPatterns?: string[]; - cache: Record; -}) { - const missingIds = patterns.filter((id) => !cache[id]); - - if (missingIds.length === 0) { - return cache; - } +export function onRefreshIndexPattern() { if (memoizedGetAvailableOperationsByMetadata.cache.clear) { // clear operations meta data cache because index pattern reference may change memoizedGetAvailableOperationsByMetadata.cache.clear(); } - - const allIndexPatterns = await Promise.allSettled( - missingIds.map((id) => indexPatternsService.get(id)) - ); - // ignore rejected indexpatterns here, they're already handled at the app level - let indexPatterns = allIndexPatterns - .filter( - (response): response is PromiseFulfilledResult => response.status === 'fulfilled' - ) - .map((response) => response.value); - - // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds - for (let i = 0; notUsedPatterns && i < notUsedPatterns?.length && !indexPatterns.length; i++) { - const resp = await indexPatternsService.get(notUsedPatterns[i]).catch((e) => { - // do nothing - }); - if (resp) { - indexPatterns = [resp]; - } - } - - const indexPatternsObject = indexPatterns.reduce( - (acc, indexPattern) => ({ - [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern), - ...acc, - }), - { ...cache } - ); - - return indexPatternsObject; } const getLastUsedIndexPatternId = ( @@ -200,13 +72,6 @@ export function injectReferences( }; } -export function getIndexPatterns({ - indexPatternRefs, - indexPatterns, -}: Partial = {}) { - return { indexPatternRefs: indexPatternRefs ?? [], indexPatterns: indexPatterns ?? {} }; -} - export function createStateFromPersisted({ persistedState, references, @@ -252,32 +117,31 @@ export function getUsedIndexPatterns({ // take out the undefined from the list .filter(Boolean); - return { usedPatterns, indexPatternIds }; + return { + usedPatterns, + allIndexPatternIds: indexPatternIds, + }; } -// @TODO: rewire the new logic to this -export async function loadInitialStateMini({ +export function loadInitialState({ persistedState, references, defaultIndexPatternId, storage, initialContext, - indexPatternRefs, - getOrLoadIndexPatterns, + indexPatternRefs = [], + indexPatterns = {}, }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; defaultIndexPatternId?: string; storage: IStorageWrapper; initialContext?: VisualizeFieldContext | VisualizeEditorContext; - indexPatternRefs: IndexPatternRef[]; - getOrLoadIndexPatterns: (args: { - patterns: string[]; - notUsedPatterns?: string[]; - }) => Record; -}): Promise> { + indexPatternRefs?: IndexPatternRef[]; + indexPatterns?: Record; +}): IndexPatternPrivateState { const state = createStateFromPersisted({ persistedState, references }); - const { usedPatterns, indexPatternIds } = getUsedIndexPatterns({ + const { usedPatterns, allIndexPatternIds: indexPatternIds } = getUsedIndexPatterns({ state, defaultIndexPatternId, storage, @@ -289,88 +153,6 @@ export async function loadInitialStateMini({ const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedPatterns); - const indexPatterns = await getOrLoadIndexPatterns({ - patterns: usedPatterns, - notUsedPatterns, - }); - - // Priority list: - // * start with the indexPattern in context - // * then fallback to the used ones - // * then as last resort use a first one from not used refs - const currentIndexPatternId = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].find( - (id) => id != null && availableIndexPatterns.has(id) && indexPatterns[id] - ); - - if (currentIndexPatternId) { - setLastUsedIndexPatternId(storage, currentIndexPatternId); - } - - return { - layers: {}, - ...state, - currentIndexPatternId, - existingFields: {}, - isFirstExistenceFetch: true, - }; -} - -export async function loadInitialState({ - persistedState, - references, - defaultIndexPatternId, - storage, - indexPatternsService, - initialContext, - options, -}: { - persistedState?: IndexPatternPersistedState; - references?: SavedObjectReference[]; - defaultIndexPatternId?: string; - storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; - initialContext?: VisualizeFieldContext | VisualizeEditorContext; - options?: InitializationOptions; -}): Promise { - const { isFullEditor } = options ?? {}; - // make it explicit or TS will infer never[] and break few lines down - const indexPatternRefs: IndexPatternRef[] = await (isFullEditor - ? loadIndexPatternRefs(indexPatternsService) - : []); - - const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); - const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; - const indexPatternIds = []; - if (initialContext) { - if ('isVisualizeAction' in initialContext) { - for (const { indexPatternId } of initialContext.layers) { - indexPatternIds.push(indexPatternId); - } - } else { - indexPatternIds.push(initialContext.indexPatternId); - } - } - const state = - persistedState && references ? injectReferences(persistedState, references) : undefined; - const usedPatterns = ( - initialContext - ? indexPatternIds - : uniq(state ? Object.values(state.layers).map((l) => l.indexPatternId) : [fallbackId]) - ) - // take out the undefined from the list - .filter(Boolean); - - const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); - - const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedPatterns); - - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: {}, - patterns: usedPatterns, - notUsedPatterns, - }); - // Priority list: // * start with the indexPattern in context // * then fallback to the used ones @@ -387,10 +169,6 @@ export async function loadInitialState({ layers: {}, ...state, currentIndexPatternId, - indexPatternRefs, - indexPatterns, - existingFields: {}, - isFirstExistenceFetch: true, }; } @@ -398,45 +176,33 @@ export async function changeIndexPattern({ id, state, setState, - onError, storage, - indexPatternsService, + indexPatterns, + indexPatternService, }: { id: string; state: IndexPatternPrivateState; setState: SetState; - onError: ErrorHandler; storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; + indexPatterns: Record; + indexPatternService: IndexPatternServiceAPI; }) { - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: state.indexPatterns, - patterns: [id], + const newIndexPatterns = await indexPatternService.addIndexPattern({ + id, + cache: indexPatterns, }); - - if (indexPatterns[id] == null) { - return onError(Error('Missing indexpatterns')); - } - - try { + if (newIndexPatterns) { setState( (s) => ({ ...s, layers: isSingleEmptyLayer(state.layers) ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) : state.layers, - indexPatterns: { - ...s.indexPatterns, - [id]: indexPatterns[id], - }, currentIndexPatternId: id, }), { applyImmediately: true } ); setLastUsedIndexPatternId(storage, id); - } catch (err) { - onError(err); } } @@ -445,159 +211,37 @@ export async function changeLayerIndexPattern({ layerId, state, setState, - onError, replaceIfPossible, storage, - indexPatternsService, + indexPatterns, + indexPatternService, }: { indexPatternId: string; layerId: string; state: IndexPatternPrivateState; setState: SetState; - onError: ErrorHandler; replaceIfPossible?: boolean; storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; + indexPatterns: Record; + indexPatternService: IndexPatternServiceAPI; }) { - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: state.indexPatterns, - patterns: [indexPatternId], + const newIndexPatterns = await indexPatternService.addIndexPattern({ + id: indexPatternId, + cache: indexPatterns, }); - if (indexPatterns[indexPatternId] == null) { - return onError(Error('Missing indexpatterns')); - } - - try { + if (newIndexPatterns) { setState((s) => ({ ...s, layers: { ...s.layers, [layerId]: updateLayerIndexPattern(s.layers[layerId], indexPatterns[indexPatternId]), }, - indexPatterns: { - ...s.indexPatterns, - [indexPatternId]: indexPatterns[indexPatternId], - }, currentIndexPatternId: replaceIfPossible ? indexPatternId : s.currentIndexPatternId, })); setLastUsedIndexPatternId(storage, indexPatternId); - } catch (err) { - onError(err); } } -async function loadIndexPatternRefs( - indexPatternsService: IndexPatternsService -): Promise { - const indexPatterns = await indexPatternsService.getIdsWithTitle(); - - return indexPatterns.sort((a, b) => { - return a.title.localeCompare(b.title); - }); -} - -export async function syncExistingFields({ - indexPatterns, - dateRange, - fetchJson, - setState, - isFirstExistenceFetch, - currentIndexPatternTitle, - dslQuery, - showNoDataPopover, -}: { - dateRange: DateRange; - indexPatterns: Array<{ - id: string; - title: string; - fields: IndexPatternField[]; - timeFieldName?: string | null; - hasRestrictions: boolean; - }>; - fetchJson: HttpSetup['post']; - setState: SetState; - isFirstExistenceFetch: boolean; - currentIndexPatternTitle: string; - dslQuery: object; - showNoDataPopover: () => void; -}) { - const existenceRequests = indexPatterns.map((pattern) => { - if (pattern.hasRestrictions) { - return { - indexPatternTitle: pattern.title, - existingFieldNames: pattern.fields.map((field) => field.name), - }; - } - const body: Record = { - dslQuery, - fromDate: dateRange.fromDate, - toDate: dateRange.toDate, - }; - - if (pattern.timeFieldName) { - body.timeFieldName = pattern.timeFieldName; - } - - return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { - body: JSON.stringify(body), - }) as Promise; - }); - - try { - const emptinessInfo = await Promise.all(existenceRequests); - if (isFirstExistenceFetch) { - const fieldsCurrentIndexPattern = emptinessInfo.find( - (info) => info.indexPatternTitle === currentIndexPatternTitle - ); - if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { - showNoDataPopover(); - } - } - - setState( - (state) => ({ - ...state, - isFirstExistenceFetch: false, - existenceFetchFailed: false, - existenceFetchTimeout: false, - existingFields: emptinessInfo.reduce( - (acc, info) => { - acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); - return acc; - }, - { ...state.existingFields } - ), - }), - { applyImmediately: true } - ); - } catch (e) { - // show all fields as available if fetch failed or timed out - setState( - (state) => ({ - ...state, - existenceFetchFailed: e.res?.status !== 408, - existenceFetchTimeout: e.res?.status === 408, - existingFields: indexPatterns.reduce( - (acc, pattern) => { - acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name)); - return acc; - }, - { ...state.existingFields } - ), - }), - { applyImmediately: true } - ); - } -} - -function booleanMap(keys: string[]) { - return keys.reduce((acc, key) => { - acc[key] = true; - return acc; - }, {} as Record); -} - function isSingleEmptyLayer(layerMap: IndexPatternPrivateState['layers']) { const layers = Object.values(layerMap); return layers.length === 1 && layers[0].columnOrder.length === 0; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index c4402277148c73..1ddd6b43aa5418 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -9,7 +9,8 @@ import { createMockedIndexPattern } from '../../../mocks'; import { formulaOperation, GenericOperationDefinition, GenericIndexPatternColumn } from '..'; import { FormulaIndexPatternColumn } from './formula'; import { insertOrReplaceFormulaColumn } from './parse'; -import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; +import { IndexPattern, IndexPatternField } from '../../../../editor_frame_service/types'; import { tinymathFunctions } from './util'; import { TermsIndexPatternColumn } from '../terms'; import { MovingAverageIndexPatternColumn } from '../calculations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 9548d9473e6568..e0c1c4b3b84cce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -14,7 +14,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import type { IndexPatternLayer, IndexPattern } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; import { rangeOperation } from '..'; import { RangeIndexPatternColumn } from './ranges'; import { @@ -25,8 +25,9 @@ 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 '../../../../editor_frame_service/types'; // mocking random id generator function jest.mock('@elastic/eui', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx index 47cc121be095b2..49d913655af453 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx @@ -6,5 +6,4 @@ */ export * from './label_input'; -export * from './buckets'; export * from './form_row'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx index 6381b2843cf2c0..a6b734d373849d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx @@ -15,13 +15,18 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DragDropBuckets, NewBucketButton } from '../shared_components/buckets'; -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 { IndexPattern, IndexPatternPrivateState } from '../../../types'; +import type { IndexPatternPrivateState } from '../../../types'; import type { OperationSupportMatrix } from '../../../dimension_panel'; import { supportedTypes } from './constants'; +import type { IndexPattern } from '../../../../editor_frame_service/types'; const generateId = htmlIdGenerator(); export const MAX_MULTI_FIELDS_SIZE = 3; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 33c8fcd1af665a..2e8eb43b554d81 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -28,12 +28,13 @@ import { LastValueIndexPatternColumn, operationDefinitionMap, } from '..'; -import { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../../../types'; +import { IndexPatternLayer, IndexPatternPrivateState } from '../../../types'; import { FrameDatasourceAPI } from '../../../../types'; import { DateHistogramIndexPatternColumn } from '../date_histogram'; import { getOperationSupportMatrix } from '../../../dimension_panel/operation_support'; import { FieldSelect } from '../../../dimension_panel/field_select'; import { ReferenceEditor } from '../../../dimension_panel/reference_editor'; +import { IndexPattern } from '../../../../editor_frame_service/types'; // mocking random id generator function jest.mock('@elastic/eui', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c89ec6ae021995..cfbc5d4455fd27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -23,7 +23,7 @@ import { operationDefinitionMap, OperationType } from '.'; import { TermsIndexPatternColumn } from './definitions/terms'; import { DateHistogramIndexPatternColumn } from './definitions/date_histogram'; import { AvgIndexPatternColumn } from './definitions/metrics'; -import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; +import type { IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; @@ -38,6 +38,7 @@ import { } from './definitions'; import { TinymathAST } from '@kbn/tinymath'; import { CoreStart } from '@kbn/core/public'; +import { IndexPattern } from '../../editor_frame_service/types'; jest.mock('.'); jest.mock('../../id_generator'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index a1edd6132d22aa..be3dd8702e20d0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -13,6 +13,8 @@ import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/pu import type { DatasourceFixAction, FrameDatasourceAPI, + IndexPattern, + IndexPatternField, OperationMetadata, VisualizationDimensionGroupConfig, } from '../../types'; @@ -27,8 +29,6 @@ import { } from './definitions'; import type { DataViewDragDropOperation, - IndexPattern, - IndexPatternField, IndexPatternLayer, IndexPatternPrivateState, } from '../types'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 396bf78f82db6b..b30c91a7f10844 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -6,7 +6,7 @@ */ import { memoize } from 'lodash'; -import { OperationMetadata } from '../../types'; +import type { IndexPattern, IndexPatternField, OperationMetadata } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -15,7 +15,6 @@ import { renameOperationsMapping, BaseIndexPatternColumn, } from './definitions'; -import { IndexPattern, IndexPatternField } from '../types'; import { documentField } from '../document_field'; import { hasField } from '../pure_utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts index e22fb30b23b272..a4e86c2e4570b4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts @@ -6,15 +6,7 @@ */ import { keyBy } from 'lodash'; -import { IndexPatternField, IndexPatternPrivateState } from './types'; - -export function fieldExists( - existingFields: IndexPatternPrivateState['existingFields'], - indexPatternTitle: string, - fieldName: string -) { - return existingFields[indexPatternTitle] && existingFields[indexPatternTitle][fieldName]; -} +import type { IndexPatternField } from '../editor_frame_service/types'; export function getFieldByNameFactory(newFields: IndexPatternField[]) { const fieldsLookup = keyBy(newFields, 'name'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 2a672d2848c674..7ec2797796db25 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -13,13 +13,12 @@ import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { Datatable } from '@kbn/expressions-plugin'; import { search } from '@kbn/data-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; -import { - IndexPattern, +import type { GenericIndexPatternColumn, IndexPatternLayer, IndexPatternPrivateState, } from './types'; -import { FramePublicAPI } from '../types'; +import type { FramePublicAPI, IndexPattern } from '../types'; export const timeShiftOptions = [ { @@ -187,12 +186,12 @@ export function getDisallowedPreviousShiftMessage( export function getStateTimeShiftWarningMessages( datatableUtilities: DatatableUtilitiesService, state: IndexPatternPrivateState, - { activeData }: FramePublicAPI + { activeData, dataViews }: FramePublicAPI ) { if (!state) return; const warningMessages: React.ReactNode[] = []; Object.entries(state.layers).forEach(([layerId, layer]) => { - const layerIndexPattern = state.indexPatterns[layer.indexPatternId]; + const layerIndexPattern = dataViews.indexPatterns[layer.indexPatternId]; if (!layerIndexPattern) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 8e59f299c69171..a49e5bb43514b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -22,10 +22,11 @@ import { } from '@kbn/expressions-plugin/public'; import { GenericIndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; -import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; +import { IndexPatternPrivateState, IndexPatternLayer } from './types'; import { DateHistogramIndexPatternColumn, RangeIndexPatternColumn } from './operations/definitions'; import { FormattedIndexPatternColumn } from './operations/definitions/column_types'; import { isColumnFormatted, isColumnOfType } from './operations/definitions/helpers'; +import type { IndexPattern, IndexPatternMap } from '../types'; export type OriginalColumn = { id: string } & GenericIndexPatternColumn; @@ -408,12 +409,13 @@ function sortedReferences(columns: Array; - hasRestrictions: boolean; -} - -export type IndexPatternField = FieldSpec & { - displayName: string; - aggregationRestrictions?: Partial; - meta?: boolean; - runtime?: boolean; -}; - export interface IndexPatternLayer { columnOrder: string[]; columns: Record; @@ -92,16 +65,16 @@ export type PersistedIndexPatternLayer = Omit; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; + // indexPatternRefs: IndexPatternRef[]; + // indexPatterns: Record; - /** - * indexPatternId -> fieldName -> boolean - */ - existingFields: Record>; - isFirstExistenceFetch: boolean; - existenceFetchFailed?: boolean; - existenceFetchTimeout?: boolean; + // /** + // * indexPatternId -> fieldName -> boolean + // */ + // existingFields: Record>; + // isFirstExistenceFetch: boolean; + // existenceFetchFailed?: boolean; + // existenceFetchTimeout?: boolean; isDimensionClosePrevented?: boolean; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index fe12328e3177a7..7266bdf1e0a966 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -16,8 +16,8 @@ import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui'; import type { DatatableColumn } from '@kbn/expressions-plugin'; import { groupBy, escape } from 'lodash'; import type { Query } from '@kbn/data-plugin/common'; -import type { FramePublicAPI, StateSetter } from '../types'; -import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from './types'; +import type { FramePublicAPI, IndexPattern, StateSetter } from '../types'; +import type { IndexPatternLayer, IndexPatternPrivateState } from './types'; import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types'; import { @@ -162,7 +162,7 @@ const accuracyModeEnabledWarning = (columnName: string, docLink: string) => ( export function getPrecisionErrorWarningMessages( datatableUtilities: DatatableUtilitiesService, state: IndexPatternPrivateState, - { activeData }: FramePublicAPI, + { activeData, dataViews }: FramePublicAPI, docLinks: DocLinksStart, setState: StateSetter ) { @@ -181,7 +181,7 @@ export function getPrecisionErrorWarningMessages( const currentLayer = state.layers[layerId]; const currentColumn = currentLayer?.columns[column.id]; if (currentLayer && currentColumn && datatableUtilities.hasPrecisionError(column)) { - const indexPattern = state.indexPatterns[currentLayer.indexPatternId]; + const indexPattern = dataViews.indexPatterns[currentLayer.indexPatternId]; // currentColumnIsTerms is mostly a type guard. If there's a precision error, // we already know that we're dealing with a terms-based operation (at least for now). const currentColumnIsTerms = isColumnOfType( diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index a312f338f8504d..3782baca9c077d 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -12,6 +12,7 @@ import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; @@ -275,11 +276,16 @@ export class LensPlugin { data: plugins.data, timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, - documentToExpression: this.editorFrameService!.documentToExpression, + documentToExpression: (doc) => + this.editorFrameService!.documentToExpression(doc, { + dataViews: plugins.dataViews, + storage: new Storage(localStorage), + uiSettings: core.uiSettings, + }), injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), visualizationMap, datasourceMap, - indexPatternService: plugins.dataViews, + dataViews: plugins.dataViews, uiActions: plugins.uiActions, usageCollection, inspector: plugins.inspector, diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index 9ad4f4a154ce4f..395e1c4348bf83 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -11,7 +11,7 @@ import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { IndexPatternRef } from './types'; +import { IndexPatternRef } from '../../types'; export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { label: string; diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts new file mode 100644 index 00000000000000..4f53930fa49732 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IndexPattern } from '../../types'; + +/** + * Checks if the provided field contains data (works for meta field) + */ +export function fieldContainsData( + field: string, + indexPattern: IndexPattern, + existingFields: Record +) { + return ( + indexPattern.getFieldByName(field)?.type === 'document' || fieldExists(existingFields, field) + ); +} + +/** + * Performs an existence check on the existingFields data structure for the provided field. + * Does not work for meta fields. + */ +export function fieldExists(existingFields: Record, fieldName: string) { + return existingFields[fieldName]; +} diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts index 4454d791489231..4de03b2f8b92c3 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts @@ -6,4 +6,4 @@ */ export { ChangeIndexPattern } from './dataview_picker'; -export type { IndexPatternRef } from './types'; +export { fieldExists, fieldContainsData } from './helpers'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx similarity index 97% rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx rename to x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx index 101eca3ab1785e..aba0cbfc40a6e5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx @@ -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'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx rename to x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 936cdc71ccb1bb..a4e6bf046b36d1 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -11,7 +11,12 @@ export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; export { FieldPicker, LensFieldIcon, TruncatedLabel } from './field_picker'; export type { FieldOption, FieldOptionValue } from './field_picker'; -export { ChangeIndexPattern, IndexPatternRef } from './dataview_picker'; +export { ChangeIndexPattern, fieldExists, fieldContainsData } from './dataview_picker'; +export { + NewBucketButton, + DraggableBucketContainer, + DragDropBuckets, +} from './drag_drop_bucket/buckets'; export { RangeInputField } from './range_input_field'; export { BucketAxisBoundsControl, diff --git a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts index 90cd4ba27723eb..7d1e6a0b48fbae 100644 --- a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts @@ -23,11 +23,14 @@ import { onActiveDataChange } from '../lens_slice'; import { DatasourceMap } from '../../types'; function isTimeBased(state: LensState, datasourceMap: DatasourceMap) { - const { activeDatasourceId, datasourceStates } = state.lens; + const { activeDatasourceId, datasourceStates, dataViews } = state.lens; return Boolean( activeDatasourceId && datasourceStates[activeDatasourceId] && - datasourceMap[activeDatasourceId].isTimeBased?.(datasourceStates[activeDatasourceId].state) + datasourceMap[activeDatasourceId].isTimeBased?.( + datasourceStates[activeDatasourceId].state, + dataViews.indexPatterns + ) ); } diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index 7b9c345ff89f63..e41247557aa0af 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -33,6 +33,7 @@ export const { rollbackSuggestion, submitSuggestion, switchDatasource, + updateIndexPatterns, setToggleFullscreen, initEmpty, editVisualizationAction, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 3a9356571c1e30..5f978ca9f3a586 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -12,8 +12,8 @@ import { setState, initEmpty, LensStoreDeps } from '..'; import { disableAutoApply, getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; -import { getInitialDatasourceId } from '../../utils'; -import { initializeDatasources, mergeIndexPatterns } from '../../editor_frame_service/editor_frame'; +import { getInitialDatasourceId, getInitialDataViewsObject } from '../../utils'; +import { initializeSources } from '../../editor_frame_service/editor_frame'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document } from '../../persistence'; @@ -103,24 +103,36 @@ export function loadInitial( const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; const { lens } = store.getState(); + const loaderSharedArgs = { + dataViews: lensServices.dataViews, + storage: lensServices.storage, + defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'), + }; + if ( !initialInput || (attributeService.inputIsRefType(initialInput) && initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { - // @TODO: preload indexpattern refs - // @TODO: collect all used dataviews/indexpatterns from datasources & visualizations - return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { - isFullEditor: true, - }) - .then((result) => { + return initializeSources( + { + datasourceMap, + datasourceStates: lens.datasourceStates, + initialContext, + ...loaderSharedArgs, + }, + { + isFullEditor: true, + } + ) + .then(({ states, indexPatterns, indexPatternRefs }) => { store.dispatch( initEmpty({ newState: { ...emptyState, - ...mergeIndexPatterns(datasourceMap, result), + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), searchSessionId: data.search.session.getSessionId() || data.search.session.start(), - datasourceStates: Object.entries(result).reduce( + datasourceStates: Object.entries(states).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, [datasourceId]: { @@ -145,6 +157,40 @@ export function loadInitial( }); redirectCallback(); }); + // return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { + // isFullEditor: true, + // }) + // .then((result) => { + // store.dispatch( + // initEmpty({ + // newState: { + // ...emptyState, + // searchSessionId: data.search.session.getSessionId() || data.search.session.start(), + // datasourceStates: Object.entries(result).reduce( + // (state, [datasourceId, datasourceState]) => ({ + // ...state, + // [datasourceId]: { + // ...datasourceState, + // isLoading: false, + // }, + // }), + // {} + // ), + // isLoading: false, + // }, + // initialContext, + // }) + // ); + // if (autoApplyDisabled) { + // store.dispatch(disableAutoApply()); + // } + // }) + // .catch((e: { message: string }) => { + // notifications.toasts.addDanger({ + // title: e.message, + // }); + // redirectCallback(); + // }); } getPersisted({ initialInput, lensServices, history }) @@ -175,16 +221,28 @@ export function loadInitial( // Don't overwrite any pinned filters data.query.filterManager.setAppFilters(filters); - initializeDatasources( - datasourceMap, - docDatasourceStates, - doc.references, - initialContext, + // initializeDatasources( + // datasourceMap, + // docDatasourceStates, + // doc.references, + // initialContext, + // { + // isFullEditor: true, + // } + // ) + initializeSources( { - isFullEditor: true, - } + datasourceMap, + datasourceStates: docDatasourceStates, + references: doc.references, + initialContext, + dataViews: lensServices.dataViews, + storage: lensServices.storage, + defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'), + }, + { isFullEditor: true } ) - .then((result) => { + .then(({ states, indexPatterns, indexPatternRefs }) => { const currentSessionId = data.search.session.getSessionId(); store.dispatch( setState({ @@ -205,7 +263,8 @@ export function loadInitial( activeId: doc.visualizationType, state: doc.state.visualization, }, - datasourceStates: Object.entries(result).reduce( + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + datasourceStates: Object.entries(states).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, [datasourceId]: { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index bdb03523a72e79..4103d665f58ec3 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -14,7 +14,7 @@ import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import type { VisualizeEditorContext, Suggestion } from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; -import { LensAppState, LensStoreDeps, VisualizationState } from './types'; +import { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; import { Datasource, Visualization } from '../types'; import { generateId } from '../id_generator'; import type { LayerType } from '../../common/types'; @@ -38,8 +38,12 @@ export const initialState: LensAppState = { state: null, activeId: null, }, - indexPatternRefs: [], - indexPatterns: {}, + dataViews: { + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + isFirstExistenceFetch: false, + }, }; export const getPreloadedState = ({ @@ -164,6 +168,10 @@ export const setLayerDefaultDimension = createAction<{ groupId: string; }>('lens/setLayerDefaultDimension'); +export const updateIndexPatterns = createAction>( + 'lens/updateIndexPatterns' +); + export const lensActions = { setState, onActiveDataChange, @@ -189,6 +197,7 @@ export const lensActions = { removeOrClearLayer, addLayer, setLayerDefaultDimension, + updateIndexPatterns, }; export const makeLensReducer = (storeDeps: LensStoreDeps) => { @@ -283,6 +292,12 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ? activeVisualization.clearLayer(state.visualization.state, layerId) : activeVisualization.removeLayer(state.visualization.state, layerId); }, + [updateIndexPatterns.type]: (state, { payload }: { payload: Partial }) => { + return { + ...state, + dataViews: { ...state.dataViews, ...payload }, + }; + }, [updateDatasourceState.type]: ( state, { @@ -450,6 +465,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { datasourceStates: newState.datasourceStates, visualizationMap, visualizeTriggerFieldContext: payload.initialContext, + dataViews: newState.dataViews, }); if (suggestion) { return { @@ -636,8 +652,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), - indexPatternRefs: current(state.indexPatternRefs), - indexPatterns: current(state.indexPatterns), + dataViews: current(state.dataViews), }; const activeDatasource = datasourceMap[state.activeDatasourceId]; @@ -697,8 +712,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), - indexPatternRefs: current(state.indexPatternRefs), - indexPatterns: current(state.indexPatterns), + dataViews: current(state.dataViews), }, activeVisualization, activeDatasource, @@ -756,10 +770,15 @@ function addInitialValueIfAvailable({ if (!noDatasource && activeDatasource?.initializeDimension) { return { - activeDatasourceState: activeDatasource.initializeDimension(datasourceState, layerId, { - ...info, - columnId: columnId || info.columnId, - }), + activeDatasourceState: activeDatasource.initializeDimension( + datasourceState, + layerId, + framePublicAPI.dataViews.indexPatterns, + { + ...info, + columnId: columnId || info.columnId, + } + ), activeVisualizationState, }; } else { diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index e4334270b6c59b..a7e7a55e395925 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -27,8 +27,7 @@ export const selectChangesApplied = (state: LensState) => export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; export const selectActiveData = (state: LensState) => state.lens.activeData; -export const selectIndexPatternRefs = (state: LensState) => state.lens.indexPatternRefs; -export const selectIndexPatterns = (state: LensState) => state.lens.indexPatterns; +export const selectDataViews = (state: LensState) => state.lens.dataViews; export const selectIsFullscreenDatasource = (state: LensState) => Boolean(state.lens.isFullscreenDatasource); @@ -167,16 +166,14 @@ export const selectFramePublicAPI = createSelector( selectActiveData, selectInjectedDependencies as SelectInjectedDependenciesFunction, selectResolvedDateRange, - selectIndexPatternRefs, - selectIndexPatterns, + selectDataViews, ], - (datasourceStates, activeData, datasourceMap, dateRange, indexPatternRefs, indexPatterns) => { + (datasourceStates, activeData, datasourceMap, dateRange, dataViews) => { return { datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), activeData, dateRange, - indexPatternRefs, - indexPatterns, + dataViews, }; } ); diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 53d53ff29649bf..8227ce438ae8ee 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -11,7 +11,7 @@ import { Filter, Query } from '@kbn/es-query'; import { SavedQuery } from '@kbn/data-plugin/public'; import { Document } from '../persistence'; -import { TableInspectorAdapter } from '../editor_frame_service/types'; +import type { TableInspectorAdapter } from '../editor_frame_service/types'; import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; import { @@ -19,15 +19,25 @@ import { VisualizationMap, SharingSavedObjectProps, VisualizeEditorContext, + IndexPattern, } from '../types'; import type { IndexPatternRef } from '../shared_components'; -import type { IndexPattern } from '../indexpattern_datasource'; +// import type { IndexPattern } from '../indexpattern_datasource'; export interface VisualizationState { activeId: string | null; state: unknown; } -export type DatasourceStates = Record; +export interface DataViewsState { + indexPatternRefs: IndexPatternRef[]; + indexPatterns: Record; + existingFields: Record>; + isFirstExistenceFetch: boolean; + existenceFetchFailed?: boolean; + existenceFetchTimeout?: boolean; +} + +export type DatasourceStates = Record; export interface PreviewState { visualization: VisualizationState; datasourceStates: DatasourceStates; @@ -55,8 +65,8 @@ export interface LensAppState extends EditorFrameState { searchSessionId: string; resolvedDateRange: DateRange; sharingSavedObjectProps?: Omit; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; + // Dataview/Indexpattern management has moved in here from datasource + dataViews: DataViewsState; } export type DispatchSetState = (state: Partial) => { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 3aa5292cba4547..7174c1dcbd326d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Ast } from '@kbn/interpreter'; +import type { Ast } from '@kbn/interpreter'; import type { IconType } from '@elastic/eui/src/components/icon/icon'; import type { CoreSetup, @@ -14,7 +14,7 @@ import type { import type { PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject } from 'react'; -import { Filter, TimeRange } from '@kbn/es-query'; +import type { Filter, TimeRange } from '@kbn/es-query'; import type { ExpressionAstExpression, ExpressionRendererEvent, @@ -28,8 +28,11 @@ import type { RowClickContext, VisualizeFieldContext, } from '@kbn/ui-actions-plugin/public'; -import { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; +import type { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public'; +import type { IndexPatternAggRestrictions } from '@kbn/data-plugin/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; +import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; +import type { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; import type { DateRange, LayerType, SortingHint } from '../common'; import type { LensSortActionData, @@ -45,23 +48,57 @@ import { LENS_EDIT_PAGESIZE_ACTION, } from './datatable_visualization/components/constants'; import type { LensInspector } from './lens_inspector_service'; -import type { IndexPattern } from './indexpattern_datasource'; -import type { IndexPatternRef } from './shared_components'; +import { DataViewsState } from './state_management/types'; +import { IndexPatternServiceAPI } from './data_views_service/service'; + +export interface IndexPatternRef { + id: string; + title: string; + name?: string; +} + +export interface IndexPattern { + id: string; + fields: IndexPatternField[]; + getFieldByName(name: string): IndexPatternField | undefined; + title: string; + name?: string; + timeFieldName?: string; + fieldFormatMap?: Record< + string, + { + id: string; + params: FieldFormatParams; + } + >; + hasRestrictions: boolean; +} + +export type IndexPatternField = FieldSpec & { + displayName: string; + aggregationRestrictions?: Partial; + meta?: boolean; + runtime?: boolean; +}; export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; layerId: string; + indexPatterns: IndexPatternMap; } export interface EditorFrameProps { showNoDataPopover: () => void; lensInspector: LensInspector; + indexPatternService: IndexPatternServiceAPI; } export type VisualizationMap = Record; export type DatasourceMap = Record; +export type IndexPatternMap = Record; +export type ExistingFieldsMap = Record>; export interface EditorFrameInstance { EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement; @@ -212,6 +249,7 @@ export interface GetDropPropsArgs { prioritizedOperation?: string; isNewColumn?: boolean; }; + indexPatterns: IndexPatternMap; } /** @@ -227,13 +265,10 @@ export interface Datasource { state?: P, savedObjectReferences?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, + indexPatternRefs?: IndexPatternRef[], + indexPatterns?: IndexPatternMap, options?: InitializationOptions - ) => Promise; - - getIndexPatterns: (state?: T) => { - indexPatterns: Record; - indexPatternRefs: IndexPatternRef[]; - }; + ) => T; // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; @@ -243,10 +278,16 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; - removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; + removeColumn: (props: { + prevState: T; + layerId: string; + columnId: string; + indexPatterns: IndexPatternMap; + }) => T; initializeDimension?: ( state: T, layerId: string, + indexPatterns: IndexPatternMap, value: { columnId: string; groupId: string; @@ -295,32 +336,44 @@ export interface Datasource { setState: StateSetter; }) => void; - refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; + // refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; + onRefreshIndexPattern: () => void; - toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; + toExpression: ( + state: T, + layerId: string, + indexPatterns: IndexPatternMap + ) => ExpressionAstExpression | string | null; getDatasourceSuggestionsForField: ( state: T, field: unknown, - filterFn: (layerId: string) => boolean + filterFn: (layerId: string) => boolean, + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsForVisualizeCharts: ( state: T, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsForVisualizeField: ( state: T, indexPatternId: string, - fieldName: string + fieldName: string, + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, + indexPatterns: IndexPatternMap, filterFn?: (layerId: string) => boolean, activeData?: Record ) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; - getErrorMessages: (state: T) => + getErrorMessages: ( + state: T, + indexPatterns: Record + ) => | Array<{ shortMessage: string; longMessage: React.ReactNode; @@ -334,7 +387,7 @@ export interface Datasource { /** * Check the internal state integrity and returns a list of missing references */ - checkIntegrity: (state: T) => string[]; + checkIntegrity: (state: T, indexPatterns: IndexPatternMap) => string[]; /** * The frame calls this function to display warnings about visualization */ @@ -346,11 +399,16 @@ export interface Datasource { /** * Checks if the visualization created is time based, for example date histogram */ - isTimeBased: (state: T) => boolean; + isTimeBased: (state: T, indexPatterns: IndexPatternMap) => boolean; /** * Given the current state layer and a columnId will verify if the column configuration has errors */ - isValidColumn: (state: T, layerId: string, columnId: string) => boolean; + isValidColumn: ( + state: T, + indexPatterns: IndexPatternMap, + layerId: string, + columnId: string + ) => boolean; /** * Are these datasources equivalent? */ @@ -411,6 +469,8 @@ export interface DatasourceDataPanelProps { dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; uiActions: UiActionsStart; + indexPatternService: IndexPatternServiceAPI; + frame: FramePublicAPI; } interface SharedDimensionProps { @@ -433,6 +493,8 @@ export type DatasourceDimensionProps = SharedDimensionProps & { onRemove?: (accessor: string) => void; state: T; activeData?: Record; + indexPatterns: IndexPatternMap; + existingFields: Record>; hideTooltip?: boolean; invalid?: boolean; invalidMessage?: string; @@ -469,6 +531,8 @@ export interface DatasourceLayerPanelProps { state: T; setState: StateSetter; activeData?: Record; + dataViews: DataViewsState; + indexPatternService: IndexPatternServiceAPI; } export interface DragDropOperation { @@ -502,6 +566,7 @@ export interface DatasourceDimensionDropProps { export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { source: DragDropIdentifier; dropType: DropType; + indexPatterns: IndexPatternMap; }; export type FieldOnlyDataType = @@ -726,8 +791,7 @@ export interface FramePublicAPI { * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; + dataViews: DataViewsState; } export interface FrameDatasourceAPI extends FramePublicAPI { query: Query; @@ -781,6 +845,15 @@ export interface Visualization { */ initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T; + /** + * Retrieve the used indexpatterns in the visualization + */ + getUsedIndexPatterns?: ( + state?: T, + indexPatternRefs?: IndexPatternRef[], + savedObjectReferences?: SavedObjectReference[] + ) => { usedPatterns: string[] }; + getMainPalette?: (state: T) => undefined | PaletteOutput; /** @@ -974,11 +1047,6 @@ export interface Visualization { * On Edit events the frame will call this to know what's going to be the next visualization state */ onEditAction?: (state: T, event: LensEditEvent) => T; - - getInnerDatasource?: ( - state: T, - frame: FramePublicAPI - ) => { datasource: Datasource; state: unknown }; } // Use same technique as TriggerContext diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 4b0f24ce058356..23ec16797fe498 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -14,8 +14,16 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public' import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public'; import type { Document } from './persistence/saved_object_store'; -import type { Datasource, DatasourceMap, Visualization, StateSetter } from './types'; +import type { + Datasource, + DatasourceMap, + Visualization, + StateSetter, + IndexPatternMap, + IndexPatternRef, +} from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; +import { IndexPatternServiceAPI } from './data_views_service/service'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { @@ -58,6 +66,18 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; +export function getInitialDataViewsObject( + indexPatterns: IndexPatternMap, + indexPatternRefs: IndexPatternRef[] +) { + return { + indexPatterns, + indexPatternRefs, + existingFields: {}, + isFirstExistenceFetch: true, + }; +} + export function handleIndexPatternChange({ activeDatasources, datasourceStates, @@ -78,23 +98,57 @@ export function handleIndexPatternChange({ }); } -export function refreshIndexPatternsList({ +export async function refreshIndexPatternsList({ activeDatasources, + indexPatternService, indexPatternId, - setDatasourceState, + indexPatternsCache, }: { + indexPatternService: IndexPatternServiceAPI; activeDatasources: Record; indexPatternId: string; - setDatasourceState: StateSetter; -}): void { - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasource?.refreshIndexPatternsList?.({ - indexPatternId, - setState: setDatasourceState, - }); + indexPatternsCache: IndexPatternMap; +}) { + // collect all the onRefreshIndex callbacks from datasources + const onRefreshCallbacks = Object.values(activeDatasources) + .map((datasource) => datasource?.onRefreshIndexPattern) + .filter(Boolean); + + const [newlyMappedIndexPattern, indexPatternRefs] = await Promise.all([ + indexPatternService.loadIndexPatterns({ + cache: {}, + patterns: [indexPatternId], + onIndexPatternRefresh: () => onRefreshCallbacks.forEach((fn) => fn()), + }), + indexPatternService.loadIndexPatternRefs({ isFullEditor: true }), + ]); + const indexPattern = newlyMappedIndexPattern[indexPatternId]; + indexPatternService.updateIndexPatternsCache({ + indexPatterns: { + ...indexPatternsCache, + [indexPatternId]: indexPattern, + }, + indexPatternRefs, }); } +// export function refreshIndexPatternsList({ +// activeDatasources, +// indexPatternId, +// setDatasourceState, +// }: { +// activeDatasources: Record; +// indexPatternId: string; +// setDatasourceState: StateSetter; +// }): void { +// Object.entries(activeDatasources).forEach(([id, datasource]) => { +// datasource?.refreshIndexPatternsList?.({ +// indexPatternId, +// setState: setDatasourceState, +// }); +// }); +// } + export function getIndexPatternsIds({ activeDatasources, datasourceStates, @@ -121,9 +175,9 @@ export function getIndexPatternsIds({ export async function getIndexPatternsObjects( ids: string[], - indexPatternsService: DataViewsContract + dataViews: DataViewsContract ): Promise<{ indexPatterns: DataView[]; rejectedIds: string[] }> { - const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); + const responses = await Promise.allSettled(ids.map((id) => dataViews.get(id))); const fullfilled = responses.filter( (response): response is PromiseFulfilledResult => response.status === 'fulfilled' ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 29fa2b8875ba1c..ead1152e3bfdea 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -8,146 +8,41 @@ import './index.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiDatePicker, - EuiFormRow, - EuiSwitch, - EuiSwitchEvent, - EuiButtonGroup, - EuiFormLabel, - EuiFormControlLayout, - EuiText, - transparentize, - EuiSpacer, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent, EuiButtonGroup, EuiSpacer } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import moment from 'moment'; -import { - EventAnnotationConfig, - PointInTimeEventAnnotationConfig, - PointInTimeQueryEventAnnotationConfig, - RangeEventAnnotationConfig, -} from '@kbn/event-annotation-plugin/common/types'; -import { pick } from 'lodash'; -import { search } from '@kbn/data-plugin/public'; -import type { DatatableUtilitiesService, Query } from '@kbn/data-plugin/common'; +import type { PointInTimeQueryEventAnnotationConfig } from '@kbn/event-annotation-plugin/common/types'; +import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { defaultAnnotationColor, defaultAnnotationRangeColor, isRangeAnnotation, isQueryAnnotation, } from '@kbn/event-annotation-plugin/public'; -import Color from 'color'; import { FieldOption, FieldOptionValue, FieldPicker, } from '../../../shared_components/field_picker'; -import { getDataLayers } from '../../visualization_helpers'; -import { FormatFactory } from '../../../../common'; -import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; +import type { FormatFactory } from '../../../../common'; +import { + DimensionEditorSection, + fieldExists, + NameInput, + useDebouncedValue, +} from '../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; -import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers'; +import { defaultAnnotationLabel } from '../../annotations/helpers'; import { ColorPicker } from '../color_picker'; import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings'; import { LineStyleSettings } from '../shared/line_style_settings'; import { updateLayer } from '..'; import { annotationsIconSet } from './icon_set'; -import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types'; -import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types'; -import { QueryInput } from '../../../indexpattern_datasource/query_input'; - -type ManualEventAnnotationType = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig; - -export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { - return new Color(transparentize(color, 0.1)).hexa(); -}; - -export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { - return new Color(transparentize(color, 1)).hex(); -}; - -export const getEndTimestamp = ( - datatableUtilities: DatatableUtilitiesService, - startTime: string, - { activeData, dateRange }: FramePublicAPI, - dataLayers: XYDataLayerConfig[] -) => { - const startTimeNumber = moment(startTime).valueOf(); - const dateRangeFraction = - (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; - const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); - const dataLayersId = dataLayers.map(({ layerId }) => layerId); - if ( - !dataLayersId.length || - !activeData || - Object.entries(activeData) - .filter(([key]) => dataLayersId.includes(key)) - .every(([, { rows }]) => !rows || !rows.length) - ) { - return fallbackValue; - } - const xColumn = activeData?.[dataLayersId[0]].columns.find( - (column) => column.id === dataLayers[0].xAccessor - ); - if (!xColumn) { - return fallbackValue; - } - - const dateInterval = datatableUtilities.getDateHistogramMeta(xColumn)?.interval; - if (!dateInterval) return fallbackValue; - const intervalDuration = search.aggs.parseInterval(dateInterval); - if (!intervalDuration) return fallbackValue; - return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); -}; - -const sanitizeProperties = (annotation: EventAnnotationConfig) => { - if (isRangeAnnotation(annotation)) { - const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ - 'type', - 'label', - 'key', - 'id', - 'isHidden', - 'color', - 'outside', - ]); - return rangeAnnotation; - } - if (isQueryAnnotation(annotation)) { - const lineAnnotation: PointInTimeQueryEventAnnotationConfig = pick(annotation, [ - 'type', - 'id', - 'label', - 'key', - 'isHidden', - 'lineStyle', - 'lineWidth', - 'color', - 'icon', - 'textVisibility', - 'textSource', - 'textField', - 'query', - 'additionalFields', - ]); - return lineAnnotation; - } - const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ - 'type', - 'id', - 'label', - 'key', - 'isHidden', - 'lineStyle', - 'lineWidth', - 'color', - 'icon', - 'textVisibility', - ]); - return lineAnnotation; -}; +import type { VisualizationDimensionEditorProps } from '../../../types'; +import type { State, XYState, XYAnnotationLayerConfig } from '../../types'; +import { ConfigPanelManualAnnotation } from './manual_annotation_panel'; +import { ConfigPanelQueryAnnotation } from './query_annotation_panel'; +import { TooltipSection } from './tooltip_annotation_panel'; +import { sanitizeProperties } from './helpers'; export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps & { @@ -297,10 +192,10 @@ export const AnnotationsPanel = ( if (textDecorationSelected !== 'field') { return null; } - const currentIndexPattern = frame.indexPatterns[localLayer.indexPatternId]; - // @TODO: refactor this to group fields by type + const currentIndexPattern = + frame.dataViews.indexPatterns[localLayer.indexPatternId]; const options = currentIndexPattern.fields - .filter(({ displayName }) => displayName) + .filter(({ displayName, type }) => displayName && type !== 'document') .map( (field) => ({ @@ -310,10 +205,12 @@ export const AnnotationsPanel = ( field: field.name, dataType: field.type, }, - // @TODO: add the existing check here - exists: true, + exists: fieldExists( + frame.dataViews.existingFields[currentIndexPattern.title], + field.name + ), compatible: true, - 'data-test-subj': `lns-fieldOption-${field.name}`, + 'data-test-subj': `lnsXY-annotation-fieldOption-${field.name}`, } as FieldOption) ); const selectedField = (currentAnnotation as PointInTimeQueryEventAnnotationConfig) @@ -411,7 +308,7 @@ export const AnnotationsPanel = ( onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })} /> - {isQueryBased && ( + {isQueryBased && currentAnnotation && ( - {}} - isDisabled={false} - > - {i18n.translate('xpack.lens.xyChart.annotation.addField', { - defaultMessage: 'Add field', - })} - + )} @@ -443,341 +335,6 @@ export const AnnotationsPanel = ( ); }; -export const defaultQuery: Query = { - query: '', - language: 'kuery', -}; - -const ConfigPanelQueryAnnotation = ({ - annotation, - frame, - state, - onChange, - layer, -}: { - annotation?: PointInTimeQueryEventAnnotationConfig; - onChange: (annotations: Partial | undefined) => void; - frame: FramePublicAPI; - state: XYState; - layer: XYAnnotationLayerConfig; -}) => { - const inputQuery = annotation?.query ?? defaultQuery; - const currentIndexPattern = frame.indexPatterns[layer.indexPatternId]; - // list only supported field by operation, remove the rest - const options = currentIndexPattern.fields - .filter((field) => field.type === 'date' && field.displayName) - .map((field) => { - return { - label: field.displayName, - value: { - type: 'field', - field: field.name, - dataType: field.type, - }, - // @TODO: add the existing check here - exists: true, - compatible: true, - 'data-test-subj': `lns-fieldOption-${field.name}`, - } as FieldOption; - }); - - const selectedField = annotation?.key.field; - return ( - <> - - - - - {}} - placeholder={ - inputQuery.language === 'kuery' - ? i18n.translate('xpack.lens.annotations.query.queryPlaceholderKql', { - defaultMessage: '{example}', - values: { example: 'method : "GET"' }, - }) - : i18n.translate('xpack.lens.annotations.query.queryPlaceholderLucene', { - defaultMessage: '{example}', - values: { example: 'method:GET' }, - }) - } - /> - - - ); -}; - -const ConfigPanelManualAnnotation = ({ - annotation, - frame, - state, - onChange, - datatableUtilities, -}: { - annotation?: ManualEventAnnotationType | undefined; - onChange: (annotations: Partial | undefined) => void; - datatableUtilities: DatatableUtilitiesService; - frame: FramePublicAPI; - state: XYState; -}) => { - const isRange = isRangeAnnotation(annotation); - return ( - <> - {isRange ? ( - <> - { - if (date) { - const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); - if (currentEndTime < date.valueOf()) { - const currentStartTime = moment(annotation?.key.timestamp).valueOf(); - const dif = currentEndTime - currentStartTime; - onChange({ - key: { - ...(annotation?.key || { type: 'range' }), - timestamp: date.toISOString(), - endTimestamp: moment(date.valueOf() + dif).toISOString(), - }, - }); - } else { - onChange({ - key: { - ...(annotation?.key || { type: 'range' }), - timestamp: date.toISOString(), - }, - }); - } - } - }} - label={i18n.translate('xpack.lens.xyChart.annotationDate', { - defaultMessage: 'Annotation date', - })} - /> - { - if (date) { - const currentStartTime = moment(annotation?.key.timestamp).valueOf(); - if (currentStartTime > date.valueOf()) { - const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); - const dif = currentEndTime - currentStartTime; - onChange({ - key: { - ...(annotation?.key || { type: 'range' }), - endTimestamp: date.toISOString(), - timestamp: moment(date.valueOf() - dif).toISOString(), - }, - }); - } else { - onChange({ - key: { - ...(annotation?.key || { type: 'range' }), - endTimestamp: date.toISOString(), - }, - }); - } - } - }} - /> - - ) : ( - { - if (date) { - onChange({ - key: { - ...(annotation?.key || { type: 'point_in_time' }), - timestamp: date.toISOString(), - }, - }); - } - }} - /> - )} - - - ); -}; - -const ConfigPanelApplyAsRangeSwitch = ({ - annotation, - datatableUtilities, - onChange, - frame, - state, -}: { - annotation?: ManualEventAnnotationType; - datatableUtilities: DatatableUtilitiesService; - onChange: (annotations: Partial | undefined) => void; - frame: FramePublicAPI; - state: XYState; -}) => { - const isRange = isRangeAnnotation(annotation); - return ( - - - {i18n.translate('xpack.lens.xyChart.applyAsRange', { - defaultMessage: 'Apply as range', - })} - - } - checked={isRange} - onChange={() => { - if (isRange) { - const newPointAnnotation: PointInTimeEventAnnotationConfig = { - type: 'manual', - key: { - type: 'point_in_time', - timestamp: annotation.key.timestamp, - }, - id: annotation.id, - label: - annotation.label === defaultRangeAnnotationLabel - ? defaultAnnotationLabel - : annotation.label, - color: toLineAnnotationColor(annotation.color), - isHidden: annotation.isHidden, - }; - onChange(newPointAnnotation); - } else if (annotation) { - const fromTimestamp = moment(annotation?.key.timestamp); - const dataLayers = getDataLayers(state.layers); - const newRangeAnnotation: RangeEventAnnotationConfig = { - type: 'manual', - key: { - type: 'range', - timestamp: annotation.key.timestamp, - endTimestamp: getEndTimestamp( - datatableUtilities, - fromTimestamp.toISOString(), - frame, - dataLayers - ), - }, - id: annotation.id, - label: - annotation.label === defaultAnnotationLabel - ? defaultRangeAnnotationLabel - : annotation.label, - color: toRangeAnnotationColor(annotation.color), - isHidden: annotation.isHidden, - }; - onChange(newRangeAnnotation); - } - }} - compressed - /> - - ); -}; - -const ConfigPanelRangeDatePicker = ({ - value, - label, - prependLabel, - onChange, - dataTestSubj = 'lnsXY_annotation_date_picker', -}: { - value: moment.Moment; - prependLabel?: string; - label?: string; - onChange: (val: moment.Moment | null) => void; - dataTestSubj?: string; -}) => { - return ( - - {prependLabel ? ( - {prependLabel} - } - > - - - ) : ( - - )} - - ); -}; - const ConfigPanelHideSwitch = ({ value, onChange, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/helpers.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/helpers.ts new file mode 100644 index 00000000000000..791686b9e744f3 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/helpers.ts @@ -0,0 +1,116 @@ +/* + * 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 { search } from '@kbn/data-plugin/public'; +import { transparentize } from '@elastic/eui'; +import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; +import type { + EventAnnotationConfig, + RangeEventAnnotationConfig, + PointInTimeQueryEventAnnotationConfig, + PointInTimeEventAnnotationConfig, +} from '@kbn/event-annotation-plugin/common'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, + isQueryAnnotation, +} from '@kbn/event-annotation-plugin/public'; +import Color from 'color'; +import { pick } from 'lodash'; +import moment from 'moment'; +import type { FramePublicAPI } from '../../../types'; +import type { XYDataLayerConfig } from '../../types'; + +export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { + return new Color(transparentize(color, 0.1)).hexa(); +}; + +export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { + return new Color(transparentize(color, 1)).hex(); +}; + +export const getEndTimestamp = ( + datatableUtilities: DatatableUtilitiesService, + startTime: string, + { activeData, dateRange }: FramePublicAPI, + dataLayers: XYDataLayerConfig[] +) => { + const startTimeNumber = moment(startTime).valueOf(); + const dateRangeFraction = + (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; + const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); + const dataLayersId = dataLayers.map(({ layerId }) => layerId); + if ( + !dataLayersId.length || + !activeData || + Object.entries(activeData) + .filter(([key]) => dataLayersId.includes(key)) + .every(([, { rows }]) => !rows || !rows.length) + ) { + return fallbackValue; + } + const xColumn = activeData?.[dataLayersId[0]].columns.find( + (column) => column.id === dataLayers[0].xAccessor + ); + if (!xColumn) { + return fallbackValue; + } + + const dateInterval = datatableUtilities.getDateHistogramMeta(xColumn)?.interval; + if (!dateInterval) return fallbackValue; + const intervalDuration = search.aggs.parseInterval(dateInterval); + if (!intervalDuration) return fallbackValue; + return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); +}; + +export const sanitizeProperties = (annotation: EventAnnotationConfig) => { + if (isRangeAnnotation(annotation)) { + const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ + 'type', + 'label', + 'key', + 'id', + 'isHidden', + 'color', + 'outside', + ]); + return rangeAnnotation; + } + if (isQueryAnnotation(annotation)) { + const lineAnnotation: PointInTimeQueryEventAnnotationConfig = pick(annotation, [ + 'type', + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + 'textSource', + 'textField', + 'query', + 'additionalFields', + ]); + return lineAnnotation; + } + const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + 'type', + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + ]); + return lineAnnotation; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx new file mode 100644 index 00000000000000..809c6cb49fbe50 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx @@ -0,0 +1,131 @@ +/* + * 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 { DatatableUtilitiesService } from '@kbn/data-plugin/common'; +import { isRangeAnnotation } from '@kbn/event-annotation-plugin/public'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React from 'react'; +import type { FramePublicAPI } from '../../../types'; +import type { XYState } from '../../types'; +import { + ConfigPanelRangeDatePicker, + ConfigPanelApplyAsRangeSwitch, +} from './range_annotation_panel'; +import type { ManualEventAnnotationType } from './types'; + +export const ConfigPanelManualAnnotation = ({ + annotation, + frame, + state, + onChange, + datatableUtilities, +}: { + annotation?: ManualEventAnnotationType | undefined; + onChange: (annotations: Partial | undefined) => void; + datatableUtilities: DatatableUtilitiesService; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + <> + {isRange ? ( + <> + { + if (date) { + const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); + if (currentEndTime < date.valueOf()) { + const currentStartTime = moment(annotation?.key.timestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + endTimestamp: moment(date.valueOf() + dif).toISOString(), + }, + }); + } else { + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + }, + }); + } + } + }} + label={i18n.translate('xpack.lens.xyChart.annotationDate', { + defaultMessage: 'Annotation date', + })} + /> + { + if (date) { + const currentStartTime = moment(annotation?.key.timestamp).valueOf(); + if (currentStartTime > date.valueOf()) { + const currentEndTime = moment(annotation?.key.endTimestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + timestamp: moment(date.valueOf() - dif).toISOString(), + }, + }); + } else { + onChange({ + key: { + ...(annotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + }, + }); + } + } + }} + /> + + ) : ( + { + if (date) { + onChange({ + key: { + ...(annotation?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + /> + )} + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx new file mode 100644 index 00000000000000..ae2bad90c85381 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import type { Query } from '@kbn/data-plugin/common'; +import type { PointInTimeQueryEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { QueryInput } from '../../../indexpattern_datasource/query_input'; +import { + fieldExists, + FieldOption, + FieldOptionValue, + FieldPicker, +} from '../../../shared_components'; +import type { FramePublicAPI } from '../../../types'; +import type { XYState, XYAnnotationLayerConfig } from '../../types'; + +export const defaultQuery: Query = { + query: '', + language: 'kuery', +}; + +export const ConfigPanelQueryAnnotation = ({ + annotation, + frame, + state, + onChange, + layer, +}: { + annotation?: PointInTimeQueryEventAnnotationConfig; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; + layer: XYAnnotationLayerConfig; +}) => { + const inputQuery = annotation?.query ?? defaultQuery; + const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; + // list only supported field by operation, remove the rest + const options = currentIndexPattern.fields + .filter((field) => field.type === 'date' && field.displayName) + .map((field) => { + return { + label: field.displayName, + value: { + type: 'field', + field: field.name, + dataType: field.type, + }, + exists: fieldExists(frame.dataViews.existingFields[currentIndexPattern.title], field.name), + compatible: true, + 'data-test-subj': `lns-fieldOption-${field.name}`, + } as FieldOption; + }); + + const selectedField = annotation?.key.field; + return ( + <> + + + + + {}} + placeholder={ + inputQuery.language === 'kuery' + ? i18n.translate('xpack.lens.annotations.query.queryPlaceholderKql', { + defaultMessage: '{example}', + values: { example: 'method : "GET"' }, + }) + : i18n.translate('xpack.lens.annotations.query.queryPlaceholderLucene', { + defaultMessage: '{example}', + values: { example: 'method:GET' }, + }) + } + /> + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx new file mode 100644 index 00000000000000..9cbae77f4d6384 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx @@ -0,0 +1,151 @@ +/* + * 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 { DatatableUtilitiesService } from '@kbn/data-plugin/common'; +import type { + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-plugin/common'; +import { isRangeAnnotation } from '@kbn/event-annotation-plugin/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + EuiFormRow, + EuiSwitch, + EuiText, + EuiFormControlLayout, + EuiFormLabel, + EuiDatePicker, +} from '@elastic/eui'; +import moment from 'moment'; +import type { FramePublicAPI } from '../../../types'; +import { defaultRangeAnnotationLabel, defaultAnnotationLabel } from '../../annotations/helpers'; +import type { XYState } from '../../types'; +import { getDataLayers } from '../../visualization_helpers'; +import { toLineAnnotationColor, getEndTimestamp, toRangeAnnotationColor } from './helpers'; +import type { ManualEventAnnotationType } from './types'; + +export const ConfigPanelApplyAsRangeSwitch = ({ + annotation, + datatableUtilities, + onChange, + frame, + state, +}: { + annotation?: ManualEventAnnotationType; + datatableUtilities: DatatableUtilitiesService; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + + + {i18n.translate('xpack.lens.xyChart.applyAsRange', { + defaultMessage: 'Apply as range', + })} + + } + checked={isRange} + onChange={() => { + if (isRange) { + const newPointAnnotation: PointInTimeEventAnnotationConfig = { + type: 'manual', + key: { + type: 'point_in_time', + timestamp: annotation.key.timestamp, + }, + id: annotation.id, + label: + annotation.label === defaultRangeAnnotationLabel + ? defaultAnnotationLabel + : annotation.label, + color: toLineAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newPointAnnotation); + } else if (annotation) { + const fromTimestamp = moment(annotation?.key.timestamp); + const dataLayers = getDataLayers(state.layers); + const newRangeAnnotation: RangeEventAnnotationConfig = { + type: 'manual', + key: { + type: 'range', + timestamp: annotation.key.timestamp, + endTimestamp: getEndTimestamp( + datatableUtilities, + fromTimestamp.toISOString(), + frame, + dataLayers + ), + }, + id: annotation.id, + label: + annotation.label === defaultAnnotationLabel + ? defaultRangeAnnotationLabel + : annotation.label, + color: toRangeAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newRangeAnnotation); + } + }} + compressed + /> + + ); +}; + +export const ConfigPanelRangeDatePicker = ({ + value, + label, + prependLabel, + onChange, + dataTestSubj = 'lnsXY_annotation_date_picker', +}: { + value: moment.Moment; + prependLabel?: string; + label?: string; + onChange: (val: moment.Moment | null) => void; + dataTestSubj?: string; +}) => { + return ( + + {prependLabel ? ( + {prependLabel} + } + > + + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx new file mode 100644 index 00000000000000..3567ebc821dedf --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -0,0 +1,229 @@ +/* + * 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 { + htmlIdGenerator, + EuiButton, + EuiButtonIcon, + EuiDraggable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; +import type { PointInTimeQueryEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import type { ExistingFieldsMap, IndexPattern } from '../../../types'; +import { + fieldExists, + FieldOption, + FieldOptionValue, + FieldPicker, + useDebouncedValue, + NewBucketButton, + DragDropBuckets, +} from '../../../shared_components'; + +const generateId = htmlIdGenerator(); + +export interface FieldInputsProps { + currentConfig: PointInTimeQueryEventAnnotationConfig; + setConfig: (config: PointInTimeQueryEventAnnotationConfig) => void; + indexPattern: IndexPattern; + existingFields: ExistingFieldsMap; + invalidFields?: string[]; +} + +interface WrappedValue { + id: string; + value: string | undefined; + isNew?: boolean; +} + +type SafeWrappedValue = Omit & { value: string }; + +function removeNewEmptyField(v: WrappedValue): v is SafeWrappedValue { + return v.value != null; +} + +export function TooltipSection({ + currentConfig, + setConfig, + indexPattern, + existingFields, + invalidFields, +}: FieldInputsProps) { + const onChangeWrapped = useCallback( + (values: WrappedValue[]) => { + setConfig({ + ...currentConfig, + additionalFields: values.filter(removeNewEmptyField).map(({ value }) => value), + }); + }, + [setConfig, currentConfig] + ); + const wrappedValues = useMemo(() => { + return currentConfig.additionalFields?.map((value) => ({ id: generateId(), value })) || []; + }, [currentConfig]); + + const { inputValue: localValues, handleInputChange } = useDebouncedValue({ + onChange: onChangeWrapped, + value: wrappedValues, + }); + + const onFieldSelectChange = useCallback( + (choice, index = 0) => { + const fields = [...localValues]; + + if (indexPattern.getFieldByName(choice.field)) { + fields[index] = { id: generateId(), value: choice.field }; + + // update the layer state + handleInputChange(fields); + } + }, + [localValues, indexPattern, handleInputChange] + ); + + // diminish attention to adding fields alternative + if (localValues.length === 0) { + return ( + <> + + {}}> + {i18n.translate('xpack.lens.xyChart.annotation.tooltip.noFields', { + defaultMessage: 'None selected', + })} + + + { + handleInputChange([ + ...localValues, + { id: generateId(), value: undefined, isNew: true }, + ]); + }} + label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { + defaultMessage: 'Add field', + })} + /> + + ); + } + const disableActions = localValues.length === 2 && localValues.some(({ isNew }) => isNew); + const options = indexPattern.fields + .filter(({ displayName, type }) => displayName && type !== 'document') + .map( + (field) => + ({ + label: field.displayName, + value: { + type: 'field', + field: field.name, + dataType: field.type, + }, + exists: fieldExists(existingFields[indexPattern.title], field.name), + compatible: true, + 'data-test-subj': `lnsXY-annotation-tooltip-fieldOption-${field.name}`, + } as FieldOption) + ); + + return ( + <> + { + handleInputChange(updatedValues); + }} + onDragStart={() => {}} + droppableId="ANNOTATION_TOOLTIP_DROPPABLE_AREA" + items={localValues} + > + {localValues.map(({ id, value, isNew }, index) => { + return ( + + {(provided) => ( + + {/* Empty for spacing */} + + + + + + + + { + handleInputChange(localValues.filter((_, i) => i !== index)); + }} + data-test-subj={`lnsXY-annotation-tooltip-removeField-${index}`} + isDisabled={disableActions && !isNew} + /> + + + )} + + ); + })} + + { + handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]); + }} + data-test-subj={`lnsXY-annotation-tooltip-addField`} + label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { + defaultMessage: 'Add field', + })} + /> + + ); +} diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/types.ts similarity index 52% rename from x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/types.ts index 2a61512bbab06d..f446afb6be265e 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/types.ts @@ -5,8 +5,11 @@ * 2.0. */ -export interface IndexPatternRef { - id: string; - title: string; - name?: string; -} +import { + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-plugin/common'; + +export type ManualEventAnnotationType = + | PointInTimeEventAnnotationConfig + | RangeEventAnnotationConfig; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index 1eb4bdebec7622..67e2a983745557 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -89,14 +89,14 @@ function AnnotationLayerHeaderContent({ { const newLayer = { ...layer, indexPatternId: newIndexPatternId }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx index 99a74f287c1a6d..da5411ebebac1f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx @@ -152,7 +152,7 @@ export function TextDecorationSetting({ buttonSize="compressed" options={options} idSelected={ - currentConfig?.textVisibility + !currentConfig?.textVisibility ? `${idPrefix}none` : `${idPrefix}${selectedVisibleOption ?? 'name'}` } From 0f6dbe69dbeaa80680458a21663c57101f3c0c62 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 14:55:03 +0200 Subject: [PATCH 07/98] :construction: More work on change index pattern --- .../common/event_annotation_group/index.ts | 6 -- .../lens/public/app_plugin/lens_top_nav.tsx | 100 +++++++----------- .../lens/public/data_views_service/loader.ts | 14 +-- .../lens/public/data_views_service/service.ts | 13 +-- .../config_panel/config_panel.tsx | 33 +++++- .../editor_frame/config_panel/layer_panel.tsx | 27 ++++- .../editor_frame/data_panel_wrapper.tsx | 24 ++++- .../editor_frame/state_helpers.ts | 30 +----- .../indexpattern_datasource/datapanel.tsx | 5 +- .../indexpattern_datasource/indexpattern.tsx | 54 +++++----- .../public/indexpattern_datasource/loader.ts | 99 ++++++++--------- .../public/state_management/lens_slice.ts | 59 +++++++++++ .../lens/public/state_management/types.ts | 3 +- x-pack/plugins/lens/public/types.ts | 17 ++- x-pack/plugins/lens/public/utils.ts | 21 ---- .../public/xy_visualization/visualization.tsx | 14 +++ 16 files changed, 285 insertions(+), 234 deletions(-) diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts index 111811f203497c..4258d4835c103f 100644 --- a/src/plugins/event_annotation/common/event_annotation_group/index.ts +++ b/src/plugins/event_annotation/common/event_annotation_group/index.ts @@ -18,15 +18,10 @@ export interface EventAnnotationGroupOutput { } export interface EventAnnotationGroupArgs { - index: string; annotations: EventAnnotationOutput[]; index?: IndexPatternExpressionType; } -export type EventAnnotationGroupOutput = EventAnnotationGroupArgs & { - type: 'event_annotation_group'; -}; - export function eventAnnotationGroup(): ExpressionFunctionDefinition< 'event_annotation_group', null, @@ -66,7 +61,6 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition< type: 'event_annotation_group', index: args.index, annotations: args.annotations, - index: args.index, }; }, }; diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 7ce25f7cc60ead..761b551e7964bf 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -16,7 +16,6 @@ import { exporters, getEsQueryConfig } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { trackUiEvent } from '../lens_ui_telemetry'; -import type { StateSetter } from '../types'; import { LensAppServices, LensTopNavActions, @@ -30,16 +29,15 @@ import { useLensDispatch, LensAppState, DispatchSetState, - updateDatasourceState, } from '../state_management'; import { getIndexPatternsObjects, getIndexPatternsIds, getResolvedDateRange, - handleIndexPatternChange, refreshIndexPatternsList, } from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; +import { changeIndexPattern } from '../state_management/lens_slice'; function getLensTopNavConfig(options: { showSaveAndReturn: boolean; @@ -236,19 +234,6 @@ export const LensTopNavMenu = ({ dataViews: dataViewsService, } = useKibana().services; - const dispatch = useLensDispatch(); - const dispatchSetState: DispatchSetState = React.useCallback( - (state: Partial) => dispatch(setState(state)), - [dispatch] - ); - - const [indexPatterns, setIndexPatterns] = useState([]); - const [currentIndexPattern, setCurrentIndexPattern] = useState(); - const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState([]); - const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView()); - const closeFieldEditor = useRef<() => void | undefined>(); - const closeDataViewEditor = useRef<() => void | undefined>(); - const { isSaveable, isLinkedToOriginatingApp, @@ -262,6 +247,42 @@ export const LensTopNavMenu = ({ dataViews, } = useLensSelector((state) => state.lens); + const dispatch = useLensDispatch(); + const dispatchSetState: DispatchSetState = React.useCallback( + (state: Partial) => dispatch(setState(state)), + [dispatch] + ); + const dispatchChangeIndexPattern = React.useCallback( + async (indexPatternId) => { + const newIndexPatterns = await indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: dataViews.indexPatterns, + }); + dispatch( + changeIndexPattern({ + dataViews: { indexPatterns: newIndexPatterns }, + datasourceIds: Object.keys(datasourceStates), + visualizationIds: visualization.activeId ? [visualization.activeId] : [], + indexPatternId, + }) + ); + }, + [ + dataViews.indexPatterns, + datasourceStates, + dispatch, + indexPatternService, + visualization.activeId, + ] + ); + + const [indexPatterns, setIndexPatterns] = useState([]); + const [currentIndexPattern, setCurrentIndexPattern] = useState(); + const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState([]); + const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView()); + const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); + const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); useEffect(() => { @@ -611,18 +632,6 @@ export const LensTopNavMenu = ({ }); }, [data.query.filterManager, data.query.queryString, dispatchSetState]); - const setDatasourceState: StateSetter = useMemo(() => { - return (updater) => { - dispatch( - updateDatasourceState({ - updater, - datasourceId: activeDatasourceId!, - clearStagedPreview: true, - }) - ); - }; - }, [activeDatasourceId, dispatch]); - const refreshFieldList = useCallback(async () => { if (currentIndexPattern && currentIndexPattern.id) { refreshIndexPatternsList({ @@ -688,32 +697,14 @@ export const LensTopNavMenu = ({ closeDataViewEditor.current = dataViewEditor.openEditor({ onSave: async (dataView) => { if (dataView.id) { - handleIndexPatternChange({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - indexPatternId: dataView.id, - setDatasourceState, - }); + await dispatchChangeIndexPattern(dataView.id); refreshFieldList(); } }, }); } : undefined, - [ - dataViewEditor, - canEditDataView, - datasourceMap, - datasourceStates, - refreshFieldList, - setDatasourceState, - ] + [canEditDataView, dataViewEditor, dispatchChangeIndexPattern, refreshFieldList] ); const dataViewPickerProps = { @@ -730,18 +721,7 @@ export const LensTopNavMenu = ({ (indexPattern) => indexPattern.id === newIndexPatternId ); setCurrentIndexPattern(currentDataView); - handleIndexPatternChange({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - indexPatternId: newIndexPatternId, - setDatasourceState, - }); + dispatchChangeIndexPattern(newIndexPatternId); }, }; diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index 5b2a33d66e1334..89d1765bc61afe 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -167,18 +167,13 @@ export async function loadIndexPatterns({ return indexPatternsObject; } -export async function changeIndexPattern({ +export async function loadIndexPattern({ id, - updateIndexPatterns, onError, dataViews, cache = {}, }: { id: string; - updateIndexPatterns: ( - newFieldState: Pick, - options?: { applyImmediately: boolean } - ) => void; onError: ErrorHandler; dataViews: DataViewsContract; cache?: IndexPatternMap; @@ -198,13 +193,6 @@ export async function changeIndexPattern({ ...cache, [id]: indexPatterns[id], }; - - updateIndexPatterns( - { - indexPatterns: newIndexPatterns, - }, - { applyImmediately: true } - ); return newIndexPatterns; } diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/data_views_service/service.ts index 43a77e919fae6b..047013525266f1 100644 --- a/x-pack/plugins/lens/public/data_views_service/service.ts +++ b/x-pack/plugins/lens/public/data_views_service/service.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import type { DateRange } from '../../common'; import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; import { - changeIndexPattern, + loadIndexPattern, loadIndexPatternRefs, loadIndexPatterns, syncExistingFields, @@ -51,13 +51,9 @@ export interface IndexPatternServiceAPI { */ loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise; /** - * Load an indexPattern to the cache, usually used in conjuction with a indexPattern change action. - * - * **Note**: - * this function has sideEffects, updating the Lens state with the new indexPattern loaded, or showing - * a notification error toast in case of loading issues + * Ensure an indexPattern is loaded in the cache, usually used in conjuction with a indexPattern change action. */ - addIndexPattern: (args: { + ensureIndexPattern: (args: { id: string; cache: IndexPatternMap; }) => Promise; @@ -107,8 +103,7 @@ export function createIndexPatternService({ ...args, }); }, - addIndexPattern: (args) => - changeIndexPattern({ updateIndexPatterns, onError: onChangeError, dataViews, ...args }), + ensureIndexPattern: (args) => loadIndexPattern({ onError: onChangeError, dataViews, ...args }), refreshExistingFields: (args) => syncExistingFields({ updateIndexPatterns, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index b87b98b061baf6..ff5f71708e562b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useMemo, memo } from 'react'; +import React, { useMemo, memo, useCallback } from 'react'; import { EuiForm } from '@elastic/eui'; import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import { UPDATE_FILTER_REFERENCES_ACTION, UPDATE_FILTER_REFERENCES_TRIGGER, } from '@kbn/unified-search-plugin/public'; +import { changeIndexPattern } from '../../../state_management/lens_slice'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -147,6 +148,35 @@ export function LayerPanels( [dispatchLens] ); + const onChangeIndexPattern = useCallback( + async ({ + indexPatternId, + datasourceId, + visualizationId, + layerId, + }: { + indexPatternId: string; + datasourceId?: string; + visualizationId?: string; + layerId?: string; + }) => { + const indexPatterns = await props.indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: props.framePublicAPI.dataViews.indexPatterns, + }); + dispatchLens( + changeIndexPattern({ + indexPatternId, + datasourceIds: datasourceId ? [datasourceId] : [], + visualizationIds: visualizationId ? [visualizationId] : [], + layerId, + dataViews: { indexPatterns }, + }) + ); + }, + [dispatchLens, props.framePublicAPI.dataViews, props.indexPatternService] + ); + return ( {layerIds.map((layerId, layerIndex) => ( @@ -161,6 +191,7 @@ export function LayerPanels( updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateDatasourceAsync={updateDatasourceAsync} + onChangeIndexPattern={onChangeIndexPattern} updateAll={updateAll} isOnlyLayer={ getRemoveOperation( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 0ebfc5afc16193..b81e5765b6e6c0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -67,6 +67,12 @@ export function LayerPanel( registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; toggleFullscreen: () => void; onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void; + onChangeIndexPattern: (args: { + indexPatternId: string; + layerId: string; + datasourceId?: string; + visualizationId?: string; + }) => void; indexPatternService: IndexPatternServiceAPI; } ) { @@ -89,7 +95,7 @@ export function LayerPanel( updateAll, updateDatasourceAsync, visualizationState, - indexPatternService, + onChangeIndexPattern, } = props; const datasourceStates = useLensSelector(selectDatasourceStates); @@ -311,6 +317,12 @@ export function LayerPanel( layerConfigProps={{ ...layerVisualizationConfigProps, setState: props.updateVisualization, + onChangeIndexPattern: (indexPatternId) => + onChangeIndexPattern({ + indexPatternId, + layerId, + visualizationId: activeVisualization.id, + }), }} activeVisualization={activeVisualization} /> @@ -334,7 +346,8 @@ export function LayerPanel( state: layerDatasourceState, activeData: props.framePublicAPI.activeData, dataViews, - indexPatternService, + onChangeIndexPattern: (indexPatternId) => + onChangeIndexPattern({ indexPatternId, layerId, datasourceId }), setState: (updater: unknown) => { const newState = typeof updater === 'function' ? updater(layerDatasourceState) : updater; @@ -373,9 +386,13 @@ export function LayerPanel( layerId, state: visualizationState, frame: framePublicAPI, - setState: (newState) => { - props.updateVisualization(newState); - }, + setState: props.updateVisualization, + onChangeIndexPattern: (indexPatternId) => + onChangeIndexPattern({ + indexPatternId, + layerId, + visualizationId: activeVisualization.id, + }), }} /> )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 18a40b28929119..09f8233ef1bca3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -7,7 +7,7 @@ import './data_panel_wrapper.scss'; -import React, { useMemo, memo, useContext, useState, useEffect } from 'react'; +import React, { useMemo, memo, useContext, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { Storage } from '@kbn/kibana-utils-plugin/public'; @@ -30,6 +30,7 @@ import { } from '../../state_management'; import { initializeSources } from './state_helpers'; import type { IndexPatternServiceAPI } from '../../data_views_service/service'; +import { changeIndexPattern } from '../../state_management/lens_slice'; interface DataPanelWrapperProps { datasourceMap: DatasourceMap; @@ -105,6 +106,26 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { props.core.uiSettings, ]); + const onChangeIndexPattern = useCallback( + async (indexPatternId: string, datasourceId: string, layerId?: string) => { + // reload the indexpattern + const indexPatterns = await props.indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: props.frame.dataViews.indexPatterns, + }); + // now update the state + dispatchLens( + changeIndexPattern({ + dataViews: { indexPatterns }, + datasourceIds: [datasourceId], + indexPatternId, + layerId, + }) + ); + }, + [props.indexPatternService, props.frame.dataViews.indexPatterns, dispatchLens] + ); + const datasourceProps: DatasourceDataPanelProps = { ...externalContext, dragDropContext: useContext(DragContext), @@ -115,6 +136,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, uiActions: props.plugins.uiActions, + onChangeIndexPattern, indexPatternService: props.indexPatternService, frame: props.frame, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 92992f9eac0237..6017a7125abaeb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -199,7 +199,7 @@ export function initializeDatasources({ for (const [datasourceId, datasource] of Object.entries(datasourceMap)) { if (datasourceStates[datasourceId]) { const state = datasource.initialize( - datasourceStates[datasourceId] || undefined, + datasourceStates[datasourceId].state || undefined, references, initialContext, indexPatternRefs, @@ -211,34 +211,6 @@ export function initializeDatasources({ return states; } -// export async function initializeDatasources( -// datasourceMap: DatasourceMap, -// datasourceStates: DatasourceStates, -// references?: SavedObjectReference[], -// initialContext?: VisualizeFieldContext | VisualizeEditorContext, -// options?: InitializationOptions -// ) { -// const states: DatasourceStates = {}; - -// await Promise.all( -// Object.entries(datasourceMap).map(([datasourceId, datasource]) => { -// if (datasourceStates[datasourceId]) { -// return datasource -// .initialize( -// datasourceStates[datasourceId].state || undefined, -// references, -// initialContext, -// options -// ) -// .then((datasourceState) => { -// states[datasourceId] = { isLoading: false, state: datasourceState }; -// }); -// } -// }) -// ); -// return states; -// } - export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceStates: DatasourceStates, datasourceMap: DatasourceMap diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ffb1987b54ccb6..ff993c9e67f175 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -51,7 +51,10 @@ import { FieldGroups, FieldList } from './field_list'; import { fieldContainsData, fieldExists } from '../shared_components'; import { IndexPatternServiceAPI } from '../data_views_service/service'; -export type Props = Omit, 'core'> & { +export type Props = Omit< + DatasourceDataPanelProps, + 'core' | 'onChangeIndexPattern' +> & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 067798148f1770..656b29558b211b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -42,6 +42,7 @@ import { injectReferences, loadInitialState, onRefreshIndexPattern, + triggerActionOnIndexPatternChange, } from './loader'; import { toExpression } from './to_expression'; import { @@ -135,9 +136,11 @@ export function getIndexPatternDatasource({ }) { const uiSettings = core.uiSettings; + const DATASOURCE_ID = 'indexpattern'; + // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { - id: 'indexpattern', + id: DATASOURCE_ID, initialize( persistedState?: IndexPatternPersistedState, @@ -156,15 +159,6 @@ export function getIndexPatternDatasource({ indexPatternRefs, indexPatterns, }); - // return loadInitialState({ - // persistedState, - // references, - // defaultIndexPatternId: core.uiSettings.get('defaultIndex'), - // storage, - // indexPatternsService, - // initialContext, - // options, - // }); }, getPersistableState(state: IndexPatternPrivateState) { @@ -251,26 +245,20 @@ export function getIndexPatternDatasource({ domElement: Element, props: DatasourceDataPanelProps ) { + const { onChangeIndexPattern, ...otherProps } = props; render( { - changeIndexPattern({ - id, - state, - setState, - storage, - indexPatterns: props.frame.dataViews.indexPatterns, - indexPatternService: props.indexPatternService, - }); + changeIndexPattern={(indexPattern) => { + onChangeIndexPattern(indexPattern, DATASOURCE_ID); }} data={data} dataViews={dataViews} fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={dataViewFieldEditor} - {...props} + {...otherProps} core={core} uiActions={uiActions} onIndexPatternRefresh={onRefreshIndexPattern} @@ -393,23 +381,20 @@ export function getIndexPatternDatasource({ domElement: Element, props: DatasourceLayerPanelProps ) => { + const { onChangeIndexPattern, ...otherProps } = props; render( { - changeLayerIndexPattern({ + triggerActionOnIndexPatternChange({ indexPatternId, - setState: props.setState, state: props.state, layerId: props.layerId, - replaceIfPossible: true, - storage, - indexPatterns: props.dataViews.indexPatterns, - indexPatternService: props.indexPatternService, uiActions, }); + onChangeIndexPattern(indexPatternId, DATASOURCE_ID, props.layerId); }} - {...props} + {...otherProps} /> , domElement @@ -459,6 +444,19 @@ export function getIndexPatternDatasource({ }, onRefreshIndexPattern, + onIndexPatternChange(state, indexPatterns, indexPatternId, layerId) { + if (layerId) { + return changeLayerIndexPattern({ + indexPatternId, + layerId, + state, + replaceIfPossible: true, + storage, + indexPatterns, + }); + } + return changeIndexPattern({ indexPatternId, state, storage, indexPatterns }); + }, // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed @@ -480,7 +478,7 @@ export function getIndexPatternDatasource({ const visibleColumnIds = layer.columnOrder.filter((colId) => !isReferenced(layer, colId)); return { - datasourceId: 'indexpattern', + datasourceId: DATASOURCE_ID, getTableSpec: () => { // consider also referenced columns in this case // but map fields to the top referencing column diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 555de27471cf35..204e5761404112 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -17,15 +17,12 @@ import { UiActionsStart, VisualizeFieldContext, } from '@kbn/ui-actions-plugin/public'; -import type { DatasourceDataPanelProps, VisualizeEditorContext } from '../types'; +import type { VisualizeEditorContext } from '../types'; import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { memoizedGetAvailableOperationsByMetadata, updateLayerIndexPattern } from './operations'; import { readFromStorage, writeToStorage } from '../settings_storage'; import type { IndexPattern, IndexPatternRef } from '../types'; -import { IndexPatternServiceAPI } from '../data_views_service/service'; - -type SetState = DatasourceDataPanelProps['setState']; export function onRefreshIndexPattern() { if (memoizedGetAvailableOperationsByMetadata.cache.clear) { @@ -180,65 +177,40 @@ export function loadInitialState({ }; } -export async function changeIndexPattern({ - id, +export function changeIndexPattern({ + indexPatternId, state, - setState, storage, indexPatterns, - indexPatternService, }: { - id: string; + indexPatternId: string; state: IndexPatternPrivateState; - setState: SetState; storage: IStorageWrapper; indexPatterns: Record; - indexPatternService: IndexPatternServiceAPI; }) { - const newIndexPatterns = await indexPatternService.addIndexPattern({ - id, - cache: indexPatterns, - }); - if (newIndexPatterns) { - setState( - (s) => ({ - ...s, - layers: isSingleEmptyLayer(state.layers) - ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) - : state.layers, - currentIndexPatternId: id, - }), - { applyImmediately: true } - ); - setLastUsedIndexPatternId(storage, id); - } + setLastUsedIndexPatternId(storage, indexPatternId); + return { + ...state, + layers: isSingleEmptyLayer(state.layers) + ? mapValues(state.layers, (layer) => + updateLayerIndexPattern(layer, indexPatterns[indexPatternId]) + ) + : state.layers, + currentIndexPatternId: indexPatternId, + }; } -export async function changeLayerIndexPattern({ - indexPatternId, - layerId, +export function triggerActionOnIndexPatternChange({ state, - setState, - replaceIfPossible, - storage, - indexPatterns, - indexPatternService, + layerId, uiActions, + indexPatternId, }: { indexPatternId: string; layerId: string; state: IndexPatternPrivateState; - setState: SetState; - replaceIfPossible?: boolean; - storage: IStorageWrapper; - indexPatterns: Record; - indexPatternService: IndexPatternServiceAPI; uiActions: UiActionsStart; }) { - const newIndexPatterns = await indexPatternService.addIndexPattern({ - id: indexPatternId, - cache: indexPatterns, - }); const fromDataView = state.layers[layerId].indexPatternId; const toDataView = indexPatternId; @@ -252,17 +224,32 @@ export async function changeLayerIndexPattern({ defaultDataView: toDataView, usedDataViews: Object.values(Object.values(state.layers).map((layer) => layer.indexPatternId)), } as ActionExecutionContext); - if (newIndexPatterns) { - setState((s) => ({ - ...s, - layers: { - ...s.layers, - [layerId]: updateLayerIndexPattern(s.layers[layerId], indexPatterns[indexPatternId]), - }, - currentIndexPatternId: replaceIfPossible ? indexPatternId : s.currentIndexPatternId, - })); - setLastUsedIndexPatternId(storage, indexPatternId); - } +} + +export function changeLayerIndexPattern({ + indexPatternId, + indexPatterns, + layerId, + state, + replaceIfPossible, + storage, +}: { + indexPatternId: string; + layerId: string; + state: IndexPatternPrivateState; + replaceIfPossible?: boolean; + storage: IStorageWrapper; + indexPatterns: Record; +}) { + setLastUsedIndexPatternId(storage, indexPatternId); + return { + ...state, + layers: { + ...state.layers, + [layerId]: updateLayerIndexPattern(state.layers[layerId], indexPatterns[indexPatternId]), + }, + currentIndexPatternId: replaceIfPossible ? indexPatternId : state.currentIndexPatternId, + }; } function isSingleEmptyLayer(layerMap: IndexPatternPrivateState['layers']) { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 4103d665f58ec3..addcfacc3ecf8e 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -171,6 +171,13 @@ export const setLayerDefaultDimension = createAction<{ export const updateIndexPatterns = createAction>( 'lens/updateIndexPatterns' ); +export const changeIndexPattern = createAction<{ + visualizationIds?: string[]; + datasourceIds?: string[]; + indexPatternId: string; + layerId?: string; + dataViews: Partial; +}>('lens/changeIndexPattern'); export const lensActions = { setState, @@ -198,6 +205,7 @@ export const lensActions = { addLayer, setLayerDefaultDimension, updateIndexPatterns, + changeIndexPattern, }; export const makeLensReducer = (storeDeps: LensStoreDeps) => { @@ -292,6 +300,57 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ? activeVisualization.clearLayer(state.visualization.state, layerId) : activeVisualization.removeLayer(state.visualization.state, layerId); }, + [changeIndexPattern.type]: ( + state, + { + payload, + }: { + payload: { + visualizationIds?: string; + datasourceIds?: string; + layerId?: string; + indexPatternId: string; + dataViews: Pick; + }; + } + ) => { + const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload; + if (visualizationIds?.length) { + for (const visualizationId of visualizationIds) { + const activeVisualization = + visualizationId && + state.visualization.activeId !== visualizationId && + visualizationMap[visualizationId]; + if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) { + state.visualization.state = activeVisualization.onIndexPatternChange( + state.visualization.state, + layerId, + indexPatternId + ); + } + } + } + if (datasourceIds?.length) { + for (const datasourceId of datasourceIds) { + const activeDatasource = datasourceId && datasourceMap[datasourceId]; + if (activeDatasource && activeDatasource?.onIndexPatternChange) { + state.datasourceStates = { + ...state.datasourceStates, + [datasourceId]: { + isLoading: false, + state: activeDatasource.onIndexPatternChange( + state.datasourceStates[datasourceId].state, + dataViews.indexPatterns, + indexPatternId, + layerId + ), + }, + }; + } + } + } + state.dataViews = { ...state.dataViews, ...dataViews }; + }, [updateIndexPatterns.type]: (state, { payload }: { payload: Partial }) => { return { ...state, diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 8227ce438ae8ee..7b461338261fc6 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -20,9 +20,8 @@ import { SharingSavedObjectProps, VisualizeEditorContext, IndexPattern, + IndexPatternRef, } from '../types'; -import type { IndexPatternRef } from '../shared_components'; -// import type { IndexPattern } from '../indexpattern_datasource'; export interface VisualizationState { activeId: string | null; state: unknown; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 4ede783556f42d..5081db3a28f0a0 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -335,8 +335,13 @@ export interface Datasource { state: T; setState: StateSetter; }) => void; + onIndexPatternChange?: ( + state: T, + indexPatterns: IndexPatternMap, + indexPatternId: string, + layerId?: string + ) => T; - // refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; onRefreshIndexPattern: () => void; toExpression: ( @@ -472,6 +477,7 @@ export interface DatasourceDataPanelProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + onChangeIndexPattern: (indexPatternId: string, datasourceId: string, layerId?: string) => void; uiActions: UiActionsStart; indexPatternService: IndexPatternServiceAPI; frame: FramePublicAPI; @@ -536,7 +542,7 @@ export interface DatasourceLayerPanelProps { setState: StateSetter; activeData?: Record; dataViews: DataViewsState; - indexPatternService: IndexPatternServiceAPI; + onChangeIndexPattern: (indexPatternId: string, datasourceId: string, layerId?: string) => void; } export interface DragDropOperation { @@ -627,6 +633,7 @@ export interface VisualizationConfigProps { export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; + onChangeIndexPattern: (indexPatternId: string, layerId: string) => void; }; export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps & { @@ -1051,6 +1058,12 @@ export interface Visualization { * On Edit events the frame will call this to know what's going to be the next visualization state */ onEditAction?: (state: T, event: LensEditEvent) => T; + + /** + * Some visualization track indexPattern changes (i.e. annotations) + * This method makes it aware of the change and produces a new updated state + */ + onIndexPatternChange?: (state: T, indexPatternId: string, layerId?: string) => T; } // Use same technique as TriggerContext diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 23ec16797fe498..13ff13b6ab93b0 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -18,7 +18,6 @@ import type { Datasource, DatasourceMap, Visualization, - StateSetter, IndexPatternMap, IndexPatternRef, } from './types'; @@ -78,26 +77,6 @@ export function getInitialDataViewsObject( }; } -export function handleIndexPatternChange({ - activeDatasources, - datasourceStates, - indexPatternId, - setDatasourceState, -}: { - activeDatasources: Record; - datasourceStates: DatasourceStates; - indexPatternId: string; - setDatasourceState: StateSetter; -}): void { - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasource?.updateCurrentIndexPatternId?.({ - state: datasourceStates[id].state, - indexPatternId, - setState: setDatasourceState, - }); - }); -} - export async function refreshIndexPatternsList({ activeDatasources, indexPatternService, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c29ce605d8eff1..e2c75e635452a6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -199,6 +199,20 @@ export const getXyVisualization = ({ ]; }, + onIndexPatternChange(state, indexPatternId, layerId) { + const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[layerIndex]; + if (!layer || !isAnnotationsLayer(layer)) { + return state; + } + const newLayers = [...state.layers]; + newLayers[layerIndex] = { ...layer, indexPatternId }; + return { + ...state, + layers: newLayers, + }; + }, + getConfiguration({ state, frame, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); if (!layer) { From 57d814db0b0edee7acc2dedc243cf777b17cdff8 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 17:34:25 +0200 Subject: [PATCH 08/98] Move lens dataViews state into main state --- x-pack/plugins/lens/public/app_plugin/app.tsx | 29 +- .../lens/public/app_plugin/lens_top_nav.tsx | 117 ++--- .../public/app_plugin/show_underlying_data.ts | 4 +- .../plugins/lens/public/app_plugin/types.ts | 2 + .../lens/public/data_views_service/loader.ts | 309 ++++++++++++ .../lens/public/data_views_service/service.ts | 117 +++++ .../buttons/draggable_dimension_button.tsx | 4 + .../buttons/empty_dimension_button.tsx | 4 + .../config_panel/config_panel.tsx | 36 +- .../editor_frame/config_panel/layer_panel.tsx | 123 +++-- .../editor_frame/config_panel/types.ts | 2 + .../editor_frame/data_panel_wrapper.tsx | 63 ++- .../editor_frame/editor_frame.tsx | 8 +- .../editor_frame/expression_helpers.ts | 12 +- .../editor_frame/state_helpers.ts | 264 ++++++++-- .../editor_frame/suggestion_helpers.ts | 20 +- .../editor_frame/suggestion_panel.tsx | 9 +- .../workspace_panel/chart_switch.tsx | 1 + .../workspace_panel/workspace_panel.tsx | 6 +- .../public/editor_frame_service/service.tsx | 30 +- .../lens/public/editor_frame_service/types.ts | 2 +- .../lens/public/embeddable/embeddable.tsx | 4 +- .../public/embeddable/embeddable_factory.ts | 6 +- .../indexpattern_datasource/datapanel.tsx | 131 +++-- .../bucket_nesting_editor.test.tsx | 2 +- .../dimension_panel/dimension_editor.tsx | 22 +- .../dimension_panel/dimension_panel.tsx | 5 +- .../droppable/get_drop_props.ts | 27 +- .../droppable/on_drop_handler.ts | 33 +- .../dimension_panel/field_select.tsx | 13 +- .../dimension_panel/operation_support.ts | 6 +- .../indexpattern_datasource/field_item.tsx | 4 +- .../indexpattern_datasource/field_list.tsx | 167 +++---- .../fields_accordion.tsx | 4 +- .../public/indexpattern_datasource/index.ts | 2 +- .../indexpattern_datasource/indexpattern.tsx | 156 +++--- .../indexpattern_suggestions.ts | 111 +++-- .../layerpanel.test.tsx | 2 +- .../indexpattern_datasource/layerpanel.tsx | 13 +- .../public/indexpattern_datasource/loader.ts | 464 ++++-------------- .../definitions/formula/formula.test.tsx | 3 +- .../definitions/ranges/ranges.test.tsx | 5 +- .../definitions/shared_components/index.tsx | 1 - .../definitions/terms/field_inputs.tsx | 11 +- .../definitions/terms/terms.test.tsx | 3 +- .../operations/layer_helpers.test.ts | 3 +- .../operations/layer_helpers.ts | 4 +- .../operations/operations.ts | 3 +- .../indexpattern_datasource/pure_helpers.ts | 10 +- .../indexpattern_datasource/query_input.tsx | 7 +- .../time_shift_utils.tsx | 9 +- .../indexpattern_datasource/to_expression.ts | 6 +- .../public/indexpattern_datasource/types.ts | 48 +- .../public/indexpattern_datasource/utils.tsx | 8 +- x-pack/plugins/lens/public/plugin.ts | 10 +- .../dataview_picker/dataview_picker.tsx | 93 ++++ .../dataview_picker/helpers.ts | 29 ++ .../dataview_picker/index.ts | 9 + .../drag_drop_bucket/buckets.test.tsx | 79 +++ .../drag_drop_bucket/buckets.tsx | 160 ++++++ .../field_picker/field_picker.tsx | 2 +- .../lens/public/shared_components/index.ts | 6 + .../context_middleware/index.ts | 7 +- .../lens/public/state_management/index.ts | 1 + .../init_middleware/load_initial.ts | 95 +++- .../public/state_management/lens_slice.ts | 99 +++- .../lens/public/state_management/selectors.ts | 5 +- .../lens/public/state_management/types.ts | 17 +- x-pack/plugins/lens/public/types.ts | 141 +++++- x-pack/plugins/lens/public/utils.ts | 93 ++-- .../lens/public/xy_visualization/index.ts | 8 +- .../lens/public/xy_visualization/types.ts | 2 + .../public/xy_visualization/visualization.tsx | 38 +- .../visualization_helpers.tsx | 13 +- 74 files changed, 2324 insertions(+), 1038 deletions(-) create mode 100644 x-pack/plugins/lens/public/data_views_service/loader.ts create mode 100644 x-pack/plugins/lens/public/data_views_service/service.ts create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts create mode 100644 x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4f5027c908b09e..93d14d4306fb04 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -25,11 +25,13 @@ import { LensAppState, DispatchSetState, selectSavedObjectFormat, + updateIndexPatterns, } from '../state_management'; import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container'; import { LensInspector } from '../lens_inspector_service'; import { getEditPath } from '../../common'; import { isLensEqual } from './lens_document_equality'; +import { IndexPatternServiceAPI, createIndexPatternService } from '../data_views_service/service'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -66,6 +68,7 @@ export function App({ getOriginatingAppName, spaces, http, + notifications, executionContext, // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag, @@ -360,6 +363,22 @@ export function App({ ); }, [initialContext]); + const indexPatternService = useMemo( + () => + createIndexPatternService({ + dataViews: lensAppServices.dataViews, + uiSettings: lensAppServices.uiSettings, + core: { http, notifications }, + updateIndexPatterns: (newIndexPatternsState, options) => { + dispatch(updateIndexPatterns(newIndexPatternsState)); + if (options?.applyImmediately) { + dispatch(applyChanges()); + } + }, + }), + [dispatch, http, notifications, lensAppServices] + ); + return ( <>
@@ -381,6 +400,7 @@ export function App({ topNavMenuEntryGenerators={topNavMenuEntryGenerators} initialContext={initialContext} theme$={theme$} + indexPatternService={indexPatternService} /> {getLegacyUrlConflictCallout()} {(!isLoading || persistedDoc) && ( @@ -388,6 +408,7 @@ export function App({ editorFrame={editorFrame} showNoDataPopover={showNoDataPopover} lensInspector={lensInspector} + indexPatternService={indexPatternService} /> )}
@@ -449,13 +470,19 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({ editorFrame, showNoDataPopover, lensInspector, + indexPatternService, }: { editorFrame: EditorFrameInstance; lensInspector: LensInspector; showNoDataPopover: () => void; + indexPatternService: IndexPatternServiceAPI; }) { const { EditorFrameContainer } = editorFrame; return ( - + ); }); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 5dd2368d2b83ac..98acf40dedb74c 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -15,7 +15,6 @@ import { tableHasFormulas } from '@kbn/data-plugin/common'; import { exporters, getEsQueryConfig } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { StateSetter } from '../types'; import { LensAppServices, LensTopNavActions, @@ -29,16 +28,15 @@ import { useLensDispatch, LensAppState, DispatchSetState, - updateDatasourceState, } from '../state_management'; import { getIndexPatternsObjects, getIndexPatternsIds, getResolvedDateRange, - handleIndexPatternChange, refreshIndexPatternsList, } from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; +import { changeIndexPattern } from '../state_management/lens_slice'; function getLensTopNavConfig(options: { showSaveAndReturn: boolean; @@ -219,6 +217,7 @@ export const LensTopNavMenu = ({ topNavMenuEntryGenerators, initialContext, theme$, + indexPatternService, }: LensTopNavMenuProps) => { const { data, @@ -231,14 +230,50 @@ export const LensTopNavMenu = ({ dashboardFeatureFlag, dataViewFieldEditor, dataViewEditor, - dataViews, + dataViews: dataViewsService, } = useKibana().services; + const { + isSaveable, + isLinkedToOriginatingApp, + query, + activeData, + savedQuery, + activeDatasourceId, + datasourceStates, + visualization, + filters, + dataViews, + } = useLensSelector((state) => state.lens); + const dispatch = useLensDispatch(); const dispatchSetState: DispatchSetState = React.useCallback( (state: Partial) => dispatch(setState(state)), [dispatch] ); + const dispatchChangeIndexPattern = React.useCallback( + async (indexPatternId) => { + const newIndexPatterns = await indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: dataViews.indexPatterns, + }); + dispatch( + changeIndexPattern({ + dataViews: { indexPatterns: newIndexPatterns }, + datasourceIds: Object.keys(datasourceStates), + visualizationIds: visualization.activeId ? [visualization.activeId] : [], + indexPatternId, + }) + ); + }, + [ + dataViews.indexPatterns, + datasourceStates, + dispatch, + indexPatternService, + visualization.activeId, + ] + ); const [indexPatterns, setIndexPatterns] = useState([]); const [currentIndexPattern, setCurrentIndexPattern] = useState(); @@ -247,18 +282,6 @@ export const LensTopNavMenu = ({ const closeFieldEditor = useRef<() => void | undefined>(); const closeDataViewEditor = useRef<() => void | undefined>(); - const { - isSaveable, - isLinkedToOriginatingApp, - query, - activeData, - savedQuery, - activeDatasourceId, - datasourceStates, - visualization, - filters, - } = useLensSelector((state) => state.lens); - const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); useEffect(() => { @@ -290,7 +313,7 @@ export const LensTopNavMenu = ({ // Update the cached index patterns if the user made a change to any of them if (hasIndexPatternsChanged) { - getIndexPatternsObjects(indexPatternIds, dataViews).then( + getIndexPatternsObjects(indexPatternIds, dataViewsService).then( ({ indexPatterns: indexPatternObjects, rejectedIds }) => { setIndexPatterns(indexPatternObjects); setRejectedIndexPatterns(rejectedIds); @@ -303,7 +326,7 @@ export const LensTopNavMenu = ({ rejectedIndexPatterns, datasourceMap, indexPatterns, - dataViews, + dataViewsService, ]); useEffect(() => { @@ -366,6 +389,7 @@ export const LensTopNavMenu = ({ datasourceMap[activeDatasourceId], datasourceStates[activeDatasourceId].state, activeData, + dataViews.indexPatterns, data.query.timefilter.timefilter.getTime(), application.capabilities ); @@ -375,6 +399,7 @@ export const LensTopNavMenu = ({ datasourceMap, datasourceStates, activeData, + dataViews.indexPatterns, data.query.timefilter.timefilter, application.capabilities, ]); @@ -604,18 +629,6 @@ export const LensTopNavMenu = ({ }); }, [data.query.filterManager, data.query.queryString, dispatchSetState]); - const setDatasourceState: StateSetter = useMemo(() => { - return (updater) => { - dispatch( - updateDatasourceState({ - updater, - datasourceId: activeDatasourceId!, - clearStagedPreview: true, - }) - ); - }; - }, [activeDatasourceId, dispatch]); - const refreshFieldList = useCallback(async () => { if (currentIndexPattern && currentIndexPattern.id) { refreshIndexPatternsList({ @@ -627,7 +640,8 @@ export const LensTopNavMenu = ({ {} ), indexPatternId: currentIndexPattern.id, - setDatasourceState, + indexPatternService, + indexPatternsCache: dataViews.indexPatterns, }); } // start a new session so all charts are refreshed @@ -637,7 +651,8 @@ export const LensTopNavMenu = ({ data.search.session, datasourceMap, datasourceStates, - setDatasourceState, + indexPatternService, + dataViews.indexPatterns, ]); const editField = useMemo( @@ -679,32 +694,14 @@ export const LensTopNavMenu = ({ closeDataViewEditor.current = dataViewEditor.openEditor({ onSave: async (dataView) => { if (dataView.id) { - handleIndexPatternChange({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - indexPatternId: dataView.id, - setDatasourceState, - }); + await dispatchChangeIndexPattern(dataView.id); refreshFieldList(); } }, }); } : undefined, - [ - dataViewEditor, - canEditDataView, - datasourceMap, - datasourceStates, - refreshFieldList, - setDatasourceState, - ] + [canEditDataView, dataViewEditor, dispatchChangeIndexPattern, refreshFieldList] ); const dataViewPickerProps = { @@ -721,18 +718,7 @@ export const LensTopNavMenu = ({ (indexPattern) => indexPattern.id === newIndexPatternId ); setCurrentIndexPattern(currentDataView); - handleIndexPatternChange({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - indexPatternId: newIndexPatternId, - setDatasourceState, - }); + dispatchChangeIndexPattern(newIndexPatternId); }, }; @@ -759,7 +745,8 @@ export const LensTopNavMenu = ({ allLoaded && activeDatasourceId && datasourceMap[activeDatasourceId].isTimeBased( - datasourceStates[activeDatasourceId].state + datasourceStates[activeDatasourceId].state, + dataViews.indexPatterns ) ) } diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index 310e91d3caaad6..39d7cda3677c07 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -20,7 +20,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities } from '@kbn/core/public'; import { partition } from 'lodash'; import { TableInspectorAdapter } from '../editor_frame_service/types'; -import { Datasource } from '../types'; +import { Datasource, IndexPatternMap } from '../types'; /** * Joins a series of queries. @@ -61,6 +61,7 @@ export function getLayerMetaInfo( currentDatasource: Datasource | undefined, datasourceState: unknown, activeData: TableInspectorAdapter | undefined, + indexPatterns: IndexPatternMap, timeRange: TimeRange | undefined, capabilities: RecursiveReadonly<{ navLinks: Capabilities['navLinks']; @@ -93,6 +94,7 @@ export function getLayerMetaInfo( const datasourceAPI = currentDatasource.getPublicAPI({ layerId: firstLayerId, state: datasourceState, + indexPatterns, }); // maybe add also datasourceId validation here? if (datasourceAPI.datasourceId !== 'indexpattern') { diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index abb6cfa6a06a6e..fe4df5f26593d1 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -47,6 +47,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 '../data_views_service/service'; export interface RedirectToOriginProps { input?: LensEmbeddableInput; @@ -107,6 +108,7 @@ export interface LensTopNavMenuProps { topNavMenuEntryGenerators: LensTopNavMenuEntryGenerator[]; initialContext?: VisualizeFieldContext | VisualizeEditorContext; theme$: Observable; + indexPatternService: IndexPatternServiceAPI; } export interface HistoryLocationState { diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts new file mode 100644 index 00000000000000..89d1765bc61afe --- /dev/null +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -0,0 +1,309 @@ +/* + * 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 { isNestedField } from '@kbn/data-views-plugin/common'; +import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { keyBy } from 'lodash'; +import { HttpSetup } from '@kbn/core/public'; +import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types'; +import { documentField } from '../indexpattern_datasource/document_field'; +import { BASE_API_URL, DateRange, ExistingFields } from '../../common'; +import { DataViewsState } from '../state_management'; + +type ErrorHandler = (err: Error) => void; + +/** + * All these functions will be used by the Embeddable instance too, + * therefore keep all these functions pretty raw here and do not use the IndexPatternService + */ + +export function getFieldByNameFactory(newFields: IndexPatternField[]) { + const fieldsLookup = keyBy(newFields, 'name'); + return (name: string) => fieldsLookup[name]; +} + +export function convertDataViewIntoLensIndexPattern( + dataView: DataView, + restrictionRemapper: (name: string) => string +): IndexPattern { + const newFields = dataView.fields + .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) + .map((field): IndexPatternField => { + // Convert the getters on the index pattern service into plain JSON + const base = { + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + meta: dataView.metaFields.includes(field.name), + esTypes: field.esTypes, + scripted: field.scripted, + runtime: Boolean(field.runtimeField), + }; + + // Simplifies tests by hiding optional properties instead of undefined + return base.scripted + ? { + ...base, + lang: field.lang, + script: field.script, + } + : base; + }) + .concat(documentField); + + const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; + if (typeMeta?.aggs) { + const aggs = Object.keys(typeMeta.aggs); + newFields.forEach((field, index) => { + const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; + aggs.forEach((agg) => { + const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; + if (restriction) { + restrictionsObj[restrictionRemapper(agg)] = restriction; + } + }); + if (Object.keys(restrictionsObj).length) { + newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; + } + }); + } + + return { + id: dataView.id!, // id exists for sure because we got index patterns by id + title, + name: name ? name : title, + timeFieldName, + fieldFormatMap: + fieldFormatMap && + Object.fromEntries( + Object.entries(fieldFormatMap).map(([id, format]) => [ + id, + 'toJSON' in format ? format.toJSON() : format, + ]) + ), + fields: newFields, + getFieldByName: getFieldByNameFactory(newFields), + hasRestrictions: !!typeMeta?.aggs, + }; +} + +export async function loadIndexPatternRefs( + indexPatternsService: DataViewsContract +): Promise { + const indexPatterns = await indexPatternsService.getIdsWithTitle(); + + return indexPatterns.sort((a, b) => { + return a.title.localeCompare(b.title); + }); +} + +/** + * Map ES agg names with Lens ones + */ +const renameOperationsMapping: Record = { + avg: 'average', + cardinality: 'unique_count', +}; + +function onRestrictionMapping(agg: string): string { + return agg in renameOperationsMapping ? renameOperationsMapping[agg] : agg; +} + +export async function loadIndexPatterns({ + dataViews, + patterns, + notUsedPatterns, + cache, + onIndexPatternRefresh, +}: { + dataViews: DataViewsContract; + patterns: string[]; + notUsedPatterns?: string[]; + cache: Record; + onIndexPatternRefresh?: () => void; +}) { + const missingIds = patterns.filter((id) => !cache[id]); + + if (missingIds.length === 0) { + return cache; + } + + onIndexPatternRefresh?.(); + + const allIndexPatterns = await Promise.allSettled(missingIds.map((id) => dataViews.get(id))); + // ignore rejected indexpatterns here, they're already handled at the app level + let indexPatterns = allIndexPatterns + .filter( + (response): response is PromiseFulfilledResult => response.status === 'fulfilled' + ) + .map((response) => response.value); + + // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds + if (!indexPatterns.length && notUsedPatterns) { + for (const notUsedPattern of notUsedPatterns) { + const resp = await dataViews.get(notUsedPattern).catch((e) => { + // do nothing + }); + if (resp) { + indexPatterns = [resp]; + } + } + } + + const indexPatternsObject = indexPatterns.reduce( + (acc, indexPattern) => ({ + [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, onRestrictionMapping), + ...acc, + }), + { ...cache } + ); + + return indexPatternsObject; +} + +export async function loadIndexPattern({ + id, + onError, + dataViews, + cache = {}, +}: { + id: string; + onError: ErrorHandler; + dataViews: DataViewsContract; + cache?: IndexPatternMap; +}) { + const indexPatterns = await loadIndexPatterns({ + dataViews, + cache, + patterns: [id], + }); + + if (indexPatterns[id] == null) { + onError(Error('Missing indexpatterns')); + return; + } + + const newIndexPatterns = { + ...cache, + [id]: indexPatterns[id], + }; + return newIndexPatterns; +} + +async function refreshExistingFields({ + dateRange, + fetchJson, + indexPatternList, + dslQuery, +}: { + dateRange: DateRange; + indexPatternList: IndexPattern[]; + fetchJson: HttpSetup['post']; + dslQuery: object; +}) { + try { + const emptinessInfo = await Promise.all( + indexPatternList.map((pattern) => { + if (pattern.hasRestrictions) { + return { + indexPatternTitle: pattern.title, + existingFieldNames: pattern.fields.map((field) => field.name), + }; + } + const body: Record = { + dslQuery, + fromDate: dateRange.fromDate, + toDate: dateRange.toDate, + }; + + if (pattern.timeFieldName) { + body.timeFieldName = pattern.timeFieldName; + } + + return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { + body: JSON.stringify(body), + }) as Promise; + }) + ); + return { result: emptinessInfo, status: 200 }; + } catch (e) { + return { result: undefined, status: e.res?.status as number }; + } +} + +type FieldsPropsFromDataViewsState = Pick< + DataViewsState, + 'existingFields' | 'isFirstExistenceFetch' | 'existenceFetchTimeout' | 'existenceFetchFailed' +>; +export async function syncExistingFields({ + updateIndexPatterns, + isFirstExistenceFetch, + currentIndexPatternTitle, + onNoData, + existingFields, + ...requestOptions +}: { + dateRange: DateRange; + indexPatternList: IndexPattern[]; + existingFields: Record>; + fetchJson: HttpSetup['post']; + updateIndexPatterns: ( + newFieldState: FieldsPropsFromDataViewsState, + options: { applyImmediately: boolean } + ) => void; + isFirstExistenceFetch: boolean; + currentIndexPatternTitle: string; + dslQuery: object; + onNoData?: () => void; +}) { + const { indexPatternList } = requestOptions; + const newExistingFields = { ...existingFields }; + + const { result, status } = await refreshExistingFields(requestOptions); + + if (result) { + if (isFirstExistenceFetch) { + const fieldsCurrentIndexPattern = result.find( + (info) => info.indexPatternTitle === currentIndexPatternTitle + ); + if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { + onNoData?.(); + } + } + + for (const { indexPatternTitle, existingFieldNames } of result) { + newExistingFields[indexPatternTitle] = booleanMap(existingFieldNames); + } + } else { + for (const { title, fields } of indexPatternList) { + newExistingFields[title] = booleanMap(fields.map((field) => field.name)); + } + } + + updateIndexPatterns( + { + isFirstExistenceFetch: status !== 200, + existingFields: newExistingFields, + ...(result + ? {} + : { + existenceFetchFailed: status !== 418, + existenceFetchTimeout: status === 418, + }), + }, + { applyImmediately: true } + ); +} + +function booleanMap(keys: string[]) { + return keys.reduce((acc, key) => { + acc[key] = true; + return acc; + }, {} as Record); +} diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/data_views_service/service.ts new file mode 100644 index 00000000000000..047013525266f1 --- /dev/null +++ b/x-pack/plugins/lens/public/data_views_service/service.ts @@ -0,0 +1,117 @@ +/* + * 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 { DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import type { DateRange } from '../../common'; +import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; +import { + loadIndexPattern, + loadIndexPatternRefs, + loadIndexPatterns, + syncExistingFields, +} from './loader'; +import type { DataViewsState } from '../state_management'; + +interface IndexPatternServiceProps { + core: Pick; + dataViews: DataViewsContract; + uiSettings: IUiSettingsClient; + updateIndexPatterns: ( + newState: Partial, + options?: { applyImmediately: boolean } + ) => void; +} + +/** + * This service is only available for the full editor version + * and it encapsulate all the indexpattern methods and state + * in a single object. + * NOTE: this is not intended to be used with the Embeddable branch + */ +export interface IndexPatternServiceAPI { + /** + * Loads a list of indexPatterns from a list of id (patterns) + * leveraging existing cache. Eventually fallbacks to unused indexPatterns ( notUsedPatterns ) + * @returns IndexPatternMap + */ + loadIndexPatterns: (args: { + patterns: string[]; + notUsedPatterns?: string[]; + cache: IndexPatternMap; + onIndexPatternRefresh?: () => void; + }) => Promise; + /** + * Load indexPatternRefs with title and ids + */ + loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise; + /** + * Ensure an indexPattern is loaded in the cache, usually used in conjuction with a indexPattern change action. + */ + ensureIndexPattern: (args: { + id: string; + cache: IndexPatternMap; + }) => Promise; + /** + * Loads the existingFields map given the current context + */ + refreshExistingFields: (args: { + dateRange: DateRange; + currentIndexPatternTitle: string; + dslQuery: object; + onNoData?: () => void; + existingFields: Record>; + indexPatternList: IndexPattern[]; + isFirstExistenceFetch: boolean; + }) => Promise; + /** + * Retrieves the default indexPattern from the uiSettings + */ + getDefaultIndex: () => string; + + /** + * Update the Lens state cache of indexPatterns + */ + updateIndexPatternsCache: ( + newState: Partial, + options?: { applyImmediately: boolean } + ) => void; +} + +export function createIndexPatternService({ + core, + dataViews, + uiSettings, + updateIndexPatterns, +}: IndexPatternServiceProps): IndexPatternServiceAPI { + const onChangeError = (err: Error) => + core.notifications.toasts.addError(err, { + title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { + defaultMessage: 'Error loading data view', + }), + }); + return { + updateIndexPatternsCache: updateIndexPatterns, + loadIndexPatterns: (args) => { + return loadIndexPatterns({ + dataViews, + ...args, + }); + }, + ensureIndexPattern: (args) => loadIndexPattern({ onError: onChangeError, dataViews, ...args }), + refreshExistingFields: (args) => + syncExistingFields({ + updateIndexPatterns, + fetchJson: core.http.post, + ...args, + }), + loadIndexPatternRefs: async ({ isFullEditor }) => + isFullEditor ? loadIndexPatternRefs(dataViews) : [], + getDefaultIndex: () => uiSettings.get('defaultIndex'), + }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index 32aba270e846b1..cb95c7e9e2c26d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -13,6 +13,7 @@ import { isOperation, DropType, DatasourceLayers, + IndexPatternMap, } from '../../../../types'; import { getCustomDropTarget, @@ -37,6 +38,7 @@ export function DraggableDimensionButton({ layerDatasource, datasourceLayers, registerNewButtonRef, + indexPatterns, }: { layerId: string; groupIndex: number; @@ -53,6 +55,7 @@ export function DraggableDimensionButton({ accessorIndex: number; columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; + indexPatterns: IndexPatternMap; }) { const { dragging } = useContext(DragContext); @@ -73,6 +76,7 @@ export function DraggableDimensionButton({ filterOperations: group.filterOperations, prioritizedOperation: group.prioritizedOperation, }, + indexPatterns, }, sharedDatasource ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index a35366611ae180..30b543bb5f0ae4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -18,6 +18,7 @@ import { DropType, DatasourceLayers, isOperation, + IndexPatternMap, } from '../../../../types'; import { getCustomDropTarget, @@ -111,6 +112,7 @@ export function EmptyDimensionButton({ onClick, onDrop, datasourceLayers, + indexPatterns, }: { layerId: string; groupIndex: number; @@ -121,6 +123,7 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; datasourceLayers: DatasourceLayers; state: unknown; + indexPatterns: IndexPatternMap; }) { const { dragging } = useContext(DragContext); const sharedDatasource = @@ -148,6 +151,7 @@ export function EmptyDimensionButton({ prioritizedOperation: group.prioritizedOperation, isNewColumn: true, }, + indexPatterns, }, sharedDatasource ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index ce4fbbba70236a..72a76a1acd73f1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import React, { useMemo, memo } from 'react'; +import React, { useMemo, memo, useCallback } from 'react'; import { EuiForm } from '@elastic/eui'; import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import { UPDATE_FILTER_REFERENCES_ACTION, UPDATE_FILTER_REFERENCES_TRIGGER, } from '@kbn/unified-search-plugin/public'; +import { changeIndexPattern } from '../../../state_management/lens_slice'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; import { generateId } from '../../../id_generator'; @@ -49,7 +50,7 @@ export function LayerPanels( activeVisualization: Visualization; } ) { - const { activeVisualization, datasourceMap } = props; + const { activeVisualization, datasourceMap, indexPatternService } = props; const { activeDatasourceId, visualization, datasourceStates } = useLensSelector( (state) => state.lens ); @@ -146,6 +147,35 @@ export function LayerPanels( [dispatchLens] ); + const onChangeIndexPattern = useCallback( + async ({ + indexPatternId, + datasourceId, + visualizationId, + layerId, + }: { + indexPatternId: string; + datasourceId?: string; + visualizationId?: string; + layerId?: string; + }) => { + const indexPatterns = await props.indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: props.framePublicAPI.dataViews.indexPatterns, + }); + dispatchLens( + changeIndexPattern({ + indexPatternId, + datasourceIds: datasourceId ? [datasourceId] : [], + visualizationIds: visualizationId ? [visualizationId] : [], + layerId, + dataViews: { indexPatterns }, + }) + ); + }, + [dispatchLens, props.framePublicAPI.dataViews, props.indexPatternService] + ); + return ( {layerIds.map((layerId, layerIndex) => ( @@ -160,6 +190,7 @@ export function LayerPanels( updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateDatasourceAsync={updateDatasourceAsync} + onChangeIndexPattern={onChangeIndexPattern} updateAll={updateAll} isOnlyLayer={ getRemoveOperation( @@ -212,6 +243,7 @@ export function LayerPanels( removeLayerRef(layerId); }} toggleFullscreen={toggleFullscreen} + indexPatternService={indexPatternService} /> ))} void; toggleFullscreen: () => void; onEmptyDimensionAdd: (columnId: string, group: { groupId: string }) => void; + onChangeIndexPattern: (args: { + indexPatternId: string; + layerId: string; + datasourceId?: string; + visualizationId?: string; + }) => void; + indexPatternService: IndexPatternServiceAPI; } ) { const [activeDimension, setActiveDimension] = useState( @@ -86,6 +94,7 @@ export function LayerPanel( updateAll, updateDatasourceAsync, visualizationState, + onChangeIndexPattern, } = props; const datasourceStates = useLensSelector(selectDatasourceStates); @@ -186,6 +195,7 @@ export function LayerPanel( }, dimensionGroups: groups, dropType, + indexPatterns: framePublicAPI.dataViews.indexPatterns, }) ); } @@ -209,15 +219,15 @@ export function LayerPanel( }; }, [ layerDatasource, - layerDatasourceState, setNextFocusedButtonId, + layerDatasourceState, groups, + updateDatasource, + datasourceId, activeVisualization, + updateVisualization, props.visualizationState, framePublicAPI, - updateVisualization, - datasourceId, - updateDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -293,6 +303,8 @@ export function LayerPanel( ] ); + const { dataViews } = props.framePublicAPI; + return ( <>
@@ -304,6 +316,12 @@ export function LayerPanel( layerConfigProps={{ ...layerVisualizationConfigProps, setState: props.updateVisualization, + onChangeIndexPattern: (indexPatternId) => + onChangeIndexPattern({ + indexPatternId, + layerId, + visualizationId: activeVisualization.id, + }), }} activeVisualization={activeVisualization} /> @@ -318,45 +336,64 @@ export function LayerPanel( /> + {(layerDatasource || activeVisualization.renderLayerPanel) && } {layerDatasource && ( - <> - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + + onChangeIndexPattern({ indexPatternId, layerId, datasourceId }), + setState: (updater: unknown) => { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + indexPatterns: dataViews.indexPatterns, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, + frame: framePublicAPI, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame: framePublicAPI, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + )} + {activeVisualization.renderLayerPanel && ( + + onChangeIndexPattern({ + indexPatternId, + layerId, + visualizationId: activeVisualization.id, + }), + }} + /> )} @@ -441,6 +478,7 @@ export function LayerPanel( onDragStart={() => setHideTooltip(true)} onDragEnd={() => setHideTooltip(false)} onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} >
) : ( @@ -541,6 +583,7 @@ export function LayerPanel( }); }} onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} /> ) : null} @@ -601,6 +644,8 @@ export function LayerPanel( paramEditorCustomProps: activeGroup.paramEditorCustomProps, supportFieldFormat: activeGroup.supportFieldFormat !== false, layerType: activeVisualization.getLayerType(layerId, visualizationState), + indexPatterns: dataViews.indexPatterns, + existingFields: dataViews.existingFields, activeData: layerVisualizationConfigProps.activeData, }} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index bafa5b73a1d71c..c4a2d77c30baba 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -6,6 +6,7 @@ */ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { IndexPatternServiceAPI } from '../../../data_views_service/service'; import { Visualization, @@ -20,6 +21,7 @@ export interface ConfigPanelWrapperProps { datasourceMap: DatasourceMap; visualizationMap: VisualizationMap; core: DatasourceDimensionEditorProps['core']; + indexPatternService: IndexPatternServiceAPI; uiActions: UiActionsStart; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 16bc0814a99167..09f8233ef1bca3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -7,14 +7,16 @@ import './data_panel_wrapper.scss'; -import React, { useMemo, memo, useContext, useState, useEffect } from 'react'; +import React, { useMemo, memo, useContext, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +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 } from '../../types'; +import { StateSetter, DatasourceDataPanelProps, DatasourceMap, FramePublicAPI } from '../../types'; import { switchDatasource, useLensDispatch, @@ -26,7 +28,9 @@ import { selectActiveDatasourceId, selectDatasourceStates, } from '../../state_management'; -import { initializeDatasources } from './state_helpers'; +import { initializeSources } from './state_helpers'; +import type { IndexPatternServiceAPI } from '../../data_views_service/service'; +import { changeIndexPattern } from '../../state_management/lens_slice'; interface DataPanelWrapperProps { datasourceMap: DatasourceMap; @@ -34,7 +38,9 @@ interface DataPanelWrapperProps { core: DatasourceDataPanelProps['core']; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; - plugins: { uiActions: UiActionsStart }; + plugins: { uiActions: UiActionsStart; dataViews: DataViewsPublicPluginStart }; + indexPatternService: IndexPatternServiceAPI; + frame: FramePublicAPI; } export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { @@ -64,9 +70,20 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { useEffect(() => { if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) { - initializeDatasources(props.datasourceMap, datasourceStates, undefined, undefined, { - isFullEditor: true, - }).then((result) => { + initializeSources( + { + datasourceMap: props.datasourceMap, + datasourceStates, + dataViews: props.plugins.dataViews, + references: undefined, + initialContext: undefined, + storage: new Storage(localStorage), + defaultIndexPatternId: props.core.uiSettings.get('defaultIndex'), + }, + { + isFullEditor: true, + } + ).then((result) => { const newDatasourceStates = Object.entries(result).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, @@ -80,7 +97,34 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dispatchLens(setState({ datasourceStates: newDatasourceStates })); }); } - }, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]); + }, [ + datasourceStates, + activeDatasourceId, + props.datasourceMap, + dispatchLens, + props.plugins.dataViews, + props.core.uiSettings, + ]); + + const onChangeIndexPattern = useCallback( + async (indexPatternId: string, datasourceId: string, layerId?: string) => { + // reload the indexpattern + const indexPatterns = await props.indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: props.frame.dataViews.indexPatterns, + }); + // now update the state + dispatchLens( + changeIndexPattern({ + dataViews: { indexPatterns }, + datasourceIds: [datasourceId], + indexPatternId, + layerId, + }) + ); + }, + [props.indexPatternService, props.frame.dataViews.indexPatterns, dispatchLens] + ); const datasourceProps: DatasourceDataPanelProps = { ...externalContext, @@ -92,6 +136,9 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, uiActions: props.plugins.uiActions, + onChangeIndexPattern, + indexPatternService: props.indexPatternService, + frame: props.frame, }; const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 0b1847204bd1d5..ed8357ea681ef5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -28,6 +28,7 @@ import { selectVisualization, } from '../../state_management'; import type { LensInspector } from '../../lens_inspector_service'; +import { IndexPatternServiceAPI } from '../../data_views_service/service'; export interface EditorFrameProps { datasourceMap: DatasourceMap; @@ -37,6 +38,7 @@ export interface EditorFrameProps { plugins: EditorFrameStartPlugins; showNoDataPopover: () => void; lensInspector: LensInspector; + indexPatternService: IndexPatternServiceAPI; } export function EditorFrame(props: EditorFrameProps) { @@ -65,7 +67,8 @@ export function EditorFrame(props: EditorFrameProps) { datasourceStates, visualizationMap, datasourceMap[activeDatasourceId], - field + field, + framePublicAPI.dataViews ); }; @@ -96,6 +99,8 @@ export function EditorFrame(props: EditorFrameProps) { showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} + indexPatternService={props.indexPatternService} + frame={framePublicAPI} /> } configPanel={ @@ -105,6 +110,7 @@ export function EditorFrame(props: EditorFrameProps) { datasourceMap={datasourceMap} visualizationMap={visualizationMap} framePublicAPI={framePublicAPI} + indexPatternService={props.indexPatternService} uiActions={props.plugins.uiActions} /> ) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 367d156929714a..f1caa66ede1a2c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -7,11 +7,12 @@ import { Ast, fromExpression } from '@kbn/interpreter'; import { DatasourceStates } from '../../state_management'; -import { Visualization, DatasourceMap, DatasourceLayers } from '../../types'; +import { Visualization, DatasourceMap, DatasourceLayers, IndexPatternMap } from '../../types'; export function getDatasourceExpressionsByLayers( datasourceMap: DatasourceMap, - datasourceStates: DatasourceStates + datasourceStates: DatasourceStates, + indexPatterns: IndexPatternMap ): null | Record { const datasourceExpressions: Array<[string, Ast | string]> = []; @@ -24,7 +25,7 @@ export function getDatasourceExpressionsByLayers( const layers = datasource.getLayers(state); layers.forEach((layerId) => { - const result = datasource.toExpression(state, layerId); + const result = datasource.toExpression(state, layerId, indexPatterns); if (result) { datasourceExpressions.push([layerId, result]); } @@ -52,6 +53,7 @@ export function buildExpression({ datasourceLayers, title, description, + indexPatterns, }: { title?: string; description?: string; @@ -60,6 +62,7 @@ export function buildExpression({ datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; datasourceLayers: DatasourceLayers; + indexPatterns: IndexPatternMap; }): Ast | null { if (visualization === null) { return null; @@ -67,7 +70,8 @@ export function buildExpression({ const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers( datasourceMap, - datasourceStates + datasourceStates, + indexPatterns ); const visualizationExpression = visualization.toExpression( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 1790a18ad12486..6017a7125abaeb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -5,15 +5,21 @@ * 2.0. */ -import { SavedObjectReference } from '@kbn/core/public'; +import { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public'; import { Ast } from '@kbn/interpreter'; import memoizeOne from 'memoize-one'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; +import { difference } from 'lodash'; +import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { Datasource, DatasourceLayers, DatasourceMap, FramePublicAPI, + IndexPattern, + IndexPatternMap, + IndexPatternRef, InitializationOptions, Visualization, VisualizationMap, @@ -22,39 +28,186 @@ import { import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; import { getActiveDatasourceIdFromDoc } from '../../utils'; -import { ErrorMessage } from '../types'; +import type { ErrorMessage } from '../types'; import { getMissingCurrentDatasource, getMissingIndexPatterns, getMissingVisualizationTypeError, getUnknownVisualizationTypeError, } from '../error_helper'; -import { DatasourceStates } from '../../state_management'; +import type { DatasourceStates, DataViewsState } from '../../state_management'; +import { readFromStorage } from '../../settings_storage'; +import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader'; -export async function initializeDatasources( - datasourceMap: DatasourceMap, - datasourceStates: DatasourceStates, +function getIndexPatterns( references?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, - options?: InitializationOptions + initialId?: string ) { - const states: DatasourceStates = {}; - await Promise.all( - Object.entries(datasourceMap).map(([datasourceId, datasource]) => { - if (datasourceStates[datasourceId]) { - return datasource - .initialize( - datasourceStates[datasourceId].state || undefined, - references, - initialContext, - options - ) - .then((datasourceState) => { - states[datasourceId] = { isLoading: false, state: datasourceState }; - }); + const indexPatternIds = []; + if (initialContext) { + if ('isVisualizeAction' in initialContext) { + for (const { indexPatternId } of initialContext.layers) { + indexPatternIds.push(indexPatternId); + } + } else { + indexPatternIds.push(initialContext.indexPatternId); + } + } else { + // use the initialId only when no context is passed over + if (initialId) { + indexPatternIds.push(initialId); + } + } + if (references) { + for (const reference of references) { + if (reference.type === 'index-pattern') { + indexPatternIds.push(reference.id); } - }) + } + } + return [...new Set(indexPatternIds)]; +} + +const getLastUsedIndexPatternId = ( + storage: IStorageWrapper, + indexPatternRefs: IndexPatternRef[] +) => { + const indexPattern = readFromStorage(storage, 'indexPatternId'); + return indexPattern && indexPatternRefs.find((i) => i.id === indexPattern)?.id; +}; + +export async function initializeDataViews( + { + dataViews, + datasourceMap, + datasourceStates, + storage, + defaultIndexPatternId, + references, + initialContext, + }: { + dataViews: DataViewsContract; + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + defaultIndexPatternId: string; + storage: IStorageWrapper; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + }, + options?: InitializationOptions +) { + const { isFullEditor } = options ?? {}; + // make it explicit or TS will infer never[] and break few lines down + const indexPatternRefs: IndexPatternRef[] = await (isFullEditor + ? loadIndexPatternRefs(dataViews) + : []); + + // if no state is available, use the fallbackId + const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; + const initialId = + !initialContext && + Object.keys(datasourceMap).every((datasourceId) => !datasourceStates[datasourceId]?.state) + ? fallbackId + : undefined; + + const usedIndexPatterns = getIndexPatterns(references, initialContext, initialId); + + // load them + const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); + + const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedIndexPatterns); + + const indexPatterns = await loadIndexPatterns({ + dataViews, + patterns: usedIndexPatterns, + notUsedPatterns, + cache: {}, + }); + + return { indexPatternRefs, indexPatterns }; +} + +/** + * This function composes both initializeDataViews & initializeDatasources into a single call + */ +export async function initializeSources( + { + dataViews, + datasourceMap, + datasourceStates, + storage, + defaultIndexPatternId, + references, + initialContext, + }: { + dataViews: DataViewsContract; + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + defaultIndexPatternId: string; + storage: IStorageWrapper; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + }, + options?: InitializationOptions +) { + const { indexPatternRefs, indexPatterns } = await initializeDataViews( + { + datasourceMap, + datasourceStates, + initialContext, + dataViews, + storage, + defaultIndexPatternId, + references, + }, + { + isFullEditor: true, + } ); + return { + indexPatterns, + indexPatternRefs, + states: initializeDatasources({ + datasourceMap, + datasourceStates, + initialContext, + indexPatternRefs, + indexPatterns, + }), + }; +} + +export function initializeDatasources({ + datasourceMap, + datasourceStates, + indexPatternRefs, + indexPatterns, + references, + initialContext, +}: { + datasourceMap: DatasourceMap; + datasourceStates: DatasourceStates; + indexPatterns: Record; + indexPatternRefs: IndexPatternRef[]; + references?: SavedObjectReference[]; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; +}) { + // init datasources + const states: DatasourceStates = {}; + for (const [datasourceId, datasource] of Object.entries(datasourceMap)) { + if (datasourceStates[datasourceId]) { + const state = datasource.initialize( + datasourceStates[datasourceId].state || undefined, + references, + initialContext, + indexPatternRefs, + indexPatterns + ); + states[datasourceId] = { isLoading: false, state }; + } + } return states; } @@ -74,6 +227,8 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ state: datasourceState, layerId: layer, + // @TODO + indexPatterns: {}, }); }); }); @@ -83,7 +238,12 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( export async function persistedStateToExpression( datasourceMap: DatasourceMap, visualizations: VisualizationMap, - doc: Document + doc: Document, + services: { + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; + dataViews: DataViewsContract; + } ): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> { const { state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates }, @@ -105,18 +265,30 @@ export async function persistedStateToExpression( }; } const visualization = visualizations[visualizationType!]; - const datasourceStates = await initializeDatasources( - datasourceMap, - Object.fromEntries( - Object.entries(persistedDatasourceStates).map(([id, state]) => [ - id, - { isLoading: false, state }, - ]) - ), - references, - undefined, + const datasourceStatesFromSO = Object.fromEntries( + Object.entries(persistedDatasourceStates).map(([id, state]) => [ + id, + { isLoading: false, state }, + ]) + ); + const { indexPatterns, indexPatternRefs } = await initializeDataViews( + { + datasourceMap, + datasourceStates: datasourceStatesFromSO, + references, + dataViews: services.dataViews, + storage: services.storage, + defaultIndexPatternId: services.uiSettings.get('defaultIndex'), + }, { isFullEditor: false } ); + const datasourceStates = initializeDatasources({ + datasourceMap, + datasourceStates: datasourceStatesFromSO, + references, + indexPatterns, + indexPatternRefs, + }); const datasourceLayers = getDatasourceLayers(datasourceStates, datasourceMap); @@ -130,7 +302,8 @@ export async function persistedStateToExpression( const indexPatternValidation = validateRequiredIndexPatterns( datasourceMap[datasourceId], - datasourceStates[datasourceId] + datasourceStates[datasourceId], + indexPatterns ); if (indexPatternValidation) { @@ -145,7 +318,7 @@ export async function persistedStateToExpression( datasourceStates[datasourceId].state, visualization, visualizationState, - { datasourceLayers } + { datasourceLayers, dataViews: { indexPatterns } as DataViewsState } ); return { @@ -157,6 +330,7 @@ export async function persistedStateToExpression( datasourceMap, datasourceStates, datasourceLayers, + indexPatterns, }), errors: validationResult, }; @@ -164,12 +338,13 @@ export async function persistedStateToExpression( export function getMissingIndexPattern( currentDatasource: Datasource | null, - currentDatasourceState: { state: unknown } | null + currentDatasourceState: { state: unknown } | null, + indexPatterns: IndexPatternMap ) { if (currentDatasourceState == null || currentDatasource == null) { return []; } - const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state); + const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state, indexPatterns); if (!missingIds.length) { return []; } @@ -178,9 +353,14 @@ export function getMissingIndexPattern( const validateRequiredIndexPatterns = ( currentDatasource: Datasource, - currentDatasourceState: { state: unknown } | null + currentDatasourceState: { state: unknown } | null, + indexPatterns: IndexPatternMap ): ErrorMessage[] | undefined => { - const missingIds = getMissingIndexPattern(currentDatasource, currentDatasourceState); + const missingIds = getMissingIndexPattern( + currentDatasource, + currentDatasourceState, + indexPatterns + ); if (!missingIds.length) { return; @@ -194,14 +374,14 @@ export const validateDatasourceAndVisualization = ( currentDatasourceState: unknown | null, currentVisualization: Visualization | null, currentVisualizationState: unknown | undefined, - frameAPI: Pick + { datasourceLayers, dataViews }: Pick ): ErrorMessage[] | undefined => { const datasourceValidationErrors = currentDatasourceState - ? currentDataSource?.getErrorMessages(currentDatasourceState) + ? currentDataSource?.getErrorMessages(currentDatasourceState, dataViews.indexPatterns) : undefined; const visualizationValidationErrors = currentVisualizationState - ? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI.datasourceLayers) + ? currentVisualization?.getErrorMessages(currentVisualizationState, datasourceLayers) : undefined; if (datasourceValidationErrors?.length || visualizationValidationErrors?.length) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index d67dc2284062e5..e9f8445a868199 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -28,6 +28,7 @@ import { DatasourceStates, VisualizationState, applyChanges, + DataViewsState, } from '../../state_management'; /** @@ -48,6 +49,7 @@ export function getSuggestions({ field, visualizeTriggerFieldContext, activeData, + dataViews, mainPalette, }: { datasourceMap: DatasourceMap; @@ -59,6 +61,7 @@ export function getSuggestions({ field?: unknown; visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; activeData?: Record; + dataViews: DataViewsState; mainPalette?: PaletteOutput; }): Suggestion[] { const datasources = Object.entries(datasourceMap).filter( @@ -91,25 +94,29 @@ export function getSuggestions({ if ('isVisualizeAction' in visualizeTriggerFieldContext) { dataSourceSuggestions = datasource.getDatasourceSuggestionsForVisualizeCharts( datasourceState, - visualizeTriggerFieldContext.layers + visualizeTriggerFieldContext.layers, + dataViews.indexPatterns ); } else { // used for navigating from Discover to Lens dataSourceSuggestions = datasource.getDatasourceSuggestionsForVisualizeField( datasourceState, visualizeTriggerFieldContext.indexPatternId, - visualizeTriggerFieldContext.fieldName + visualizeTriggerFieldContext.fieldName, + dataViews.indexPatterns ); } } else if (field) { dataSourceSuggestions = datasource.getDatasourceSuggestionsForField( datasourceState, field, - (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]) // a field dragged to workspace should added to data layer + (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]), // a field dragged to workspace should added to data layer + dataViews.indexPatterns ); } else { dataSourceSuggestions = datasource.getDatasourceSuggestionsFromCurrentState( datasourceState, + dataViews.indexPatterns, (layerId) => isLayerSupportedByVisualization(layerId, [layerTypes.DATA]), activeData ); @@ -162,12 +169,14 @@ export function getVisualizeFieldSuggestions({ datasourceStates, visualizationMap, visualizeTriggerFieldContext, + dataViews, }: { datasourceMap: DatasourceMap; datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; subVisualizationId?: string; visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; + dataViews: DataViewsState; }): Suggestion | undefined { const activeVisualization = visualizationMap?.[Object.keys(visualizationMap)[0]] || null; const suggestions = getSuggestions({ @@ -177,6 +186,7 @@ export function getVisualizeFieldSuggestions({ activeVisualization, visualizationState: undefined, visualizeTriggerFieldContext, + dataViews, }); if (visualizeTriggerFieldContext && 'isVisualizeAction' in visualizeTriggerFieldContext) { @@ -266,7 +276,8 @@ export function getTopSuggestionForField( datasourceStates: DatasourceStates, visualizationMap: Record>, datasource: Datasource, - field: DragDropIdentifier + field: DragDropIdentifier, + dataViews: DataViewsState ) { const hasData = Object.values(datasourceLayers).some( (datasourceLayer) => datasourceLayer.getTableSpec().length > 0 @@ -288,6 +299,7 @@ export function getTopSuggestionForField( visualizationState: visualization.state, field, mainPalette, + dataViews, }); return ( suggestions.find((s) => s.visualizationId === visualization.activeId) || diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 67403b93d7b8cd..7400aa6dd8fab0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -209,7 +209,8 @@ export function SuggestionPanel({ const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId ? datasourceStates[activeDatasourceId] : null + activeDatasourceId ? datasourceStates[activeDatasourceId] : null, + frame.dataViews.indexPatterns ); const { suggestions, currentStateExpression, currentStateError } = useMemo(() => { const newSuggestions = missingIndexPatterns.length @@ -223,6 +224,7 @@ export function SuggestionPanel({ : undefined, visualizationState: currentVisualization.state, activeData, + dataViews: frame.dataViews, }) .filter( ({ @@ -240,6 +242,7 @@ export function SuggestionPanel({ visualizationMap[visualizationId], suggestionVisualizationState, { + dataViews: frame.dataViews, datasourceLayers: getDatasourceLayers( suggestionDatasourceId ? { @@ -511,6 +514,7 @@ function getPreviewExpression( updatedLayerApis[layerId] = datasource.getPublicAPI({ layerId, state: datasourceState, + indexPatterns: frame.dataViews.indexPatterns, }); } }); @@ -518,7 +522,8 @@ function getPreviewExpression( const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers( datasources, - datasourceStates + datasourceStates, + frame.dataViews.indexPatterns ); return visualization.toPreviewExpression( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 8c41fc5da8aaef..8556d6fdad7eb4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -496,6 +496,7 @@ function getTopSuggestion( subVisualizationId, activeData: props.framePublicAPI.activeData, mainPalette, + dataViews: props.framePublicAPI.dataViews, }); const suggestions = unfilteredSuggestions.filter((suggestion) => { // don't use extended versions of current data table on switching between visualizations diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index e6abf77b522068..9d23dd28b37075 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -189,6 +189,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ datasourceLayers, }; + const { dataViews } = framePublicAPI; const onRender$ = useCallback(() => { if (renderDeps.current) { const datasourceEvents = Object.values(renderDeps.current.datasourceMap).reduce( @@ -242,7 +243,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId ? datasourceStates[activeDatasourceId] : null + activeDatasourceId ? datasourceStates[activeDatasourceId] : null, + dataViews.indexPatterns ); const missingRefsErrors = missingIndexPatterns.length @@ -289,6 +291,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ datasourceMap, datasourceStates, datasourceLayers, + indexPatterns: dataViews.indexPatterns, }); if (ast) { @@ -330,6 +333,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ missingRefsErrors.length, unknownVisError, visualization.activeId, + dataViews.indexPatterns, ]); useEffect(() => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index dea3f7c43a4a82..8c7e2378bd354f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -6,14 +6,23 @@ */ import React from 'react'; -import { CoreStart } from '@kbn/core/public'; +import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public'; import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + DataViewsContract, +} from '@kbn/data-plugin/public'; +import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import { + DataViewsPublicPluginSetup, + DataViewsPublicPluginStart, +} from '@kbn/data-views-plugin/public'; import { Document } from '../persistence/saved_object_store'; import { Datasource, @@ -29,6 +38,7 @@ export interface EditorFrameSetupPlugins { expressions: ExpressionsSetup; charts: ChartsPluginSetup; usageCollection?: UsageCollectionSetup; + dataViews: DataViewsPublicPluginSetup; } export interface EditorFrameStartPlugins { @@ -38,6 +48,13 @@ export interface EditorFrameStartPlugins { dashboard?: DashboardStart; expressions: ExpressionsStart; charts: ChartsPluginSetup; + dataViews: DataViewsPublicPluginStart; +} + +export interface EditorFramePlugins { + dataViews: DataViewsContract; + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; } async function collectAsyncDefinitions( @@ -67,7 +84,7 @@ export class EditorFrameService { * This is an asynchronous process. * @param doc parsed Lens saved object */ - public documentToExpression = async (doc: Document) => { + public documentToExpression = async (doc: Document, services: EditorFramePlugins) => { const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ this.loadDatasources(), this.loadVisualizations(), @@ -75,7 +92,7 @@ export class EditorFrameService { const { persistedStateToExpression } = await import('../async_services'); - return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); + return persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc, services); }; public setup(): EditorFrameSetup { @@ -99,7 +116,7 @@ export class EditorFrameService { const { EditorFrame } = await import('../async_services'); return { - EditorFrameContainer: ({ showNoDataPopover, lensInspector }) => { + EditorFrameContainer: ({ showNoDataPopover, lensInspector, indexPatternService }) => { return (
; diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 69e8d1ee9d18f2..de4434a17f05ab 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -121,7 +121,7 @@ export interface LensEmbeddableDeps { injectFilterReferences: FilterManager['inject']; visualizationMap: VisualizationMap; datasourceMap: DatasourceMap; - indexPatternService: DataViewsContract; + dataViews: DataViewsContract; expressionRenderer: ReactExpressionRendererType; timefilter: TimefilterContract; basePath: IBasePath; @@ -808,7 +808,7 @@ export class Embeddable const { indexPatterns } = await getIndexPatternsObjects( this.savedVis?.references.map(({ id }) => id) || [], - this.deps.indexPatternService + this.deps.dataViews ); this.indexPatterns = uniqBy(indexPatterns, 'id'); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 07c5a5bdf5ef7d..1b6effea993417 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -38,7 +38,7 @@ export interface LensEmbeddableStartServices { attributeService: LensAttributeService; capabilities: RecursiveReadonly; expressionRenderer: ReactExpressionRendererType; - indexPatternService: DataViewsContract; + dataViews: DataViewsContract; uiActions?: UiActionsStart; usageCollection?: UsageCollectionSetup; documentToExpression: ( @@ -102,7 +102,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { uiActions, coreHttp, attributeService, - indexPatternService, + dataViews, capabilities, usageCollection, theme, @@ -117,7 +117,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { { attributeService, data, - indexPatternService, + dataViews, timefilter, inspector, expressionRenderer, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index d907c25cbaf148..b46155edf05037 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -34,21 +34,26 @@ import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; -import type { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; -import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import type { + DatasourceDataPanelProps, + DataType, + FramePublicAPI, IndexPattern, - IndexPatternPrivateState, IndexPatternField, - IndexPatternRef, -} from './types'; -import { loadIndexPatterns, syncExistingFields } from './loader'; -import { fieldExists } from './pure_helpers'; + StateSetter, +} from '../types'; +import { ChildDragDropProvider, DragContextState } from '../drag_drop'; +import type { IndexPatternPrivateState } from './types'; import { Loader } from '../loader'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { FieldGroups, FieldList } from './field_list'; +import { fieldContainsData, fieldExists } from '../shared_components'; +import { IndexPatternServiceAPI } from '../data_views_service/service'; -export type Props = Omit, 'core'> & { +export type Props = Omit< + DatasourceDataPanelProps, + 'core' | 'onChangeIndexPattern' +> & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; @@ -60,6 +65,9 @@ export type Props = Omit, 'co charts: ChartsPluginSetup; core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + frame: FramePublicAPI; + indexPatternService: IndexPatternServiceAPI; + onIndexPatternRefresh: () => void; }; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { @@ -134,40 +142,26 @@ export function IndexPatternDataPanel({ dropOntoWorkspace, hasSuggestionForField, uiActions, + indexPatternService, + frame, + onIndexPatternRefresh, }: Props) { - const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; + const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } = + frame.dataViews; + const { currentIndexPatternId } = state; const onChangeIndexPattern = useCallback( (id: string) => changeIndexPattern(id, state, setState), [state, setState, changeIndexPattern] ); - const onUpdateIndexPattern = useCallback( - (indexPattern: IndexPattern) => { - setState((prevState) => ({ - ...prevState, - indexPatterns: { - ...prevState.indexPatterns, - [indexPattern.id]: indexPattern, - }, - })); - }, - [setState] - ); - const indexPatternList = uniq( Object.values(state.layers) .map((l) => l.indexPatternId) .concat(currentIndexPatternId) ) .filter((id) => !!indexPatterns[id]) - .sort((a, b) => a.localeCompare(b)) - .map((id) => ({ - id, - title: indexPatterns[id].title, - timeFieldName: indexPatterns[id].timeFieldName, - fields: indexPatterns[id].fields, - hasRestrictions: indexPatterns[id].hasRestrictions, - })); + .sort() + .map((id) => indexPatterns[id]); const dslQuery = buildSafeEsQuery( indexPatterns[currentIndexPatternId], @@ -180,15 +174,14 @@ export function IndexPatternDataPanel({ <> - syncExistingFields({ + indexPatternService.refreshExistingFields({ dateRange, - setState, - isFirstExistenceFetch: state.isFirstExistenceFetch, currentIndexPatternTitle: indexPatterns[currentIndexPatternId]?.title || '', - showNoDataPopover, - indexPatterns: indexPatternList, - fetchJson: core.http.post, + onNoData: showNoDataPopover, dslQuery, + indexPatternList, + isFirstExistenceFetch, + existingFields, }) } loadDeps={[ @@ -197,7 +190,6 @@ export function IndexPatternDataPanel({ dateRange.fromDate, dateRange.toDate, indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','), - state.indexPatterns, ]} /> @@ -229,8 +221,6 @@ export function IndexPatternDataPanel({ ) : ( )} @@ -285,42 +274,35 @@ const fieldSearchDescriptionId = htmlId(); export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ currentIndexPatternId, - indexPatternRefs, - indexPatterns, - existenceFetchFailed, - existenceFetchTimeout, query, dateRange, filters, dragDropContext, onChangeIndexPattern, - onUpdateIndexPattern, core, data, dataViews, fieldFormats, indexPatternFieldEditor, - existingFields, charts, dropOntoWorkspace, hasSuggestionForField, uiActions, + indexPatternService, + frame, + onIndexPatternRefresh, }: Omit & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; dragDropContext: DragContextState; onChangeIndexPattern: (newId: string) => void; - onUpdateIndexPattern: (indexPattern: IndexPattern) => void; - existingFields: IndexPatternPrivateState['existingFields']; charts: ChartsPluginSetup; + frame: FramePublicAPI; indexPatternFieldEditor: IndexPatternFieldEditorStart; - existenceFetchFailed?: boolean; - existenceFetchTimeout?: boolean; + onIndexPatternRefresh: () => void; }) { const [localState, setLocalState] = useState({ nameFilter: '', @@ -330,13 +312,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ isEmptyAccordionOpen: false, isMetaAccordionOpen: false, }); + const { existenceFetchFailed, existenceFetchTimeout, indexPatterns, existingFields } = + frame.dataViews; const currentIndexPattern = indexPatterns[currentIndexPatternId]; + const existingFieldsForIndexPattern = existingFields[currentIndexPattern?.title]; const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER); const allFields = visualizeGeoFieldTrigger ? currentIndexPattern.fields : currentIndexPattern.fields.filter(({ type }) => type !== 'geo_point' && type !== 'geo_shape'); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); - const hasSyncedExistingFields = existingFields[currentIndexPattern.title]; const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( (type) => type in fieldTypeNames ); @@ -349,9 +333,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ const unfilteredFieldGroups: FieldGroups = useMemo(() => { const containsData = (field: IndexPatternField) => { const overallField = currentIndexPattern.getFieldByName(field.name); - return ( - overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name) + overallField && + existingFieldsForIndexPattern && + fieldExists(existingFieldsForIndexPattern, overallField.name) ); }; @@ -463,7 +448,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ filters.length, existenceFetchTimeout, currentIndexPattern, - existingFields, + existingFieldsForIndexPattern, ]); const fieldGroups: FieldGroups = useMemo(() => { @@ -490,10 +475,9 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]); const checkFieldExists = useCallback( - (field) => - field.type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field.name), - [existingFields, currentIndexPattern.title] + (field: IndexPatternField) => + fieldContainsData(field.name, currentIndexPattern, existingFieldsForIndexPattern), + [currentIndexPattern, existingFieldsForIndexPattern] ); const { nameFilter, typeFilter } = localState; @@ -518,15 +502,24 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }, []); const refreshFieldList = useCallback(async () => { - const newlyMappedIndexPattern = await loadIndexPatterns({ - indexPatternsService: dataViews, - cache: {}, + const newlyMappedIndexPattern = await indexPatternService.loadIndexPatterns({ patterns: [currentIndexPattern.id], + cache: {}, + onIndexPatternRefresh, + }); + indexPatternService.updateIndexPatternsCache({ + ...frame.dataViews.indexPatterns, + [currentIndexPattern.id]: newlyMappedIndexPattern[currentIndexPattern.id], }); - onUpdateIndexPattern(newlyMappedIndexPattern[currentIndexPattern.id]); // start a new session so all charts are refreshed data.search.session.start(); - }, [data, dataViews, currentIndexPattern, onUpdateIndexPattern]); + }, [ + indexPatternService, + currentIndexPattern.id, + onIndexPatternRefresh, + frame.dataViews.indexPatterns, + data.search.session, + ]); const editField = useMemo( () => @@ -709,7 +702,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ exists={checkFieldExists} fieldProps={fieldProps} fieldGroups={fieldGroups} - hasSyncedExistingFields={!!hasSyncedExistingFields} + hasSyncedExistingFields={!!existingFieldsForIndexPattern} filter={filter} currentIndexPatternId={currentIndexPatternId} existenceFetchFailed={existenceFetchFailed} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index fbecfeed0f3214..f876536ebba43b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { GenericIndexPatternColumn } from '../indexpattern'; -import { IndexPatternField } from '../types'; +import { IndexPatternField } from '../../editor_frame_service/types'; const fieldMap: Record = { a: { displayName: 'a' } as IndexPatternField, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index e1884808910d14..87f9c64075ef77 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -36,7 +36,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField } from '../pure_utils'; import { fieldIsInvalid } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; -import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types'; +import type { IndexPatternLayer } from '../types'; import { FormatSelector } from './format_selector'; import { ReferenceEditor } from './reference_editor'; import { TimeScaling } from './time_scaling'; @@ -61,8 +61,8 @@ import { NameInput } from '../../shared_components'; import { ParamEditorProps } from '../operations/definitions'; import { WrappingHelpPopover } from '../help_popover'; import { isColumn } from '../operations/definitions/helpers'; -import { FieldChoiceWithOperationType } from './field_select'; -import { documentField } from '../document_field'; +import type { FieldChoiceWithOperationType } from './field_select'; +import type { IndexPattern, IndexPatternField } from '../../types'; export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: GenericIndexPatternColumn; @@ -302,7 +302,7 @@ export function DimensionEditor(props: DimensionEditorProps) { disabledStatus: definition.getDisabledStatus && definition.getDisabledStatus( - state.indexPatterns[state.currentIndexPatternId], + props.indexPatterns[state.currentIndexPatternId], state.layers[layerId], layerType ), @@ -538,7 +538,7 @@ export function DimensionEditor(props: DimensionEditorProps) { setIsCloseable, paramEditorCustomProps, ReferenceEditor, - existingFields: state.existingFields, + existingFields: props.existingFields, ...services, }; @@ -640,7 +640,7 @@ export function DimensionEditor(props: DimensionEditorProps) { }} validation={validation} currentIndexPattern={currentIndexPattern} - existingFields={state.existingFields} + existingFields={props.existingFields} selectionStyle={selectedOperationDefinition.selectionStyle} dateRange={dateRange} labelAppend={selectedOperationDefinition?.getHelpMessage?.({ @@ -666,7 +666,7 @@ export function DimensionEditor(props: DimensionEditorProps) { selectedColumn={selectedColumn as FieldBasedIndexPatternColumn} columnId={columnId} indexPattern={currentIndexPattern} - existingFields={state.existingFields} + existingFields={props.existingFields} operationSupportMatrix={operationSupportMatrix} updateLayer={(newLayer) => { if (temporaryQuickFunction) { @@ -697,7 +697,7 @@ export function DimensionEditor(props: DimensionEditorProps) { const customParamEditor = ParamEditor ? ( <> { dropType?: DropType; source: T; target: DataViewDragDropOperation; + indexPatterns: IndexPatternMap; } export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const { target, source, dropType, state } = props; + const { target, source, dropType, state, indexPatterns } = props; if (isDraggedField(source) && isFieldDropType(dropType)) { return onFieldDrop( @@ -52,9 +54,10 @@ export function onDrop(props: DatasourceDimensionDropHandlerProps ['field_add', 'field_replace', 'field_combine'].includes(dropType); function onFieldDrop(props: DropHandlerProps, shouldAddField?: boolean) { - const { setState, state, source, target, dimensionGroups } = props; + const { setState, state, source, target, dimensionGroups, indexPatterns } = props; const prioritizedOperation = dimensionGroups.find( (g) => g.groupId === target.groupId )?.prioritizedOperation; const layer = state.layers[target.layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const targetColumn = layer.columns[target.columnId]; const newOperation = shouldAddField ? targetColumn.operationType @@ -233,13 +237,20 @@ function onReorder({ } function onMoveIncompatible( - { setState, state, source, dimensionGroups, target }: DropHandlerProps, + { + setState, + state, + source, + dimensionGroups, + target, + indexPatterns, + }: DropHandlerProps, shouldDeleteSource?: boolean ) { const targetLayer = state.layers[target.layerId]; const targetColumn = targetLayer.columns[target.columnId] || null; const sourceLayer = state.layers[source.layerId]; - const indexPattern = state.indexPatterns[sourceLayer.indexPatternId]; + const indexPattern = indexPatterns[sourceLayer.indexPatternId]; const sourceColumn = sourceLayer.columns[source.columnId]; const sourceField = getField(sourceColumn, indexPattern); const newOperation = getNewOperation(sourceField, target.filterOperations, targetColumn); @@ -304,10 +315,11 @@ function onSwapIncompatible({ source, dimensionGroups, target, + indexPatterns, }: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; const sourceLayer = state.layers[source.layerId]; - const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const indexPattern = indexPatterns[targetLayer.indexPatternId]; const sourceColumn = sourceLayer.columns[source.columnId]; const targetColumn = targetLayer.columns[target.columnId]; @@ -453,11 +465,12 @@ function onCombine({ source, target, dimensionGroups, + indexPatterns, }: DropHandlerProps) { const targetLayer = state.layers[target.layerId]; const targetColumn = targetLayer.columns[target.columnId]; const targetField = getField(targetColumn, target.dataView); - const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const indexPattern = indexPatterns[targetLayer.indexPatternId]; const sourceLayer = state.layers[source.layerId]; const sourceColumn = sourceLayer.columns[source.columnId]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 6ed035afbc4f32..8aa4175ad3f020 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -10,11 +10,11 @@ import { partition } from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; -import { fieldExists } from '../pure_helpers'; import type { OperationType } from '../indexpattern'; import type { OperationSupportMatrix } from './operation_support'; -import type { IndexPattern, IndexPatternPrivateState } from '../types'; import { FieldOption, FieldOptionValue, FieldPicker } from '../../shared_components/field_picker'; +import { fieldContainsData } from '../../shared_components'; +import type { ExistingFieldsMap, IndexPattern } from '../../types'; export type FieldChoiceWithOperationType = FieldOptionValue & { operationType: OperationType; @@ -28,7 +28,7 @@ export interface FieldSelectProps extends EuiComboBoxProps void; onDeleteColumn?: () => void; - existingFields: IndexPatternPrivateState['existingFields']; + existingFields: ExistingFieldsMap[string]; fieldIsInvalid: boolean; markAllFieldsCompatible?: boolean; 'data-test-subj'?: string; @@ -61,9 +61,10 @@ export function FieldSelect({ fields, (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); - const containsData = (field: string) => - currentIndexPattern.getFieldByName(field)?.type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field); + + function containsData(field: string) { + return fieldContainsData(field, currentIndexPattern, existingFields); + } function fieldNamesToOptions(items: string[]) { return items diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 2f703547219ec3..43d9770deb2281 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -6,7 +6,7 @@ */ import memoizeOne from 'memoize-one'; -import { DatasourceDimensionDropProps, OperationMetadata } from '../../types'; +import { DatasourceDimensionDropProps, IndexPatternMap, OperationMetadata } from '../../types'; import { OperationType } from '../indexpattern'; import { memoizedGetAvailableOperationsByMetadata, OperationFieldTuple } from '../operations'; import { IndexPatternPrivateState } from '../types'; @@ -20,7 +20,7 @@ export interface OperationSupportMatrix { type Props = Pick< DatasourceDimensionDropProps['target'], 'layerId' | 'columnId' | 'filterOperations' -> & { state: IndexPatternPrivateState }; +> & { state: IndexPatternPrivateState; indexPatterns: IndexPatternMap }; function computeOperationMatrix( operationsByMetadata: Array<{ @@ -67,7 +67,7 @@ const memoizedComputeOperationsMatrix = memoizeOne(computeOperationMatrix); // TODO: the support matrix should be available outside of the dimension panel export const getOperationSupportMatrix = (props: Props): OperationSupportMatrix => { const layerId = props.layerId; - const currentIndexPattern = props.state.indexPatterns[props.state.layers[layerId].indexPatternId]; + const currentIndexPattern = props.indexPatterns[props.state.layers[layerId].indexPatternId]; const operationsByMetadata = memoizedGetAvailableOperationsByMetadata(currentIndexPattern); return memoizedComputeOperationsMatrix(operationsByMetadata, props.filterOperations); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 316f9c87c7c290..02ca96e147605a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -44,9 +44,9 @@ import { KBN_FIELD_TYPES, ES_FIELD_TYPES, getEsQueryConfig } from '@kbn/data-plu import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { DragDrop, DragDropIdentifier } from '../drag_drop'; -import { DatasourceDataPanelProps, DataType } from '../types'; +import type { DatasourceDataPanelProps, DataType, IndexPattern, IndexPatternField } from '../types'; import { BucketedAggregation, DOCUMENT_FIELD_NAME, FieldStatsResponse } from '../../common'; -import { IndexPattern, IndexPatternField, DraggedField } from './types'; +import { DraggedField } from './types'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { VisualizeGeoFieldButton } from './visualize_geo_field_button'; import { getVisualizeGeoFieldMessage } from '../utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index efaaf86217f060..18da8488db212f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -6,15 +6,14 @@ */ import './field_list.scss'; -import { throttle } from 'lodash'; +import { partition, throttle } from 'lodash'; import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { FieldItem } from './field_item'; import { NoFieldsCallout } from './no_fields_callout'; -import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; -import { DatasourceDataPanelProps } from '../types'; +import type { DatasourceDataPanelProps, IndexPatternField } from '../types'; const PAGINATION_SIZE = 50; export type FieldGroups = Record< @@ -76,13 +75,15 @@ export const FieldList = React.memo(function FieldList({ removeField?: (name: string) => void; uiActions: UiActionsStart; }) { + const [fieldGroupsToShow, fieldFroupsToCollapse] = partition( + Object.entries(fieldGroups), + ([, { showInAccordion }]) => showInAccordion + ); const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); const [accordionState, setAccordionState] = useState>>(() => Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) + fieldGroupsToShow.map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) ) ); @@ -116,18 +117,16 @@ export const FieldList = React.memo(function FieldList({ const paginatedFields = useMemo(() => { let remainingItems = pageSize; return Object.fromEntries( - Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup]) => { - if (!accordionState[key] || remainingItems <= 0) { - return [key, []]; - } - const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); - remainingItems = remainingItems - slicedFieldList.length; - return [key, slicedFieldList]; - }) + fieldGroupsToShow.map(([key, fieldGroup]) => { + if (!accordionState[key] || remainingItems <= 0) { + return [key, []]; + } + const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); + remainingItems = remainingItems - slicedFieldList.length; + return [key, slicedFieldList]; + }) ); - }, [pageSize, fieldGroups, accordionState]); + }, [pageSize, fieldGroupsToShow, accordionState]); return (
    - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => !showInAccordion) - .flatMap(([, { fields }]) => - fields.map((field, index) => ( - - )) - )} -
- - {Object.entries(fieldGroups) - .filter(([, { showInAccordion }]) => showInAccordion) - .map(([key, fieldGroup], index) => ( - - + fields.map((field, index) => ( + { - setAccordionState((s) => ({ - ...s, - [key]: open, - })); - const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { - ...accordionState, - [key]: open, - }); - setPageSize( - Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) - ); - }} - showExistenceFetchError={existenceFetchFailed} - showExistenceFetchTimeout={existenceFetchTimeout} - renderCallout={ - - } + hideDetails={true} + key={field.name} + itemIndex={index} + groupIndex={0} + dropOntoWorkspace={dropOntoWorkspace} + hasSuggestionForField={hasSuggestionForField} uiActions={uiActions} /> - - - ))} + )) + )} + + + {fieldGroupsToShow.map(([key, fieldGroup], index) => ( + + { + setAccordionState((s) => ({ + ...s, + [key]: open, + })); + const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { + ...accordionState, + [key]: open, + }); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + showExistenceFetchError={existenceFetchFailed} + showExistenceFetchTimeout={existenceFetchTimeout} + renderCallout={ + + } + uiActions={uiActions} + /> + + + ))}
); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 33413bf0ba59bb..5db910e6d3effa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -22,10 +22,8 @@ import { Filter } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { IndexPatternField } from './types'; import { FieldItem } from './field_item'; -import { DatasourceDataPanelProps } from '../types'; -import { IndexPattern } from './types'; +import type { DatasourceDataPanelProps, IndexPattern, IndexPatternField } from '../types'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 41f2a4df9bcda3..4bf33013b3280a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -17,7 +17,7 @@ import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { FieldFormatsStart, FieldFormatsSetup } from '@kbn/field-formats-plugin/public'; import type { EditorFrameSetup } from '../types'; -export type { PersistedIndexPatternLayer, IndexPattern, FormulaPublicApi } from './types'; +export type { PersistedIndexPatternLayer, FormulaPublicApi } from './types'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b1f02328242db8..7a21f1df08922e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -31,14 +31,18 @@ import type { InitializationOptions, OperationDescriptor, FramePublicAPI, + IndexPatternField, + IndexPattern, + IndexPatternRef, } from '../types'; import { - loadInitialState, changeIndexPattern, changeLayerIndexPattern, extractReferences, injectReferences, - loadIndexPatterns, + loadInitialState, + onRefreshIndexPattern, + triggerActionOnIndexPatternChange, } from './loader'; import { toExpression } from './to_expression'; import { @@ -67,14 +71,9 @@ import { TermsIndexPatternColumn, } from './operations'; import { getReferenceRoot } from './operations/layer_helpers'; -import { - IndexPatternField, - IndexPatternPrivateState, - IndexPatternPersistedState, - IndexPattern, -} from './types'; +import { IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { mergeLayer } from './state_helpers'; -import { Datasource, StateSetter, VisualizeEditorContext } from '../types'; +import { Datasource, VisualizeEditorContext } from '../types'; import { deleteColumn, isReferenced } from './operations'; import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel'; import { DraggingIdentifier } from '../drag_drop'; @@ -137,38 +136,19 @@ export function getIndexPatternDatasource({ uiActions: UiActionsStart; }) { const uiSettings = core.uiSettings; - const onIndexPatternLoadError = (err: Error) => - core.notifications.toasts.addError(err, { - title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { - defaultMessage: 'Error loading data view', - }), - }); - - const indexPatternsService = dataViews; - - const handleChangeIndexPattern = ( - id: string, - state: IndexPatternPrivateState, - setState: StateSetter - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }; + + const DATASOURCE_ID = 'indexpattern'; // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { - id: 'indexpattern', + id: DATASOURCE_ID, - async initialize( + initialize( persistedState?: IndexPatternPersistedState, references?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, + indexPatternRefs?: IndexPatternRef[], + indexPatterns?: Record, options?: InitializationOptions ) { return loadInitialState({ @@ -176,9 +156,9 @@ export function getIndexPatternDatasource({ references, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), storage, - indexPatternsService, initialContext, - options, + indexPatternRefs, + indexPatterns, }); }, @@ -224,8 +204,8 @@ export function getIndexPatternDatasource({ return Object.keys(state.layers); }, - removeColumn({ prevState, layerId, columnId }) { - const indexPattern = prevState.indexPatterns[prevState.layers[layerId]?.indexPatternId]; + removeColumn({ prevState, layerId, columnId, indexPatterns }) { + const indexPattern = indexPatterns[prevState.layers[layerId]?.indexPatternId]; return mergeLayer({ state: prevState, layerId, @@ -237,8 +217,8 @@ export function getIndexPatternDatasource({ }); }, - initializeDimension(state, layerId, { columnId, groupId, staticValue }) { - const indexPattern = state.indexPatterns[state.layers[layerId]?.indexPatternId]; + initializeDimension(state, layerId, indexPatterns, { columnId, groupId, staticValue }) { + const indexPattern = indexPatterns[state.layers[layerId]?.indexPatternId]; if (staticValue == null) { return state; } @@ -259,25 +239,30 @@ export function getIndexPatternDatasource({ }); }, - toExpression: (state, layerId) => toExpression(state, layerId, uiSettings), + toExpression: (state, layerId, indexPatterns) => + toExpression(state, layerId, indexPatterns, uiSettings), renderDataPanel( domElement: Element, props: DatasourceDataPanelProps ) { + const { onChangeIndexPattern, ...otherProps } = props; render( { + onChangeIndexPattern(indexPattern, DATASOURCE_ID); + }} data={data} dataViews={dataViews} fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={dataViewFieldEditor} - {...props} + {...otherProps} core={core} uiActions={uiActions} + onIndexPatternRefresh={onRefreshIndexPattern} /> , @@ -317,10 +302,10 @@ export function getIndexPatternDatasource({ return columnLabelMap; }, - isValidColumn: (state: IndexPatternPrivateState, layerId: string, columnId: string) => { + isValidColumn: (state, indexPatterns, layerId, columnId) => { const layer = state.layers[layerId]; - return !isColumnInvalid(layer, columnId, state.indexPatterns[layer.indexPatternId]); + return !isColumnInvalid(layer, columnId, indexPatterns[layer.indexPatternId]); }, renderDimensionTrigger: ( @@ -397,23 +382,20 @@ export function getIndexPatternDatasource({ domElement: Element, props: DatasourceLayerPanelProps ) => { + const { onChangeIndexPattern, ...otherProps } = props; render( { - changeLayerIndexPattern({ + triggerActionOnIndexPatternChange({ indexPatternId, - setState: props.setState, state: props.state, layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - storage, - indexPatternsService, uiActions, }); + onChangeIndexPattern(indexPatternId, DATASOURCE_ID, props.layerId); }} - {...props} + {...otherProps} /> , domElement @@ -456,9 +438,26 @@ export function getIndexPatternDatasource({ }, updateCurrentIndexPatternId: ({ state, indexPatternId, setState }) => { - handleChangeIndexPattern(indexPatternId, state, setState); + setState({ + ...state, + currentIndexPatternId: indexPatternId, + }); }, + onRefreshIndexPattern, + onIndexPatternChange(state, indexPatterns, indexPatternId, layerId) { + if (layerId) { + return changeLayerIndexPattern({ + indexPatternId, + layerId, + state, + replaceIfPossible: true, + storage, + indexPatterns, + }); + } + return changeIndexPattern({ indexPatternId, state, storage, indexPatterns }); + }, getRenderEventCounters(state: IndexPatternPrivateState): string[] { const additionalEvents = { time_shift: false, @@ -489,26 +488,6 @@ export function getIndexPatternDatasource({ ].map((item) => `dimension_${item}`); }, - refreshIndexPatternsList: async ({ indexPatternId, setState }) => { - const newlyMappedIndexPattern = await loadIndexPatterns({ - indexPatternsService: dataViews, - cache: {}, - patterns: [indexPatternId], - }); - const indexPatternRefs = await dataViews.getIdsWithTitle(); - const indexPattern = newlyMappedIndexPattern[indexPatternId]; - setState((s) => { - return { - ...s, - indexPatterns: { - ...s.indexPatterns, - [indexPattern.id]: indexPattern, - }, - indexPatternRefs, - }; - }); - }, - // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId }) => { @@ -523,13 +502,13 @@ export function getIndexPatternDatasource({ }); }, - getPublicAPI({ state, layerId }: PublicAPIProps) { + getPublicAPI({ state, layerId, indexPatterns }: PublicAPIProps) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); const layer = state.layers[layerId]; const visibleColumnIds = layer.columnOrder.filter((colId) => !isReferenced(layer, colId)); return { - datasourceId: 'indexpattern', + datasourceId: DATASOURCE_ID, getTableSpec: () => { // consider also referenced columns in this case // but map fields to the top referencing column @@ -559,7 +538,7 @@ export function getIndexPatternDatasource({ return columnToOperation( layer.columns[columnId], columnLabelMap[columnId], - state.indexPatterns[layer.indexPatternId] + indexPatterns[layer.indexPatternId] ); } } @@ -571,7 +550,7 @@ export function getIndexPatternDatasource({ layer, visibleColumnIds, activeData?.[layerId], - state.indexPatterns[layer.indexPatternId], + indexPatterns[layer.indexPatternId], timeRange ), getVisualDefaults: () => getVisualDefaultsForLayer(layer), @@ -586,12 +565,13 @@ export function getIndexPatternDatasource({ }, }; }, - getDatasourceSuggestionsForField(state, draggedField, filterLayers) { + getDatasourceSuggestionsForField(state, draggedField, filterLayers, indexPatterns) { return isDraggedField(draggedField) ? getDatasourceSuggestionsForField( state, draggedField.indexPatternId, draggedField.field, + indexPatterns, filterLayers ) : []; @@ -600,23 +580,17 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsForVisualizeField, getDatasourceSuggestionsForVisualizeCharts, - getErrorMessages(state) { + getErrorMessages(state, indexPatterns) { if (!state) { return; } // Forward the indexpattern as well, as it is required by some operationType checks const layerErrors = Object.entries(state.layers) - .filter(([_, layer]) => !!state.indexPatterns[layer.indexPatternId]) + .filter(([_, layer]) => !!indexPatterns[layer.indexPatternId]) .map(([layerId, layer]) => ( - getErrorMessages( - layer, - state.indexPatterns[layer.indexPatternId], - state, - layerId, - core - ) ?? [] + getErrorMessages(layer, indexPatterns[layer.indexPatternId], state, layerId, core) ?? [] ).map((message) => ({ shortMessage: '', // Not displayed currently longMessage: typeof message === 'string' ? message : message.message, @@ -669,13 +643,13 @@ export function getIndexPatternDatasource({ ), ]; }, - checkIntegrity: (state) => { + checkIntegrity: (state, indexPatterns) => { const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId); - return ids.filter((id) => !state.indexPatterns[id]); + return ids.filter((id) => !indexPatterns[id]); }, - isTimeBased: (state) => { + isTimeBased: (state, indexPatterns) => { if (!state) return false; - const { layers, indexPatterns } = state; + const { layers } = state; return ( Boolean(layers) && Object.values(layers).some((layer) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 720c917dcbc53b..319c309cac0363 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -9,7 +9,13 @@ import { flatten, minBy, pick, mapValues, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/public'; import { generateId } from '../id_generator'; -import type { DatasourceSuggestion, TableChangeType } from '../types'; +import type { + DatasourceSuggestion, + IndexPattern, + IndexPatternField, + IndexPatternMap, + TableChangeType, +} from '../types'; import { columnToOperation } from './indexpattern'; import { insertNewColumn, @@ -28,12 +34,7 @@ import { hasTermsWithManyBuckets, } from './operations'; import { hasField } from './pure_utils'; -import type { - IndexPattern, - IndexPatternPrivateState, - IndexPatternLayer, - IndexPatternField, -} from './types'; +import type { IndexPatternPrivateState, IndexPatternLayer } from './types'; import { documentField } from './document_field'; export type IndexPatternSuggestion = DatasourceSuggestion; @@ -100,6 +101,7 @@ export function getDatasourceSuggestionsForField( state: IndexPatternPrivateState, indexPatternId: string, field: IndexPatternField, + indexPatterns: IndexPatternMap, filterLayers?: (layerId: string) => boolean ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); @@ -113,8 +115,20 @@ export function getDatasourceSuggestionsForField( // This generates a set of suggestions where we add a layer. // A second set of suggestions is generated for visualizations that don't work with layers const newId = generateId(); - return getEmptyLayerSuggestionsForField(state, newId, indexPatternId, field).concat( - getEmptyLayerSuggestionsForField({ ...state, layers: {} }, newId, indexPatternId, field) + return getEmptyLayerSuggestionsForField( + state, + newId, + indexPatternId, + field, + indexPatterns + ).concat( + getEmptyLayerSuggestionsForField( + { ...state, layers: {} }, + newId, + indexPatternId, + field, + indexPatterns + ) ); } else { // The field we're suggesting on matches an existing layer. In this case we find the layer with @@ -125,9 +139,15 @@ export function getDatasourceSuggestionsForField( (layerId) => state.layers[layerId].columnOrder.length ) as string; if (state.layers[mostEmptyLayerId].columnOrder.length === 0) { - return getEmptyLayerSuggestionsForField(state, mostEmptyLayerId, indexPatternId, field); + return getEmptyLayerSuggestionsForField( + state, + mostEmptyLayerId, + indexPatternId, + field, + indexPatterns + ); } else { - return getExistingLayerSuggestionsForField(state, mostEmptyLayerId, field); + return getExistingLayerSuggestionsForField(state, mostEmptyLayerId, field, indexPatterns); } } } @@ -135,24 +155,26 @@ export function getDatasourceSuggestionsForField( // Called when the user navigates from Visualize editor to Lens export function getDatasourceSuggestionsForVisualizeCharts( state: IndexPatternPrivateState, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter( (id) => state.layers[id].indexPatternId === context[0].indexPatternId ); if (layerIds.length !== 0) return []; - return getEmptyLayersSuggestionsForVisualizeCharts(state, context); + return getEmptyLayersSuggestionsForVisualizeCharts(state, context, indexPatterns); } function getEmptyLayersSuggestionsForVisualizeCharts( state: IndexPatternPrivateState, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const suggestions: IndexPatternSuggestion[] = []; for (let layerIdx = 0; layerIdx < context.length; layerIdx++) { const layer = context[layerIdx]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; if (!indexPattern) return []; const newId = generateId(); @@ -228,18 +250,31 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( export function getDatasourceSuggestionsForVisualizeField( state: IndexPatternPrivateState, indexPatternId: string, - fieldName: string + fieldName: string, + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { const layers = Object.keys(state.layers); const layerIds = layers.filter((id) => state.layers[id].indexPatternId === indexPatternId); // Identify the field by the indexPatternId and the fieldName - const indexPattern = state.indexPatterns[indexPatternId]; + const indexPattern = indexPatterns[indexPatternId]; const field = indexPattern.getFieldByName(fieldName); if (layerIds.length !== 0 || !field) return []; const newId = generateId(); - return getEmptyLayerSuggestionsForField(state, newId, indexPatternId, field).concat( - getEmptyLayerSuggestionsForField({ ...state, layers: {} }, newId, indexPatternId, field) + return getEmptyLayerSuggestionsForField( + state, + newId, + indexPatternId, + field, + indexPatterns + ).concat( + getEmptyLayerSuggestionsForField( + { ...state, layers: {} }, + newId, + indexPatternId, + field, + indexPatterns + ) ); } @@ -255,10 +290,11 @@ function getBucketOperation(field: IndexPatternField) { function getExistingLayerSuggestionsForField( state: IndexPatternPrivateState, layerId: string, - field: IndexPatternField + field: IndexPatternField, + indexPatterns: IndexPatternMap ) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const operations = getOperationTypesForField(field); const usableAsBucketOperation = getBucketOperation(field); const fieldInUse = Object.values(layer.columns).some( @@ -369,9 +405,10 @@ function getEmptyLayerSuggestionsForField( state: IndexPatternPrivateState, layerId: string, indexPatternId: string, - field: IndexPatternField + field: IndexPatternField, + indexPatterns: IndexPatternMap ): IndexPatternSuggestion[] { - const indexPattern = state.indexPatterns[indexPatternId]; + const indexPattern = indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; const bucketOperation = getBucketOperation(field); if (bucketOperation) { @@ -447,6 +484,7 @@ function createNewLayerWithMetricAggregation( export function getDatasourceSuggestionsFromCurrentState( state: IndexPatternPrivateState, + indexPatterns: IndexPatternMap, filterLayers: (layerId: string) => boolean = () => true ): Array> { const layers = Object.entries(state.layers || {}).filter(([layerId]) => filterLayers(layerId)); @@ -467,7 +505,7 @@ export function getDatasourceSuggestionsFromCurrentState( }) : i18n.translate('xpack.lens.indexPatternSuggestion.removeLayerLabel', { defaultMessage: 'Show only {indexPatternTitle}', - values: { indexPatternTitle: state.indexPatterns[layer.indexPatternId].title }, + values: { indexPatternTitle: indexPatterns[layer.indexPatternId].title }, }); return buildSuggestion({ @@ -495,7 +533,7 @@ export function getDatasourceSuggestionsFromCurrentState( layers .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; const [buckets, metrics, references] = getExistingColumnGroups(layer); const timeDimension = layer.columnOrder.find( (columnId) => @@ -522,7 +560,9 @@ export function getDatasourceSuggestionsFromCurrentState( if (!references.length && metrics.length && buckets.length === 0) { if (timeField && buckets.length < 1 && !hasTermsWithManyBuckets(layer)) { // suggest current metric over time if there is a default time field - suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); + suggestions.push( + createSuggestionWithDefaultDateHistogram(state, layerId, timeField, indexPatterns) + ); } if (indexPattern) { suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); @@ -541,11 +581,13 @@ export function getDatasourceSuggestionsFromCurrentState( ) { // suggest current configuration over time if there is a default time field // and no time dimension yet - suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField)); + suggestions.push( + createSuggestionWithDefaultDateHistogram(state, layerId, timeField, indexPatterns) + ); } if (buckets.length === 2) { - suggestions.push(createChangedNestingSuggestion(state, layerId)); + suggestions.push(createChangedNestingSuggestion(state, layerId, indexPatterns)); } } return suggestions; @@ -553,11 +595,15 @@ export function getDatasourceSuggestionsFromCurrentState( ); } -function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId: string) { +function createChangedNestingSuggestion( + state: IndexPatternPrivateState, + layerId: string, + indexPatterns: IndexPatternMap +) { const layer = state.layers[layerId]; const [firstBucket, secondBucket, ...rest] = layer.columnOrder; const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; - const indexPattern = state.indexPatterns[state.currentIndexPatternId]; + const indexPattern = indexPatterns[state.currentIndexPatternId]; const firstBucketColumn = layer.columns[firstBucket]; const firstBucketLabel = (hasField(firstBucketColumn) && @@ -670,10 +716,11 @@ function createAlternativeMetricSuggestions( function createSuggestionWithDefaultDateHistogram( state: IndexPatternPrivateState, layerId: string, - timeField: IndexPatternField + timeField: IndexPatternField, + indexPatterns: IndexPatternMap ) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = indexPatterns[layer.indexPatternId]; return buildSuggestion({ state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index d0c8e1ffafd693..3d366408595b0f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -12,7 +12,7 @@ import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; -import { ChangeIndexPattern } from './change_indexpattern'; +import { ChangeIndexPattern } from '../shared_components/dataview_picker/dataview_picker'; import { getFieldByNameFactory } from './pure_helpers'; import { TermsIndexPatternColumn } from './operations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index 4fd7b920e124b2..9824f70eeddfc0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { DatasourceLayerPanelProps } from '../types'; import { IndexPatternPrivateState } from './types'; -import { ChangeIndexPattern } from './change_indexpattern'; +import { ChangeIndexPattern } from '../shared_components/dataview_picker/dataview_picker'; export interface IndexPatternLayerPanelProps extends DatasourceLayerPanelProps { @@ -18,10 +18,15 @@ export interface IndexPatternLayerPanelProps onChangeIndexPattern: (newId: string) => void; } -export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatternLayerPanelProps) { +export function LayerPanel({ + state, + layerId, + onChangeIndexPattern, + dataViews, +}: IndexPatternLayerPanelProps) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; + const indexPattern = dataViews.indexPatterns[layer.indexPatternId]; const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', }); @@ -37,7 +42,7 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter fontWeight: 'normal', }} indexPatternId={layer.indexPatternId} - indexPatternRefs={state.indexPatternRefs} + indexPatternRefs={dataViews.indexPatternRefs} isMissingCurrent={!indexPattern} onChangeIndexPattern={onChangeIndexPattern} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index d3affb5b32d8c2..204e5761404112 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -7,9 +7,7 @@ import { uniq, mapValues, difference } from 'lodash'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import type { HttpSetup, SavedObjectReference } from '@kbn/core/public'; -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; -import { isNestedField } from '@kbn/data-views-plugin/common'; +import type { SavedObjectReference } from '@kbn/core/public'; import { UPDATE_FILTER_REFERENCES_ACTION, UPDATE_FILTER_REFERENCES_TRIGGER, @@ -19,148 +17,18 @@ import { UiActionsStart, VisualizeFieldContext, } from '@kbn/ui-actions-plugin/public'; -import type { - DatasourceDataPanelProps, - InitializationOptions, - VisualizeEditorContext, -} from '../types'; -import { - IndexPattern, - IndexPatternRef, - IndexPatternPersistedState, - IndexPatternPrivateState, - IndexPatternField, - IndexPatternLayer, -} from './types'; +import type { VisualizeEditorContext } from '../types'; +import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternLayer } from './types'; -import { updateLayerIndexPattern, translateToOperationName } from './operations'; -import { DateRange, ExistingFields } from '../../common/types'; -import { BASE_API_URL } from '../../common'; -import { documentField } from './document_field'; +import { memoizedGetAvailableOperationsByMetadata, updateLayerIndexPattern } from './operations'; import { readFromStorage, writeToStorage } from '../settings_storage'; -import { getFieldByNameFactory } from './pure_helpers'; -import { memoizedGetAvailableOperationsByMetadata } from './operations'; - -type SetState = DatasourceDataPanelProps['setState']; -type IndexPatternsService = Pick; -type ErrorHandler = (err: Error) => void; - -export function convertDataViewIntoLensIndexPattern(dataView: DataView): IndexPattern { - const newFields = dataView.fields - .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) - .map((field): IndexPatternField => { - // Convert the getters on the index pattern service into plain JSON - const base = { - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - meta: dataView.metaFields.includes(field.name), - esTypes: field.esTypes, - scripted: field.scripted, - runtime: Boolean(field.runtimeField), - }; - - // Simplifies tests by hiding optional properties instead of undefined - return base.scripted - ? { - ...base, - lang: field.lang, - script: field.script, - } - : base; - }) - .concat(documentField); - - const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; - if (typeMeta?.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[translateToOperationName(agg)] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - return { - id: dataView.id!, // id exists for sure because we got index patterns by id - title, - name: name ? name : title, - timeFieldName, - fieldFormatMap: - fieldFormatMap && - Object.fromEntries( - Object.entries(fieldFormatMap).map(([id, format]) => [ - id, - // @ts-expect-error FIXME Property 'toJSON' does not exist on type 'SerializedFieldFormat' - 'toJSON' in format ? format.toJSON() : format, - ]) - ), - fields: newFields, - getFieldByName: getFieldByNameFactory(newFields, false), - hasRestrictions: !!typeMeta?.aggs, - }; -} - -export async function loadIndexPatterns({ - indexPatternsService, - patterns, - notUsedPatterns, - cache, -}: { - indexPatternsService: IndexPatternsService; - patterns: string[]; - notUsedPatterns?: string[]; - cache: Record; -}) { - const missingIds = patterns.filter((id) => !cache[id]); - - if (missingIds.length === 0) { - return cache; - } +import type { IndexPattern, IndexPatternRef } from '../types'; +export function onRefreshIndexPattern() { if (memoizedGetAvailableOperationsByMetadata.cache.clear) { // clear operations meta data cache because index pattern reference may change memoizedGetAvailableOperationsByMetadata.cache.clear(); } - - const allIndexPatterns = await Promise.allSettled( - missingIds.map((id) => indexPatternsService.get(id)) - ); - // ignore rejected indexpatterns here, they're already handled at the app level - let indexPatterns = allIndexPatterns - .filter( - (response): response is PromiseFulfilledResult => response.status === 'fulfilled' - ) - .map((response) => response.value); - - // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds - for (let i = 0; notUsedPatterns && i < notUsedPatterns?.length && !indexPatterns.length; i++) { - const resp = await indexPatternsService.get(notUsedPatterns[i]).catch((e) => { - // do nothing - }); - if (resp) { - indexPatterns = [resp]; - } - } - - const indexPatternsObject = indexPatterns.reduce( - (acc, indexPattern) => ({ - [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern), - ...acc, - }), - { ...cache } - ); - - return indexPatternsObject; } const getLastUsedIndexPatternId = ( @@ -209,42 +77,43 @@ export function injectReferences( }; } -export async function loadInitialState({ +export function createStateFromPersisted({ persistedState, references, - defaultIndexPatternId, - storage, - indexPatternsService, - initialContext, - options, }: { persistedState?: IndexPatternPersistedState; references?: SavedObjectReference[]; +}) { + return persistedState && references ? injectReferences(persistedState, references) : undefined; +} + +export function getUsedIndexPatterns({ + state, + indexPatternRefs, + storage, + initialContext, + defaultIndexPatternId, +}: { + state?: { + layers: Record; + }; defaultIndexPatternId?: string; storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; initialContext?: VisualizeFieldContext | VisualizeEditorContext; - options?: InitializationOptions; -}): Promise { - const { isFullEditor } = options ?? {}; - // make it explicit or TS will infer never[] and break few lines down - const indexPatternRefs: IndexPatternRef[] = await (isFullEditor - ? loadIndexPatternRefs(indexPatternsService) - : []); - + indexPatternRefs: IndexPatternRef[]; +}) { const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); const fallbackId = lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0]?.id; const indexPatternIds = []; - if (initialContext && 'isVisualizeAction' in initialContext) { - for (let layerIdx = 0; layerIdx < initialContext.layers.length; layerIdx++) { - const layerContext = initialContext.layers[layerIdx]; - indexPatternIds.push(layerContext.indexPatternId); + if (initialContext) { + if ('isVisualizeAction' in initialContext) { + for (const { indexPatternId } of initialContext.layers) { + indexPatternIds.push(indexPatternId); + } + } else { + indexPatternIds.push(initialContext.indexPatternId); } - } else if (initialContext) { - indexPatternIds.push(initialContext.indexPatternId); } - const state = - persistedState && references ? injectReferences(persistedState, references) : undefined; const usedPatterns = ( initialContext ? indexPatternIds @@ -253,30 +122,50 @@ export async function loadInitialState({ // take out the undefined from the list .filter(Boolean); - const notUsedPatterns: string[] = difference( - uniq(indexPatternRefs.map(({ id }) => id)), - usedPatterns - ); + return { + usedPatterns, + allIndexPatternIds: indexPatternIds, + }; +} + +export function loadInitialState({ + persistedState, + references, + defaultIndexPatternId, + storage, + initialContext, + indexPatternRefs = [], + indexPatterns = {}, +}: { + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; + defaultIndexPatternId?: string; + storage: IStorageWrapper; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; + indexPatternRefs?: IndexPatternRef[]; + indexPatterns?: Record; +}): IndexPatternPrivateState { + const state = createStateFromPersisted({ persistedState, references }); + const { usedPatterns, allIndexPatternIds: indexPatternIds } = getUsedIndexPatterns({ + state, + defaultIndexPatternId, + storage, + initialContext, + indexPatternRefs, + }); const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id)); - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: {}, - patterns: usedPatterns, - notUsedPatterns, - }); + const notUsedPatterns: string[] = difference([...availableIndexPatterns], usedPatterns); // Priority list: // * start with the indexPattern in context // * then fallback to the used ones // * then as last resort use a first one from not used refs - const availableIndexPatternIds = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].filter( + const currentIndexPatternId = [...indexPatternIds, ...usedPatterns, ...notUsedPatterns].find( (id) => id != null && availableIndexPatterns.has(id) && indexPatterns[id] ); - const currentIndexPatternId = availableIndexPatternIds[0]; - if (currentIndexPatternId) { setLastUsedIndexPatternId(storage, currentIndexPatternId); } @@ -285,78 +174,41 @@ export async function loadInitialState({ layers: {}, ...state, currentIndexPatternId, - indexPatternRefs, - indexPatterns, - existingFields: {}, - isFirstExistenceFetch: true, }; } -export async function changeIndexPattern({ - id, +export function changeIndexPattern({ + indexPatternId, state, - setState, - onError, storage, - indexPatternsService, + indexPatterns, }: { - id: string; + indexPatternId: string; state: IndexPatternPrivateState; - setState: SetState; - onError: ErrorHandler; storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; + indexPatterns: Record; }) { - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: state.indexPatterns, - patterns: [id], - }); - - if (indexPatterns[id] == null) { - return onError(Error('Missing indexpatterns')); - } - - try { - setState( - (s) => ({ - ...s, - layers: isSingleEmptyLayer(state.layers) - ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) - : state.layers, - indexPatterns: { - ...s.indexPatterns, - [id]: indexPatterns[id], - }, - currentIndexPatternId: id, - }), - { applyImmediately: true } - ); - setLastUsedIndexPatternId(storage, id); - } catch (err) { - onError(err); - } + setLastUsedIndexPatternId(storage, indexPatternId); + return { + ...state, + layers: isSingleEmptyLayer(state.layers) + ? mapValues(state.layers, (layer) => + updateLayerIndexPattern(layer, indexPatterns[indexPatternId]) + ) + : state.layers, + currentIndexPatternId: indexPatternId, + }; } -export async function changeLayerIndexPattern({ - indexPatternId, - layerId, +export function triggerActionOnIndexPatternChange({ state, - setState, - onError, - replaceIfPossible, - storage, - indexPatternsService, + layerId, uiActions, + indexPatternId, }: { indexPatternId: string; layerId: string; state: IndexPatternPrivateState; - setState: SetState; - onError: ErrorHandler; - replaceIfPossible?: boolean; - storage: IStorageWrapper; - indexPatternsService: IndexPatternsService; uiActions: UiActionsStart; }) { const fromDataView = state.layers[layerId].indexPatternId; @@ -372,144 +224,32 @@ export async function changeLayerIndexPattern({ defaultDataView: toDataView, usedDataViews: Object.values(Object.values(state.layers).map((layer) => layer.indexPatternId)), } as ActionExecutionContext); - - const indexPatterns = await loadIndexPatterns({ - indexPatternsService, - cache: state.indexPatterns, - patterns: [indexPatternId], - }); - if (indexPatterns[indexPatternId] == null) { - return onError(Error('Missing indexpatterns')); - } - - try { - setState((s) => ({ - ...s, - layers: { - ...s.layers, - [layerId]: updateLayerIndexPattern(s.layers[layerId], indexPatterns[indexPatternId]), - }, - indexPatterns: { - ...s.indexPatterns, - [indexPatternId]: indexPatterns[indexPatternId], - }, - currentIndexPatternId: replaceIfPossible ? indexPatternId : s.currentIndexPatternId, - })); - setLastUsedIndexPatternId(storage, indexPatternId); - } catch (err) { - onError(err); - } -} - -async function loadIndexPatternRefs( - indexPatternsService: IndexPatternsService -): Promise { - const indexPatterns = await indexPatternsService.getIdsWithTitle(); - - return indexPatterns.sort((a, b) => { - return a.title.localeCompare(b.title); - }); } -export async function syncExistingFields({ +export function changeLayerIndexPattern({ + indexPatternId, indexPatterns, - dateRange, - fetchJson, - setState, - isFirstExistenceFetch, - currentIndexPatternTitle, - dslQuery, - showNoDataPopover, + layerId, + state, + replaceIfPossible, + storage, }: { - dateRange: DateRange; - indexPatterns: Array<{ - id: string; - title: string; - fields: IndexPatternField[]; - timeFieldName?: string | null; - hasRestrictions: boolean; - }>; - fetchJson: HttpSetup['post']; - setState: SetState; - isFirstExistenceFetch: boolean; - currentIndexPatternTitle: string; - dslQuery: object; - showNoDataPopover: () => void; + indexPatternId: string; + layerId: string; + state: IndexPatternPrivateState; + replaceIfPossible?: boolean; + storage: IStorageWrapper; + indexPatterns: Record; }) { - const existenceRequests = indexPatterns.map((pattern) => { - if (pattern.hasRestrictions) { - return { - indexPatternTitle: pattern.title, - existingFieldNames: pattern.fields.map((field) => field.name), - }; - } - const body: Record = { - dslQuery, - fromDate: dateRange.fromDate, - toDate: dateRange.toDate, - }; - - if (pattern.timeFieldName) { - body.timeFieldName = pattern.timeFieldName; - } - - return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { - body: JSON.stringify(body), - }) as Promise; - }); - - try { - const emptinessInfo = await Promise.all(existenceRequests); - if (isFirstExistenceFetch) { - const fieldsCurrentIndexPattern = emptinessInfo.find( - (info) => info.indexPatternTitle === currentIndexPatternTitle - ); - if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { - showNoDataPopover(); - } - } - - setState( - (state) => ({ - ...state, - isFirstExistenceFetch: false, - existenceFetchFailed: false, - existenceFetchTimeout: false, - existingFields: emptinessInfo.reduce( - (acc, info) => { - acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames); - return acc; - }, - { ...state.existingFields } - ), - }), - { applyImmediately: true } - ); - } catch (e) { - // show all fields as available if fetch failed or timed out - setState( - (state) => ({ - ...state, - existenceFetchFailed: e.res?.status !== 408, - existenceFetchTimeout: e.res?.status === 408, - existingFields: indexPatterns.reduce( - (acc, pattern) => { - acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name)); - return acc; - }, - { ...state.existingFields } - ), - }), - { applyImmediately: true } - ); - } -} - -function booleanMap(keys: string[]) { - return keys.reduce((acc, key) => { - acc[key] = true; - return acc; - }, {} as Record); + setLastUsedIndexPatternId(storage, indexPatternId); + return { + ...state, + layers: { + ...state.layers, + [layerId]: updateLayerIndexPattern(state.layers[layerId], indexPatterns[indexPatternId]), + }, + currentIndexPatternId: replaceIfPossible ? indexPatternId : state.currentIndexPatternId, + }; } function isSingleEmptyLayer(layerMap: IndexPatternPrivateState['layers']) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index 1e16c27253d9b9..4a8c6d437f2ac8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -9,7 +9,8 @@ import { createMockedIndexPattern } from '../../../mocks'; import { formulaOperation, GenericOperationDefinition, GenericIndexPatternColumn } from '..'; import { FormulaIndexPatternColumn } from './formula'; import { insertOrReplaceFormulaColumn } from './parse'; -import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; +import { IndexPattern, IndexPatternField } from '../../../../editor_frame_service/types'; import { tinymathFunctions } from './util'; import { TermsIndexPatternColumn } from '../terms'; import { MovingAverageIndexPatternColumn } from '../calculations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 9548d9473e6568..e0c1c4b3b84cce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -14,7 +14,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -import type { IndexPatternLayer, IndexPattern } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; import { rangeOperation } from '..'; import { RangeIndexPatternColumn } from './ranges'; import { @@ -25,8 +25,9 @@ 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 '../../../../editor_frame_service/types'; // mocking random id generator function jest.mock('@elastic/eui', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx index 47cc121be095b2..49d913655af453 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx @@ -6,5 +6,4 @@ */ export * from './label_input'; -export * from './buckets'; export * from './form_row'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx index 6381b2843cf2c0..a6b734d373849d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx @@ -15,13 +15,18 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DragDropBuckets, NewBucketButton } from '../shared_components/buckets'; -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 { IndexPattern, IndexPatternPrivateState } from '../../../types'; +import type { IndexPatternPrivateState } from '../../../types'; import type { OperationSupportMatrix } from '../../../dimension_panel'; import { supportedTypes } from './constants'; +import type { IndexPattern } from '../../../../editor_frame_service/types'; const generateId = htmlIdGenerator(); export const MAX_MULTI_FIELDS_SIZE = 3; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 424bdfd002522a..f01ccebdabcafb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -28,12 +28,13 @@ import { LastValueIndexPatternColumn, operationDefinitionMap, } from '..'; -import { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../../../types'; +import { IndexPatternLayer, IndexPatternPrivateState } from '../../../types'; import { FrameDatasourceAPI } from '../../../../types'; import { DateHistogramIndexPatternColumn } from '../date_histogram'; import { getOperationSupportMatrix } from '../../../dimension_panel/operation_support'; import { FieldSelect } from '../../../dimension_panel/field_select'; import { ReferenceEditor } from '../../../dimension_panel/reference_editor'; +import { IndexPattern } from '../../../../editor_frame_service/types'; import { cloneDeep } from 'lodash'; import { IncludeExcludeRow } from './include_exclude_options'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c89ec6ae021995..cfbc5d4455fd27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -23,7 +23,7 @@ import { operationDefinitionMap, OperationType } from '.'; import { TermsIndexPatternColumn } from './definitions/terms'; import { DateHistogramIndexPatternColumn } from './definitions/date_histogram'; import { AvgIndexPatternColumn } from './definitions/metrics'; -import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; +import type { IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; @@ -38,6 +38,7 @@ import { } from './definitions'; import { TinymathAST } from '@kbn/tinymath'; import { CoreStart } from '@kbn/core/public'; +import { IndexPattern } from '../../editor_frame_service/types'; jest.mock('.'); jest.mock('../../id_generator'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index a1edd6132d22aa..be3dd8702e20d0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -13,6 +13,8 @@ import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/pu import type { DatasourceFixAction, FrameDatasourceAPI, + IndexPattern, + IndexPatternField, OperationMetadata, VisualizationDimensionGroupConfig, } from '../../types'; @@ -27,8 +29,6 @@ import { } from './definitions'; import type { DataViewDragDropOperation, - IndexPattern, - IndexPatternField, IndexPatternLayer, IndexPatternPrivateState, } from '../types'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 396bf78f82db6b..b30c91a7f10844 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -6,7 +6,7 @@ */ import { memoize } from 'lodash'; -import { OperationMetadata } from '../../types'; +import type { IndexPattern, IndexPatternField, OperationMetadata } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -15,7 +15,6 @@ import { renameOperationsMapping, BaseIndexPatternColumn, } from './definitions'; -import { IndexPattern, IndexPatternField } from '../types'; import { documentField } from '../document_field'; import { hasField } from '../pure_utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts index 6c81634fb4c090..ba3ba9bbb824f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.ts @@ -6,17 +6,9 @@ */ import { keyBy } from 'lodash'; -import { IndexPatternField, IndexPatternPrivateState } from './types'; +import { IndexPatternField } from '../types'; import { documentField } from './document_field'; -export function fieldExists( - existingFields: IndexPatternPrivateState['existingFields'], - indexPatternTitle: string, - fieldName: string -) { - return existingFields[indexPatternTitle] && existingFields[indexPatternTitle][fieldName]; -} - export function getFieldByNameFactory( newFields: IndexPatternField[], addRecordsField: boolean = true diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx index 6d69760d51d87b..583f6b28996c9d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx @@ -20,6 +20,7 @@ export const QueryInput = ({ onSubmit, disableAutoFocus, ['data-test-subj']: dataTestSubj, + placeholder, }: { value: Query; onChange: (input: Query) => void; @@ -28,6 +29,7 @@ export const QueryInput = ({ onSubmit: () => void; disableAutoFocus?: boolean; 'data-test-subj'?: string; + placeholder?: string; }) => { const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange }); @@ -51,7 +53,8 @@ export const QueryInput = ({ } }} placeholder={ - inputValue.language === 'kuery' + placeholder ?? + (inputValue.language === 'kuery' ? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', { defaultMessage: '{example}', values: { example: 'method : "GET" or status : "404"' }, @@ -59,7 +62,7 @@ export const QueryInput = ({ : i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', { defaultMessage: '{example}', values: { example: 'method:GET OR status:404' }, - }) + })) } languageSwitcherPopoverAnchorPosition="rightDown" /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index b5c6acccac5434..7dbad00c627a60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -13,13 +13,12 @@ import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { Datatable } from '@kbn/expressions-plugin/common'; import { search } from '@kbn/data-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; -import { - IndexPattern, +import type { GenericIndexPatternColumn, IndexPatternLayer, IndexPatternPrivateState, } from './types'; -import { FramePublicAPI } from '../types'; +import type { FramePublicAPI, IndexPattern } from '../types'; export const timeShiftOptions = [ { @@ -187,12 +186,12 @@ export function getDisallowedPreviousShiftMessage( export function getStateTimeShiftWarningMessages( datatableUtilities: DatatableUtilitiesService, state: IndexPatternPrivateState, - { activeData }: FramePublicAPI + { activeData, dataViews }: FramePublicAPI ) { if (!state) return; const warningMessages: React.ReactNode[] = []; Object.entries(state.layers).forEach(([layerId, layer]) => { - const layerIndexPattern = state.indexPatterns[layer.indexPatternId]; + const layerIndexPattern = dataViews.indexPatterns[layer.indexPatternId]; if (!layerIndexPattern) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index c23cd07f866f3e..e6113bf1509704 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -22,10 +22,11 @@ import { } from '@kbn/expressions-plugin/public'; import { GenericIndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; -import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; +import { IndexPatternPrivateState, IndexPatternLayer } from './types'; import { DateHistogramIndexPatternColumn, RangeIndexPatternColumn } from './operations/definitions'; import { FormattedIndexPatternColumn } from './operations/definitions/column_types'; import { isColumnFormatted, isColumnOfType } from './operations/definitions/helpers'; +import type { IndexPattern, IndexPatternMap } from '../types'; export type OriginalColumn = { id: string } & GenericIndexPatternColumn; @@ -408,12 +409,13 @@ function sortedReferences(columns: Array; - meta?: boolean; - runtime?: boolean; -}; - export interface IndexPatternLayer { columnOrder: string[]; columns: Record; @@ -86,26 +66,20 @@ export type PersistedIndexPatternLayer = Omit; - indexPatternRefs: IndexPatternRef[]; - indexPatterns: Record; + // indexPatternRefs: IndexPatternRef[]; + // indexPatterns: Record; - /** - * indexPatternId -> fieldName -> boolean - */ - existingFields: Record>; - isFirstExistenceFetch: boolean; - existenceFetchFailed?: boolean; - existenceFetchTimeout?: boolean; + // /** + // * indexPatternId -> fieldName -> boolean + // */ + // existingFields: Record>; + // isFirstExistenceFetch: boolean; + // existenceFetchFailed?: boolean; + // existenceFetchTimeout?: boolean; isDimensionClosePrevented?: boolean; } -export interface IndexPatternRef { - id: string; - title: string; - name?: string; -} - export interface DataViewDragDropOperation extends DragDropOperation { dataView: IndexPattern; column?: GenericIndexPatternColumn; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index 85837b10103558..d67023a1a24be8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -16,8 +16,8 @@ import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui'; import type { DatatableColumn } from '@kbn/expressions-plugin/common'; import { groupBy, escape } from 'lodash'; import type { Query } from '@kbn/data-plugin/common'; -import type { FramePublicAPI, StateSetter } from '../types'; -import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from './types'; +import type { FramePublicAPI, IndexPattern, StateSetter } from '../types'; +import type { IndexPatternLayer, IndexPatternPrivateState } from './types'; import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types'; import { @@ -162,7 +162,7 @@ const accuracyModeEnabledWarning = (columnName: string, docLink: string) => ( export function getPrecisionErrorWarningMessages( datatableUtilities: DatatableUtilitiesService, state: IndexPatternPrivateState, - { activeData }: FramePublicAPI, + { activeData, dataViews }: FramePublicAPI, docLinks: DocLinksStart, setState: StateSetter ) { @@ -181,7 +181,7 @@ export function getPrecisionErrorWarningMessages( const currentLayer = state.layers[layerId]; const currentColumn = currentLayer?.columns[column.id]; if (currentLayer && currentColumn && datatableUtilities.hasPrecisionError(column)) { - const indexPattern = state.indexPatterns[currentLayer.indexPatternId]; + const indexPattern = dataViews.indexPatterns[currentLayer.indexPatternId]; // currentColumnIsTerms is mostly a type guard. If there's a precision error, // we already know that we're dealing with a terms-based operation (at least for now). const currentColumnIsTerms = isColumnOfType( diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index c66d538ed05115..b3c59056e63c62 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -12,6 +12,7 @@ import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; @@ -279,11 +280,16 @@ export class LensPlugin { data: plugins.data, timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, - documentToExpression: this.editorFrameService!.documentToExpression, + documentToExpression: (doc) => + this.editorFrameService!.documentToExpression(doc, { + dataViews: plugins.dataViews, + storage: new Storage(localStorage), + uiSettings: core.uiSettings, + }), injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), visualizationMap, datasourceMap, - indexPatternService: plugins.dataViews, + dataViews: plugins.dataViews, uiActions: plugins.uiActions, usageCollection, inspector: plugins.inspector, diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx new file mode 100644 index 00000000000000..b66a5f1d765457 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -0,0 +1,93 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; +import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { IndexPatternRef } from '../../types'; + +export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { + label: string; + title?: string; +}; + +export function ChangeIndexPattern({ + indexPatternRefs, + isMissingCurrent, + indexPatternId, + onChangeIndexPattern, + trigger, + selectableProps, +}: { + trigger: ChangeIndexPatternTriggerProps; + indexPatternRefs: IndexPatternRef[]; + isMissingCurrent?: boolean; + onChangeIndexPattern: (newId: string) => void; + indexPatternId?: string; + selectableProps?: EuiSelectableProps; +}) { + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + + // be careful to only add color with a value, otherwise it will fallbacks to "primary" + const colorProp = isMissingCurrent + ? { + color: 'danger' as const, + } + : {}; + + const createTrigger = function () { + const { label, title, ...rest } = trigger; + return ( + setPopoverIsOpen(!isPopoverOpen)} + fullWidth + {...colorProp} + {...rest} + > + {label} + + ); + }; + + return ( + <> + setPopoverIsOpen(false)} + display="block" + panelPaddingSize="none" + ownFocus + > +
+ + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + + + { + onChangeIndexPattern(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + /> +
+
+ + ); +} diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts new file mode 100644 index 00000000000000..4f53930fa49732 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/helpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IndexPattern } from '../../types'; + +/** + * Checks if the provided field contains data (works for meta field) + */ +export function fieldContainsData( + field: string, + indexPattern: IndexPattern, + existingFields: Record +) { + return ( + indexPattern.getFieldByName(field)?.type === 'document' || fieldExists(existingFields, field) + ); +} + +/** + * Performs an existence check on the existingFields data structure for the provided field. + * Does not work for meta fields. + */ +export function fieldExists(existingFields: Record, fieldName: string) { + return existingFields[fieldName]; +} diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts new file mode 100644 index 00000000000000..4de03b2f8b92c3 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/index.ts @@ -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 { ChangeIndexPattern } from './dataview_picker'; +export { fieldExists, fieldContainsData } from './helpers'; diff --git a/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx new file mode 100644 index 00000000000000..aba0cbfc40a6e5 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 React from 'react'; +import { mount, shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiIcon } from '@elastic/eui'; +import { DragDropBuckets, DraggableBucketContainer } from './buckets'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + EuiDragDropContext: 'eui-drag-drop-context', + EuiDroppable: 'eui-droppable', + EuiDraggable: (props: any) => props.children(), // eslint-disable-line @typescript-eslint/no-explicit-any + }; +}); + +describe('buckets shared components', () => { + describe('DragDropBuckets', () => { + it('should call onDragEnd when dragging ended with reordered items', () => { + const items = [
first
,
second
,
third
]; + const defaultProps = { + items, + onDragStart: jest.fn(), + onDragEnd: jest.fn(), + droppableId: 'TEST_ID', + children: items, + }; + const instance = shallow(); + act(() => { + // simulate dragging ending + instance.props().onDragEnd({ source: { index: 0 }, destination: { index: 1 } }); + }); + + expect(defaultProps.onDragEnd).toHaveBeenCalledWith([ +
second
, +
first
, +
third
, + ]); + }); + }); + describe('DraggableBucketContainer', () => { + const defaultProps = { + isInvalid: false, + invalidMessage: 'invalid', + onRemoveClick: jest.fn(), + removeTitle: 'remove', + children:
popover
, + id: '0', + idx: 0, + }; + it('should render valid component', () => { + const instance = mount(); + const popover = instance.find('[data-test-subj="popover"]'); + expect(popover).toHaveLength(1); + }); + it('should render invalid component', () => { + const instance = mount(); + const iconProps = instance.find(EuiIcon).first().props(); + expect(iconProps.color).toEqual('danger'); + expect(iconProps.type).toEqual('alert'); + expect(iconProps.title).toEqual('invalid'); + }); + it('should call onRemoveClick when remove icon is clicked', () => { + const instance = mount(); + const removeIcon = instance + .find('[data-test-subj="lns-customBucketContainer-remove"]') + .first(); + removeIcon.simulate('click'); + expect(defaultProps.onRemoveClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx new file mode 100644 index 00000000000000..0f4ba342348cf5 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx @@ -0,0 +1,160 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiButtonIcon, + EuiIcon, + EuiDragDropContext, + euiDragDropReorder, + EuiDraggable, + EuiDroppable, + EuiButtonEmpty, +} from '@elastic/eui'; + +export const NewBucketButton = ({ + label, + onClick, + ['data-test-subj']: dataTestSubj, + isDisabled, +}: { + label: string; + onClick: () => void; + 'data-test-subj'?: string; + isDisabled?: boolean; +}) => ( + + {label} + +); + +interface BucketContainerProps { + isInvalid?: boolean; + invalidMessage: string; + onRemoveClick: () => void; + removeTitle: string; + isNotRemovable?: boolean; + children: React.ReactNode; + dataTestSubj?: string; +} + +const BucketContainer = ({ + isInvalid, + invalidMessage, + onRemoveClick, + removeTitle, + children, + dataTestSubj, + isNotRemovable, +}: BucketContainerProps) => { + return ( + + + {/* Empty for spacing */} + + + + {children} + + + + + + ); +}; + +export const DraggableBucketContainer = ({ + id, + idx, + children, + ...bucketContainerProps +}: { + id: string; + idx: number; + children: React.ReactNode; +} & BucketContainerProps) => { + return ( + + {(provided) => {children}} + + ); +}; + +interface DraggableLocation { + droppableId: string; + index: number; +} + +export const DragDropBuckets = ({ + items, + onDragStart, + onDragEnd, + droppableId, + children, +}: { + items: any; // eslint-disable-line @typescript-eslint/no-explicit-any + onDragStart: () => void; + onDragEnd: (items: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + droppableId: string; + children: React.ReactElement[]; +}) => { + const handleDragEnd = ({ + source, + destination, + }: { + source?: DraggableLocation; + destination?: DraggableLocation; + }) => { + if (source && destination) { + const newItems = euiDragDropReorder(items, source.index, destination.index); + onDragEnd(newItems); + } + }; + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx index bde920446d4684..eb33bb41e03730 100644 --- a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx @@ -30,7 +30,7 @@ const DEFAULT_COMBOBOX_WIDTH = 305; const COMBOBOX_PADDINGS = 90; const DEFAULT_FONT = '14px Inter'; -export function FieldPicker({ +export function FieldPicker({ selectedOptions, options, onChoose, diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index e3a9af00ad0055..a4e6bf046b36d1 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -11,6 +11,12 @@ export { LegendSettingsPopover } from './legend_settings_popover'; 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 { + NewBucketButton, + DraggableBucketContainer, + DragDropBuckets, +} from './drag_drop_bucket/buckets'; export { RangeInputField } from './range_input_field'; export { BucketAxisBoundsControl, diff --git a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts index 90cd4ba27723eb..7d1e6a0b48fbae 100644 --- a/x-pack/plugins/lens/public/state_management/context_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/context_middleware/index.ts @@ -23,11 +23,14 @@ import { onActiveDataChange } from '../lens_slice'; import { DatasourceMap } from '../../types'; function isTimeBased(state: LensState, datasourceMap: DatasourceMap) { - const { activeDatasourceId, datasourceStates } = state.lens; + const { activeDatasourceId, datasourceStates, dataViews } = state.lens; return Boolean( activeDatasourceId && datasourceStates[activeDatasourceId] && - datasourceMap[activeDatasourceId].isTimeBased?.(datasourceStates[activeDatasourceId].state) + datasourceMap[activeDatasourceId].isTimeBased?.( + datasourceStates[activeDatasourceId].state, + dataViews.indexPatterns + ) ); } diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index 7b9c345ff89f63..e41247557aa0af 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -33,6 +33,7 @@ export const { rollbackSuggestion, submitSuggestion, switchDatasource, + updateIndexPatterns, setToggleFullscreen, initEmpty, editVisualizationAction, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index e25c57ac129c9b..5f978ca9f3a586 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -12,8 +12,8 @@ import { setState, initEmpty, LensStoreDeps } from '..'; import { disableAutoApply, getPreloadedState } from '../lens_slice'; import { SharingSavedObjectProps } from '../../types'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; -import { getInitialDatasourceId } from '../../utils'; -import { initializeDatasources } from '../../editor_frame_service/editor_frame'; +import { getInitialDatasourceId, getInitialDataViewsObject } from '../../utils'; +import { initializeSources } from '../../editor_frame_service/editor_frame'; import { LensAppServices } from '../../app_plugin/types'; import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; import { Document } from '../../persistence'; @@ -102,21 +102,37 @@ export function loadInitial( getPreloadedState(storeDeps); const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; const { lens } = store.getState(); + + const loaderSharedArgs = { + dataViews: lensServices.dataViews, + storage: lensServices.storage, + defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'), + }; + if ( !initialInput || (attributeService.inputIsRefType(initialInput) && initialInput.savedObjectId === lens.persistedDoc?.savedObjectId) ) { - return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { - isFullEditor: true, - }) - .then((result) => { + return initializeSources( + { + datasourceMap, + datasourceStates: lens.datasourceStates, + initialContext, + ...loaderSharedArgs, + }, + { + isFullEditor: true, + } + ) + .then(({ states, indexPatterns, indexPatternRefs }) => { store.dispatch( initEmpty({ newState: { ...emptyState, + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), searchSessionId: data.search.session.getSessionId() || data.search.session.start(), - datasourceStates: Object.entries(result).reduce( + datasourceStates: Object.entries(states).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, [datasourceId]: { @@ -141,6 +157,40 @@ export function loadInitial( }); redirectCallback(); }); + // return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { + // isFullEditor: true, + // }) + // .then((result) => { + // store.dispatch( + // initEmpty({ + // newState: { + // ...emptyState, + // searchSessionId: data.search.session.getSessionId() || data.search.session.start(), + // datasourceStates: Object.entries(result).reduce( + // (state, [datasourceId, datasourceState]) => ({ + // ...state, + // [datasourceId]: { + // ...datasourceState, + // isLoading: false, + // }, + // }), + // {} + // ), + // isLoading: false, + // }, + // initialContext, + // }) + // ); + // if (autoApplyDisabled) { + // store.dispatch(disableAutoApply()); + // } + // }) + // .catch((e: { message: string }) => { + // notifications.toasts.addDanger({ + // title: e.message, + // }); + // redirectCallback(); + // }); } getPersisted({ initialInput, lensServices, history }) @@ -171,16 +221,28 @@ export function loadInitial( // Don't overwrite any pinned filters data.query.filterManager.setAppFilters(filters); - initializeDatasources( - datasourceMap, - docDatasourceStates, - doc.references, - initialContext, + // initializeDatasources( + // datasourceMap, + // docDatasourceStates, + // doc.references, + // initialContext, + // { + // isFullEditor: true, + // } + // ) + initializeSources( { - isFullEditor: true, - } + datasourceMap, + datasourceStates: docDatasourceStates, + references: doc.references, + initialContext, + dataViews: lensServices.dataViews, + storage: lensServices.storage, + defaultIndexPatternId: lensServices.uiSettings.get('defaultIndex'), + }, + { isFullEditor: true } ) - .then((result) => { + .then(({ states, indexPatterns, indexPatternRefs }) => { const currentSessionId = data.search.session.getSessionId(); store.dispatch( setState({ @@ -201,7 +263,8 @@ export function loadInitial( activeId: doc.visualizationType, state: doc.state.visualization, }, - datasourceStates: Object.entries(result).reduce( + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + datasourceStates: Object.entries(states).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, [datasourceId]: { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 2a71cd9aaab48a..6de39cb5d96819 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -15,7 +15,7 @@ import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import type { VisualizeEditorContext, Suggestion } from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; -import { LensAppState, LensStoreDeps, VisualizationState } from './types'; +import { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; import { Datasource, Visualization } from '../types'; import { generateId } from '../id_generator'; import type { LayerType } from '../../common/types'; @@ -39,6 +39,12 @@ export const initialState: LensAppState = { state: null, activeId: null, }, + dataViews: { + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + isFirstExistenceFetch: false, + }, }; export const getPreloadedState = ({ @@ -163,6 +169,17 @@ export const setLayerDefaultDimension = createAction<{ groupId: string; }>('lens/setLayerDefaultDimension'); +export const updateIndexPatterns = createAction>( + 'lens/updateIndexPatterns' +); +export const changeIndexPattern = createAction<{ + visualizationIds?: string[]; + datasourceIds?: string[]; + indexPatternId: string; + layerId?: string; + dataViews: Partial; +}>('lens/changeIndexPattern'); + export const lensActions = { setState, onActiveDataChange, @@ -188,6 +205,8 @@ export const lensActions = { removeOrClearLayer, addLayer, setLayerDefaultDimension, + updateIndexPatterns, + changeIndexPattern, }; export const makeLensReducer = (storeDeps: LensStoreDeps) => { @@ -282,6 +301,68 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ? activeVisualization.clearLayer(state.visualization.state, layerId) : activeVisualization.removeLayer(state.visualization.state, layerId); }, + [changeIndexPattern.type]: ( + state, + { + payload, + }: { + payload: { + visualizationIds?: string; + datasourceIds?: string; + layerId?: string; + indexPatternId: string; + dataViews: Pick; + }; + } + ) => { + const { visualizationIds, datasourceIds, layerId, indexPatternId, dataViews } = payload; + const newState: Partial = { dataViews: { ...state.dataViews, ...dataViews } }; + if (visualizationIds?.length) { + for (const visualizationId of visualizationIds) { + const activeVisualization = + visualizationId && + state.visualization.activeId !== visualizationId && + visualizationMap[visualizationId]; + if (activeVisualization && layerId && activeVisualization?.onIndexPatternChange) { + newState.visualization = { + ...state.visualization, + state: activeVisualization.onIndexPatternChange( + state.visualization.state, + layerId, + indexPatternId + ), + }; + } + } + } + if (datasourceIds?.length) { + newState.datasourceStates = { ...state.datasourceStates }; + for (const datasourceId of datasourceIds) { + const activeDatasource = datasourceId && datasourceMap[datasourceId]; + if (activeDatasource && activeDatasource?.onIndexPatternChange) { + newState.datasourceStates = { + ...newState.datasourceStates, + [datasourceId]: { + isLoading: false, + state: activeDatasource.onIndexPatternChange( + newState.datasourceStates[datasourceId].state, + dataViews.indexPatterns, + indexPatternId, + layerId + ), + }, + }; + } + } + } + return { ...state, ...newState }; + }, + [updateIndexPatterns.type]: (state, { payload }: { payload: Partial }) => { + return { + ...state, + dataViews: { ...state.dataViews, ...payload }, + }; + }, [updateDatasourceState.type]: ( state, { @@ -449,6 +530,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { datasourceStates: newState.datasourceStates, visualizationMap, visualizeTriggerFieldContext: payload.initialContext, + dataViews: newState.dataViews, }); if (suggestion) { return { @@ -635,6 +717,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), + dataViews: current(state.dataViews), }; const activeDatasource = datasourceMap[state.activeDatasourceId]; @@ -694,6 +777,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), dateRange: current(state.resolvedDateRange), + dataViews: current(state.dataViews), }, activeVisualization, activeDatasource, @@ -751,10 +835,15 @@ function addInitialValueIfAvailable({ if (!noDatasource && activeDatasource?.initializeDimension) { return { - activeDatasourceState: activeDatasource.initializeDimension(datasourceState, layerId, { - ...info, - columnId: columnId || info.columnId, - }), + activeDatasourceState: activeDatasource.initializeDimension( + datasourceState, + layerId, + framePublicAPI.dataViews.indexPatterns, + { + ...info, + columnId: columnId || info.columnId, + } + ), activeVisualizationState, }; } else { diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 9217c66863c5c9..a7e7a55e395925 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -27,6 +27,7 @@ export const selectChangesApplied = (state: LensState) => export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; export const selectActiveData = (state: LensState) => state.lens.activeData; +export const selectDataViews = (state: LensState) => state.lens.dataViews; export const selectIsFullscreenDatasource = (state: LensState) => Boolean(state.lens.isFullscreenDatasource); @@ -165,12 +166,14 @@ export const selectFramePublicAPI = createSelector( selectActiveData, selectInjectedDependencies as SelectInjectedDependenciesFunction, selectResolvedDateRange, + selectDataViews, ], - (datasourceStates, activeData, datasourceMap, dateRange) => { + (datasourceStates, activeData, datasourceMap, dateRange, dataViews) => { return { datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), activeData, dateRange, + dataViews, }; } ); diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 4c6f4adc59ff5d..7b461338261fc6 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -11,7 +11,7 @@ import { Filter, Query } from '@kbn/es-query'; import { SavedQuery } from '@kbn/data-plugin/public'; import { Document } from '../persistence'; -import { TableInspectorAdapter } from '../editor_frame_service/types'; +import type { TableInspectorAdapter } from '../editor_frame_service/types'; import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; import { @@ -19,13 +19,24 @@ import { VisualizationMap, SharingSavedObjectProps, VisualizeEditorContext, + IndexPattern, + IndexPatternRef, } from '../types'; export interface VisualizationState { activeId: string | null; state: unknown; } -export type DatasourceStates = Record; +export interface DataViewsState { + indexPatternRefs: IndexPatternRef[]; + indexPatterns: Record; + existingFields: Record>; + isFirstExistenceFetch: boolean; + existenceFetchFailed?: boolean; + existenceFetchTimeout?: boolean; +} + +export type DatasourceStates = Record; export interface PreviewState { visualization: VisualizationState; datasourceStates: DatasourceStates; @@ -53,6 +64,8 @@ export interface LensAppState extends EditorFrameState { searchSessionId: string; resolvedDateRange: DateRange; sharingSavedObjectProps?: Omit; + // Dataview/Indexpattern management has moved in here from datasource + dataViews: DataViewsState; } export type DispatchSetState = (state: Partial) => { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 215c0ef273cbba..192d0760639419 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -4,13 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Ast } from '@kbn/interpreter'; +import type { Ast } from '@kbn/interpreter'; import type { IconType } from '@elastic/eui/src/components/icon/icon'; import type { CoreSetup, SavedObjectReference, ResolvedSimpleSavedObject } from '@kbn/core/public'; import type { PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject } from 'react'; -import { Filter, TimeRange } from '@kbn/es-query'; +import type { Filter, TimeRange } from '@kbn/es-query'; import type { ExpressionAstExpression, ExpressionRendererEvent, @@ -24,8 +24,11 @@ import type { RowClickContext, VisualizeFieldContext, } from '@kbn/ui-actions-plugin/public'; -import { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; +import type { ClickTriggerEvent, BrushTriggerEvent } from '@kbn/charts-plugin/public'; +import type { IndexPatternAggRestrictions } from '@kbn/data-plugin/public'; +import type { FieldSpec } from '@kbn/data-views-plugin/common'; +import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; +import type { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; import type { DateRange, LayerType, SortingHint } from '../common'; import type { LensSortActionData, @@ -41,21 +44,57 @@ import { LENS_EDIT_PAGESIZE_ACTION, } from './datatable_visualization/components/constants'; import type { LensInspector } from './lens_inspector_service'; +import { DataViewsState } from './state_management/types'; +import { IndexPatternServiceAPI } from './data_views_service/service'; + +export interface IndexPatternRef { + id: string; + title: string; + name?: string; +} + +export interface IndexPattern { + id: string; + fields: IndexPatternField[]; + getFieldByName(name: string): IndexPatternField | undefined; + title: string; + name?: string; + timeFieldName?: string; + fieldFormatMap?: Record< + string, + { + id: string; + params: FieldFormatParams; + } + >; + hasRestrictions: boolean; +} + +export type IndexPatternField = FieldSpec & { + displayName: string; + aggregationRestrictions?: Partial; + meta?: boolean; + runtime?: boolean; +}; export type ErrorCallback = (e: { message: string }) => void; export interface PublicAPIProps { state: T; layerId: string; + indexPatterns: IndexPatternMap; } export interface EditorFrameProps { showNoDataPopover: () => void; lensInspector: LensInspector; + indexPatternService: IndexPatternServiceAPI; } export type VisualizationMap = Record; export type DatasourceMap = Record; +export type IndexPatternMap = Record; +export type ExistingFieldsMap = Record>; export interface EditorFrameInstance { EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement; @@ -206,6 +245,7 @@ export interface GetDropPropsArgs { prioritizedOperation?: string; isNewColumn?: boolean; }; + indexPatterns: IndexPatternMap; } /** @@ -221,8 +261,10 @@ export interface Datasource { state?: P, savedObjectReferences?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, + indexPatternRefs?: IndexPatternRef[], + indexPatterns?: IndexPatternMap, options?: InitializationOptions - ) => Promise; + ) => T; // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; @@ -232,10 +274,16 @@ export interface Datasource { removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; - removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; + removeColumn: (props: { + prevState: T; + layerId: string; + columnId: string; + indexPatterns: IndexPatternMap; + }) => T; initializeDimension?: ( state: T, layerId: string, + indexPatterns: IndexPatternMap, value: { columnId: string; groupId: string; @@ -283,33 +331,50 @@ export interface Datasource { state: T; setState: StateSetter; }) => void; + onIndexPatternChange?: ( + state: T, + indexPatterns: IndexPatternMap, + indexPatternId: string, + layerId?: string + ) => T; - refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; + onRefreshIndexPattern: () => void; - toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; + toExpression: ( + state: T, + layerId: string, + indexPatterns: IndexPatternMap + ) => ExpressionAstExpression | string | null; getDatasourceSuggestionsForField: ( state: T, field: unknown, - filterFn: (layerId: string) => boolean + filterFn: (layerId: string) => boolean, + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsForVisualizeCharts: ( state: T, - context: VisualizeEditorLayersContext[] + context: VisualizeEditorLayersContext[], + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsForVisualizeField: ( state: T, indexPatternId: string, - fieldName: string + fieldName: string, + indexPatterns: IndexPatternMap ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, + indexPatterns: IndexPatternMap, filterFn?: (layerId: string) => boolean, activeData?: Record ) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; - getErrorMessages: (state: T) => + getErrorMessages: ( + state: T, + indexPatterns: Record + ) => | Array<{ shortMessage: string; longMessage: React.ReactNode; @@ -323,7 +388,7 @@ export interface Datasource { /** * Check the internal state integrity and returns a list of missing references */ - checkIntegrity: (state: T) => string[]; + checkIntegrity: (state: T, indexPatterns: IndexPatternMap) => string[]; /** * The frame calls this function to display warnings about visualization */ @@ -335,11 +400,16 @@ export interface Datasource { /** * Checks if the visualization created is time based, for example date histogram */ - isTimeBased: (state: T) => boolean; + isTimeBased: (state: T, indexPatterns: IndexPatternMap) => boolean; /** * Given the current state layer and a columnId will verify if the column configuration has errors */ - isValidColumn: (state: T, layerId: string, columnId: string) => boolean; + isValidColumn: ( + state: T, + indexPatterns: IndexPatternMap, + layerId: string, + columnId: string + ) => boolean; /** * Are these datasources equivalent? */ @@ -414,7 +484,10 @@ export interface DatasourceDataPanelProps { filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; + onChangeIndexPattern: (indexPatternId: string, datasourceId: string, layerId?: string) => void; uiActions: UiActionsStart; + indexPatternService: IndexPatternServiceAPI; + frame: FramePublicAPI; } interface SharedDimensionProps { @@ -437,6 +510,8 @@ export type DatasourceDimensionProps = SharedDimensionProps & { onRemove?: (accessor: string) => void; state: T; activeData?: Record; + indexPatterns: IndexPatternMap; + existingFields: Record>; hideTooltip?: boolean; invalid?: boolean; invalidMessage?: string; @@ -473,6 +548,8 @@ export interface DatasourceLayerPanelProps { state: T; setState: StateSetter; activeData?: Record; + dataViews: DataViewsState; + onChangeIndexPattern: (indexPatternId: string, datasourceId: string, layerId?: string) => void; } export interface DragDropOperation { @@ -506,6 +583,7 @@ export interface DatasourceDimensionDropProps { export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { source: DragDropIdentifier; dropType: DropType; + indexPatterns: IndexPatternMap; }; export type FieldOnlyDataType = @@ -562,6 +640,11 @@ export interface VisualizationConfigProps { export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; + onChangeIndexPattern: (indexPatternId: string, layerId: string) => void; +}; + +export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps & { + defaultIndexPatternId: string; }; export interface VisualizationToolbarProps { @@ -727,6 +810,7 @@ export interface FramePublicAPI { * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; + dataViews: DataViewsState; } export interface FrameDatasourceAPI extends FramePublicAPI { @@ -786,6 +870,15 @@ export interface Visualization { */ initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T; + /** + * Retrieve the used indexpatterns in the visualization + */ + getUsedIndexPatterns?: ( + state?: T, + indexPatternRefs?: IndexPatternRef[], + savedObjectReferences?: SavedObjectReference[] + ) => { usedPatterns: string[] }; + getMainPalette?: (state: T) => undefined | PaletteOutput; /** @@ -816,7 +909,7 @@ export interface Visualization { /** 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) => T; + appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId?: string) => T; /** Retrieve a list of supported layer types with initialization data */ getSupportedLayers: ( @@ -847,13 +940,22 @@ export interface Visualization { }; /** - * Header rendered as layer title This can be used for both static and dynamic content lioke + * Header rendered as layer title. This can be used for both static and dynamic content like * for extra configurability, such as for switch chart type */ renderLayerHeader?: ( domElement: Element, props: VisualizationLayerWidgetProps ) => ((cleanupElement: Element) => void) | void; + + /** + * Layer panel content rendered. This can be used to render a custom content below the title, + * like a custom dataview switch + */ + renderLayerPanel?: ( + domElement: Element, + props: VisualizationLayerWidgetProps + ) => ((cleanupElement: Element) => void) | void; /** * Toolbar rendered above the visualization. This is meant to be used to provide chart-level * settings for the visualization. @@ -971,6 +1073,11 @@ export interface Visualization { */ onEditAction?: (state: T, event: LensEditEvent) => T; + /** + * Some visualization track indexPattern changes (i.e. annotations) + * This method makes it aware of the change and produces a new updated state + */ + onIndexPatternChange?: (state: T, indexPatternId: string, layerId?: string) => T; /** * Gets custom display options for showing the visualization. */ diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 4b0f24ce058356..13ff13b6ab93b0 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -14,8 +14,15 @@ import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public' import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public'; import type { Document } from './persistence/saved_object_store'; -import type { Datasource, DatasourceMap, Visualization, StateSetter } from './types'; +import type { + Datasource, + DatasourceMap, + Visualization, + IndexPatternMap, + IndexPatternRef, +} from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; +import { IndexPatternServiceAPI } from './data_views_service/service'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { @@ -58,43 +65,69 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; -export function handleIndexPatternChange({ - activeDatasources, - datasourceStates, - indexPatternId, - setDatasourceState, -}: { - activeDatasources: Record; - datasourceStates: DatasourceStates; - indexPatternId: string; - setDatasourceState: StateSetter; -}): void { - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasource?.updateCurrentIndexPatternId?.({ - state: datasourceStates[id].state, - indexPatternId, - setState: setDatasourceState, - }); - }); +export function getInitialDataViewsObject( + indexPatterns: IndexPatternMap, + indexPatternRefs: IndexPatternRef[] +) { + return { + indexPatterns, + indexPatternRefs, + existingFields: {}, + isFirstExistenceFetch: true, + }; } -export function refreshIndexPatternsList({ +export async function refreshIndexPatternsList({ activeDatasources, + indexPatternService, indexPatternId, - setDatasourceState, + indexPatternsCache, }: { + indexPatternService: IndexPatternServiceAPI; activeDatasources: Record; indexPatternId: string; - setDatasourceState: StateSetter; -}): void { - Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasource?.refreshIndexPatternsList?.({ - indexPatternId, - setState: setDatasourceState, - }); + indexPatternsCache: IndexPatternMap; +}) { + // collect all the onRefreshIndex callbacks from datasources + const onRefreshCallbacks = Object.values(activeDatasources) + .map((datasource) => datasource?.onRefreshIndexPattern) + .filter(Boolean); + + const [newlyMappedIndexPattern, indexPatternRefs] = await Promise.all([ + indexPatternService.loadIndexPatterns({ + cache: {}, + patterns: [indexPatternId], + onIndexPatternRefresh: () => onRefreshCallbacks.forEach((fn) => fn()), + }), + indexPatternService.loadIndexPatternRefs({ isFullEditor: true }), + ]); + const indexPattern = newlyMappedIndexPattern[indexPatternId]; + indexPatternService.updateIndexPatternsCache({ + indexPatterns: { + ...indexPatternsCache, + [indexPatternId]: indexPattern, + }, + indexPatternRefs, }); } +// export function refreshIndexPatternsList({ +// activeDatasources, +// indexPatternId, +// setDatasourceState, +// }: { +// activeDatasources: Record; +// indexPatternId: string; +// setDatasourceState: StateSetter; +// }): void { +// Object.entries(activeDatasources).forEach(([id, datasource]) => { +// datasource?.refreshIndexPatternsList?.({ +// indexPatternId, +// setState: setDatasourceState, +// }); +// }); +// } + export function getIndexPatternsIds({ activeDatasources, datasourceStates, @@ -121,9 +154,9 @@ export function getIndexPatternsIds({ export async function getIndexPatternsObjects( ids: string[], - indexPatternsService: DataViewsContract + dataViews: DataViewsContract ): Promise<{ indexPatterns: DataView[]; rejectedIds: string[] }> { - const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); + const responses = await Promise.allSettled(ids.map((id) => dataViews.get(id))); const fullfilled = responses.filter( (response): response is PromiseFulfilledResult => response.status === 'fulfilled' ); diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index a7de5374c92d14..30c58d2a728b74 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -6,6 +6,7 @@ */ import type { CoreSetup } from '@kbn/core/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public'; import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; @@ -29,12 +30,15 @@ export class XyVisualization { ) { editorFrame.registerVisualization(async () => { const { getXyVisualization } = await import('../async_services'); - const [, { charts, data, fieldFormats, eventAnnotation }] = await core.getStartServices(); + const [coreStart, { data, charts, fieldFormats, eventAnnotation }] = + await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); const eventAnnotationService = await eventAnnotation.getService(); const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); return getXyVisualization({ - datatableUtilities: data.datatableUtilities, + core: coreStart, + data, + storage: new Storage(localStorage), paletteService: palettes, eventAnnotationService, fieldFormats, diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 58c4cfb7e26c4b..a763053d724750 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -113,6 +113,8 @@ export interface XYAnnotationLayerConfig { layerId: string; layerType: 'annotations'; annotations: EventAnnotationConfig[]; + hide?: boolean; + indexPatternId: string; simpleView?: boolean; } diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 2eb15c96afe95f..aedbcbb648957b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -11,13 +11,14 @@ import { Position } from '@elastic/charts'; import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { PaletteRegistry } from '@kbn/coloring'; -import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { ThemeServiceStart } from '@kbn/core/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 { 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'; +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'; @@ -79,14 +80,18 @@ import { defaultAnnotationLabel } from './annotations/helpers'; import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; export const getXyVisualization = ({ - datatableUtilities, + core, + storage, + data, paletteService, fieldFormats, useLegacyTimeAxis, kibanaTheme, eventAnnotationService, }: { - datatableUtilities: DatatableUtilitiesService; + core: CoreStart; + storage: IStorageWrapper; + data: DataPublicPluginStart; paletteService: PaletteRegistry; eventAnnotationService: EventAnnotationServiceType; fieldFormats: FieldFormatsStart; @@ -116,7 +121,7 @@ export const getXyVisualization = ({ }; }, - appendLayer(state, layerId, layerType) { + appendLayer(state, layerId, layerType, indexPatternId) { const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType; return { ...state, @@ -126,6 +131,7 @@ export const getXyVisualization = ({ seriesType: firstUsedSeriesType || state.preferredSeriesType, layerId, layerType, + indexPatternId: indexPatternId ?? core.uiSettings.get('defaultIndex'), }), ], }; @@ -137,7 +143,11 @@ export const getXyVisualization = ({ layers: state.layers.map((l) => l.layerId !== layerId ? l - : newLayerState({ seriesType: state.preferredSeriesType, layerId }) + : newLayerState({ + seriesType: state.preferredSeriesType, + layerId, + indexPatternId: core.uiSettings.get('defaultIndex'), + }) ), }; }, @@ -189,6 +199,20 @@ export const getXyVisualization = ({ ]; }, + onIndexPatternChange(state, indexPatternId, layerId) { + const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[layerIndex]; + if (!layer || !isAnnotationsLayer(layer)) { + return state; + } + const newLayers = [...state.layers]; + newLayers[layerIndex] = { ...layer, indexPatternId }; + return { + ...state, + layers: newLayers, + }; + }, + getConfiguration({ state, frame, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); if (!layer) { @@ -520,7 +544,7 @@ export const getXyVisualization = ({ renderDimensionEditor(domElement, props) { const allProps = { ...props, - datatableUtilities, + datatableUtilities: data.datatableUtilities, formatFactory: fieldFormats.deserialize, paletteService, }; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index e89edab464bfdf..fb364d7bd09f77 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -277,10 +277,17 @@ const newLayerFn = { layerType: layerTypes.REFERENCELINE, accessors: [], }), - [layerTypes.ANNOTATIONS]: ({ layerId }: { layerId: string }): XYAnnotationLayerConfig => ({ + [layerTypes.ANNOTATIONS]: ({ + layerId, + indexPatternId, + }: { + layerId: string; + indexPatternId: string; + }): XYAnnotationLayerConfig => ({ layerId, layerType: layerTypes.ANNOTATIONS, annotations: [], + indexPatternId, }), }; @@ -288,12 +295,14 @@ export function newLayerState({ layerId, layerType = layerTypes.DATA, seriesType, + indexPatternId, }: { layerId: string; layerType?: LayerType; seriesType: SeriesType; + indexPatternId: string; }) { - return newLayerFn[layerType]({ layerId, seriesType }); + return newLayerFn[layerType]({ layerId, seriesType, indexPatternId }); } export function getLayersByType(state: State, byType?: string) { From 751c2a2fcedfe145433869be200dc29dc1d5ddae Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 18:05:11 +0200 Subject: [PATCH 09/98] :fire: Remove some old cruft from the code --- .../lens/public/app_plugin/lens_top_nav.tsx | 2 +- .../init_middleware/load_initial.ts | 43 ------------------- x-pack/plugins/lens/public/types.ts | 9 ---- x-pack/plugins/lens/public/utils.ts | 17 -------- 4 files changed, 1 insertion(+), 70 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 98acf40dedb74c..12c8c76306246f 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -694,7 +694,7 @@ export const LensTopNavMenu = ({ closeDataViewEditor.current = dataViewEditor.openEditor({ onSave: async (dataView) => { if (dataView.id) { - await dispatchChangeIndexPattern(dataView.id); + dispatchChangeIndexPattern(dataView.id); refreshFieldList(); } }, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 5f978ca9f3a586..1e39fa55f4b046 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -157,40 +157,6 @@ export function loadInitial( }); redirectCallback(); }); - // return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { - // isFullEditor: true, - // }) - // .then((result) => { - // store.dispatch( - // initEmpty({ - // newState: { - // ...emptyState, - // searchSessionId: data.search.session.getSessionId() || data.search.session.start(), - // datasourceStates: Object.entries(result).reduce( - // (state, [datasourceId, datasourceState]) => ({ - // ...state, - // [datasourceId]: { - // ...datasourceState, - // isLoading: false, - // }, - // }), - // {} - // ), - // isLoading: false, - // }, - // initialContext, - // }) - // ); - // if (autoApplyDisabled) { - // store.dispatch(disableAutoApply()); - // } - // }) - // .catch((e: { message: string }) => { - // notifications.toasts.addDanger({ - // title: e.message, - // }); - // redirectCallback(); - // }); } getPersisted({ initialInput, lensServices, history }) @@ -221,15 +187,6 @@ export function loadInitial( // Don't overwrite any pinned filters data.query.filterManager.setAppFilters(filters); - // initializeDatasources( - // datasourceMap, - // docDatasourceStates, - // doc.references, - // initialContext, - // { - // isFullEditor: true, - // } - // ) initializeSources( { datasourceMap, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 192d0760639419..66f0a54b1f2da1 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -870,15 +870,6 @@ export interface Visualization { */ initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T; - /** - * Retrieve the used indexpatterns in the visualization - */ - getUsedIndexPatterns?: ( - state?: T, - indexPatternRefs?: IndexPatternRef[], - savedObjectReferences?: SavedObjectReference[] - ) => { usedPatterns: string[] }; - getMainPalette?: (state: T) => undefined | PaletteOutput; /** diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 13ff13b6ab93b0..2c848758b0975d 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -111,23 +111,6 @@ export async function refreshIndexPatternsList({ }); } -// export function refreshIndexPatternsList({ -// activeDatasources, -// indexPatternId, -// setDatasourceState, -// }: { -// activeDatasources: Record; -// indexPatternId: string; -// setDatasourceState: StateSetter; -// }): void { -// Object.entries(activeDatasources).forEach(([id, datasource]) => { -// datasource?.refreshIndexPatternsList?.({ -// indexPatternId, -// setState: setDatasourceState, -// }); -// }); -// } - export function getIndexPatternsIds({ activeDatasources, datasourceStates, From c7a9ce8c88ae70e661f4a08e11aa9e500108f1e3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 18:57:22 +0200 Subject: [PATCH 10/98] :bug: Fix dataViews layer change --- .../editor_frame/config_panel/layer_panel.tsx | 28 -------- .../indexpattern_datasource/datapanel.tsx | 14 +--- .../public/state_management/lens_slice.ts | 64 +++++++++++++------ x-pack/plugins/lens/public/types.ts | 1 - 4 files changed, 47 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 393aabdb04e744..915a1e05e6ec69 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -347,34 +347,6 @@ export function LayerPanel( dataViews, onChangeIndexPattern: (indexPatternId) => onChangeIndexPattern({ indexPatternId, layerId, datasourceId }), - setState: (updater: unknown) => { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - indexPatterns: dataViews.indexPatterns, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame: framePublicAPI, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, }} /> )} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index b46155edf05037..17f065c4f05f58 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -40,7 +40,6 @@ import type { FramePublicAPI, IndexPattern, IndexPatternField, - StateSetter, } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import type { IndexPatternPrivateState } from './types'; @@ -57,11 +56,7 @@ export type Props = Omit< data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; - changeIndexPattern: ( - id: string, - state: IndexPatternPrivateState, - setState: StateSetter - ) => void; + changeIndexPattern: (id: string) => void; charts: ChartsPluginSetup; core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; @@ -125,7 +120,6 @@ function buildSafeEsQuery( } export function IndexPatternDataPanel({ - setState, state, dragDropContext, core, @@ -149,10 +143,6 @@ export function IndexPatternDataPanel({ const { indexPatterns, indexPatternRefs, existingFields, isFirstExistenceFetch } = frame.dataViews; const { currentIndexPatternId } = state; - const onChangeIndexPattern = useCallback( - (id: string) => changeIndexPattern(id, state, setState), - [state, setState, changeIndexPattern] - ); const indexPatternList = uniq( Object.values(state.layers) @@ -231,7 +221,7 @@ export function IndexPatternDataPanel({ fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={indexPatternFieldEditor} - onChangeIndexPattern={onChangeIndexPattern} + onChangeIndexPattern={changeIndexPattern} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} uiActions={uiActions} diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 6de39cb5d96819..e4050d09c3ed86 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -13,7 +13,7 @@ import { History } from 'history'; import { LensEmbeddableInput } from '..'; import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; import { TableInspectorAdapter } from '../editor_frame_service/types'; -import type { VisualizeEditorContext, Suggestion } from '../types'; +import type { VisualizeEditorContext, Suggestion, DatasourceMap } from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; import { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; import { Datasource, Visualization } from '../types'; @@ -337,6 +337,9 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { } if (datasourceIds?.length) { newState.datasourceStates = { ...state.datasourceStates }; + const frame = createFrameAPI(state, datasourceMap); + const datasourceLayers = frame.datasourceLayers; + for (const datasourceId of datasourceIds) { const activeDatasource = datasourceId && datasourceMap[datasourceId]; if (activeDatasource && activeDatasource?.onIndexPatternChange) { @@ -352,9 +355,38 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { ), }, }; + // Update the visualization columns + if (layerId && state.visualization.activeId) { + const nextPublicAPI = activeDatasource.getPublicAPI({ + state: newState.datasourceStates[datasourceId].state, + layerId, + indexPatterns: dataViews.indexPatterns, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourceLayers[layerId] + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + const nextVisState = (newState.visualization || state.visualization).state; + const activeVisualization = visualizationMap[state.visualization.activeId]; + removed.forEach((columnId) => { + newState.visualization = { + ...state.visualization, + state: activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + frame, + }), + }; + }); + } } } } + return { ...state, ...newState }; }, [updateIndexPatterns.type]: (state, { payload }: { payload: Partial }) => { @@ -710,15 +742,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { layerType ); - const framePublicAPI = { - // any better idea to avoid `as`? - activeData: state.activeData - ? (current(state.activeData) as TableInspectorAdapter) - : undefined, - datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), - dateRange: current(state.resolvedDateRange), - dataViews: current(state.dataViews), - }; + const framePublicAPI = createFrameAPI(state, datasourceMap); const activeDatasource = datasourceMap[state.activeDatasourceId]; const { noDatasource } = @@ -770,15 +794,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({ datasourceState: state.datasourceStates[state.activeDatasourceId].state, visualizationState: state.visualization.state, - framePublicAPI: { - // any better idea to avoid `as`? - activeData: state.activeData - ? (current(state.activeData) as TableInspectorAdapter) - : undefined, - datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), - dateRange: current(state.resolvedDateRange), - dataViews: current(state.dataViews), - }, + framePublicAPI: createFrameAPI(state, datasourceMap), activeVisualization, activeDatasource, layerId, @@ -793,6 +809,16 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { }); }; +function createFrameAPI(state: LensAppState, datasourceMap: DatasourceMap) { + return { + // any better idea to avoid `as`? + activeData: state.activeData ? (current(state.activeData) as TableInspectorAdapter) : undefined, + datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), + dateRange: current(state.resolvedDateRange), + dataViews: current(state.dataViews), + }; +} + function addInitialValueIfAvailable({ visualizationState, datasourceState, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 66f0a54b1f2da1..5141834d14e802 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -546,7 +546,6 @@ export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; state: T; - setState: StateSetter; activeData?: Record; dataViews: DataViewsState; onChangeIndexPattern: (indexPatternId: string, datasourceId: string, layerId?: string) => void; From 79566af6d33fdcfbe1a13d63986c3839a8abc52e Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 19:23:48 +0200 Subject: [PATCH 11/98] :bug: Fix datasourceLayers refs --- .../editor_frame/state_helpers.ts | 8 ++++---- .../editor_frame/suggestion_panel.tsx | 3 ++- .../definitions/filters/filter_popover.tsx | 2 +- .../lens/public/state_management/lens_slice.ts | 16 ++++++++++++---- .../lens/public/state_management/selectors.ts | 10 ++++++++-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 6017a7125abaeb..b4b6a79975f71d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -213,7 +213,8 @@ export function initializeDatasources({ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceStates: DatasourceStates, - datasourceMap: DatasourceMap + datasourceMap: DatasourceMap, + indexPatterns: DataViewsState['indexPatterns'] ) { const datasourceLayers: DatasourceLayers = {}; Object.keys(datasourceMap) @@ -227,8 +228,7 @@ export const getDatasourceLayers = memoizeOne(function getDatasourceLayers( datasourceLayers[layer] = datasourceMap[id].getPublicAPI({ state: datasourceState, layerId: layer, - // @TODO - indexPatterns: {}, + indexPatterns, }); }); }); @@ -290,7 +290,7 @@ export async function persistedStateToExpression( indexPatternRefs, }); - const datasourceLayers = getDatasourceLayers(datasourceStates, datasourceMap); + const datasourceLayers = getDatasourceLayers(datasourceStates, datasourceMap, indexPatterns); const datasourceId = getActiveDatasourceIdFromDoc(doc); if (datasourceId == null) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 7400aa6dd8fab0..24e65e75ec090b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -252,7 +252,8 @@ export function SuggestionPanel({ }, } : {}, - datasourceMap + datasourceMap, + frame.dataViews.indexPatterns ), } ) == null diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index 7166da0a3c0dfd..c4ab33c36f1f1e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -10,8 +10,8 @@ import './filter_popover.scss'; import React from 'react'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; +import { IndexPattern } from '../../../../types'; import { FilterValue, defaultLabel, isQueryValid } from '.'; -import { IndexPattern } from '../../../types'; import { LabelInput } from '../shared_components'; import { QueryInput } from '../../../query_input'; diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index e4050d09c3ed86..9db4806d6885f7 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -337,7 +337,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { } if (datasourceIds?.length) { newState.datasourceStates = { ...state.datasourceStates }; - const frame = createFrameAPI(state, datasourceMap); + const frame = createFrameAPI(state, datasourceMap, newState.dataViews); const datasourceLayers = frame.datasourceLayers; for (const datasourceId of datasourceIds) { @@ -809,13 +809,21 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { }); }; -function createFrameAPI(state: LensAppState, datasourceMap: DatasourceMap) { +function createFrameAPI( + state: LensAppState, + datasourceMap: DatasourceMap, + dataViews: DataViewsState = current(state.dataViews) +) { return { // any better idea to avoid `as`? activeData: state.activeData ? (current(state.activeData) as TableInspectorAdapter) : undefined, - datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), + datasourceLayers: getDatasourceLayers( + state.datasourceStates, + datasourceMap, + dataViews.indexPatterns + ), dateRange: current(state.resolvedDateRange), - dataViews: current(state.dataViews), + dataViews, }; } diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index a7e7a55e395925..f1f53197978fa9 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -156,8 +156,10 @@ export const selectDatasourceLayers = createSelector( [ selectDatasourceStates, selectInjectedDependencies as SelectInjectedDependenciesFunction, + selectDataViews, ], - (datasourceStates, datasourceMap) => getDatasourceLayers(datasourceStates, datasourceMap) + (datasourceStates, datasourceMap, dataViews) => + getDatasourceLayers(datasourceStates, datasourceMap, dataViews.indexPatterns) ); export const selectFramePublicAPI = createSelector( @@ -170,7 +172,11 @@ export const selectFramePublicAPI = createSelector( ], (datasourceStates, activeData, datasourceMap, dateRange, dataViews) => { return { - datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap), + datasourceLayers: getDatasourceLayers( + datasourceStates, + datasourceMap, + dataViews.indexPatterns + ), activeData, dateRange, dataViews, From 4822b3674d209d1ef2f8e44926e0dc6ce9536196 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 27 Jul 2022 19:51:16 +0200 Subject: [PATCH 12/98] :fire: Remove more old cruft --- .../lens/public/indexpattern_datasource/types.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 06eedb2848363e..8c7cef5d59a113 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -66,17 +66,6 @@ export type PersistedIndexPatternLayer = Omit; - // indexPatternRefs: IndexPatternRef[]; - // indexPatterns: Record; - - // /** - // * indexPatternId -> fieldName -> boolean - // */ - // existingFields: Record>; - // isFirstExistenceFetch: boolean; - // existenceFetchFailed?: boolean; - // existenceFetchTimeout?: boolean; - isDimensionClosePrevented?: boolean; } From 220aa39d7508c11dc67290c2d85760478d0a73f3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 28 Jul 2022 11:21:48 +0200 Subject: [PATCH 13/98] :bug: Fix bug when loading SO --- .../public/editor_frame_service/editor_frame/state_helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index b4b6a79975f71d..f9136af24dde83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -175,6 +175,7 @@ export async function initializeSources( initialContext, indexPatternRefs, indexPatterns, + references, }), }; } From 759f19ad89d6ba0989fa3ba9cc69282d6f78ad00 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 28 Jul 2022 11:24:14 +0200 Subject: [PATCH 14/98] :bug: Fix initial existence flag --- x-pack/plugins/lens/public/state_management/lens_slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 9db4806d6885f7..7bee1b25e2ae68 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -43,7 +43,7 @@ export const initialState: LensAppState = { indexPatternRefs: [], indexPatterns: {}, existingFields: {}, - isFirstExistenceFetch: false, + isFirstExistenceFetch: true, }, }; From 7f590daa5d306d972cd1ec9e843dbca84fe4036b Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 28 Jul 2022 14:51:42 +0200 Subject: [PATCH 15/98] :label: Fix type issues --- .../lens/public/data_views_service/loader.ts | 2 +- .../lens/public/embeddable/embeddable.tsx | 28 ++- .../change_indexpattern.tsx | 2 +- .../dimension_panel/bucket_nesting_editor.tsx | 3 +- .../dimension_panel/dimension_editor.tsx | 1 + .../dimension_panel/droppable/mocks.ts | 6 +- .../dimension_panel/field_input.tsx | 2 +- .../dimension_panel/reference_editor.tsx | 12 +- .../dimension_panel/time_shift.tsx | 5 +- .../indexpattern_datasource/document_field.ts | 2 +- .../indexpattern_datasource/indexpattern.tsx | 4 +- .../definitions/calculations/utils.ts | 3 +- .../operations/definitions/count.tsx | 2 +- .../definitions/filters/filters.tsx | 4 +- .../formula/editor/formula_help.tsx | 2 +- .../formula/editor/math_completion.ts | 2 +- .../definitions/formula/formula.tsx | 2 +- .../definitions/formula/formula_public_api.ts | 5 +- .../operations/definitions/formula/math.tsx | 2 +- .../operations/definitions/formula/parse.ts | 3 +- .../definitions/formula/validation.ts | 3 +- .../operations/definitions/helpers.tsx | 3 +- .../operations/definitions/last_value.tsx | 2 +- .../operations/definitions/ranges/ranges.tsx | 2 +- .../definitions/shared_components/index.tsx | 2 +- .../operations/definitions/static_value.tsx | 2 +- .../definitions/terms/field_inputs.tsx | 17 +- .../operations/definitions/terms/helpers.ts | 6 +- .../operations/definitions/terms/index.tsx | 2 +- .../public/indexpattern_datasource/types.ts | 2 +- .../drag_drop_bucket/buckets.test.tsx | 79 --------- .../drag_drop_bucket/buckets.tsx | 160 ------------------ .../lens/public/shared_components/index.ts | 5 - x-pack/plugins/lens/public/types.ts | 3 +- 34 files changed, 76 insertions(+), 304 deletions(-) delete mode 100644 x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.test.tsx delete mode 100644 x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index 89d1765bc61afe..ac1a894ed5809f 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -28,7 +28,7 @@ export function getFieldByNameFactory(newFields: IndexPatternField[]) { export function convertDataViewIntoLensIndexPattern( dataView: DataView, - restrictionRemapper: (name: string) => string + restrictionRemapper: (name: string) => string = onRestrictionMapping ): IndexPattern { const newFields = dataView.fields .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index de4434a17f05ab..75787ba08402fe 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -66,6 +66,7 @@ import { Visualization, DatasourceMap, Datasource, + IndexPatternMap, } from '../types'; import { getEditPath, DOC_TYPE } from '../../common'; @@ -75,6 +76,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 '../data_views_service/loader'; export type LensSavedObjectAttributes = Omit; @@ -169,6 +171,7 @@ function getViewUnderlyingDataArgs({ filters, timeRange, esQueryConfig, + indexPatternsCache, }: { activeDatasource: Datasource; activeDatasourceState: unknown; @@ -179,11 +182,13 @@ function getViewUnderlyingDataArgs({ filters: Filter[]; timeRange: TimeRange; esQueryConfig: EsQueryConfig; + indexPatternsCache: IndexPatternMap; }) { const { error, meta } = getLayerMetaInfo( activeDatasource, activeDatasourceState, activeData, + indexPatternsCache, timeRange, capabilities ); @@ -749,7 +754,7 @@ export class Embeddable private async loadViewUnderlyingDataArgs(): Promise { const mergedSearchContext = this.getMergedSearchContext(); - if (!this.activeDataInfo.activeData || !mergedSearchContext.timeRange) { + if (!this.activeDataInfo.activeData || !mergedSearchContext.timeRange || !this.savedVis) { return false; } @@ -761,12 +766,22 @@ export class Embeddable this.activeDataInfo.activeDatasource = this.deps.datasourceMap[activeDatasourceId]; const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId]; + const indexPatternsCache = this.indexPatterns.reduce( + (acc, indexPattern) => ({ + [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern), + ...acc, + }), + {} + ); + if (!this.activeDataInfo.activeDatasourceState) { - this.activeDataInfo.activeDatasourceState = - await this.activeDataInfo.activeDatasource.initialize( - docDatasourceState, - this.savedVis?.references - ); + this.activeDataInfo.activeDatasourceState = this.activeDataInfo.activeDatasource.initialize( + docDatasourceState, + this.savedVis?.references, + undefined, + undefined, + indexPatternsCache + ); } const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({ @@ -779,6 +794,7 @@ export class Embeddable filters: mergedSearchContext.filters || [], timeRange: mergedSearchContext.timeRange, esQueryConfig: getEsQueryConfig(this.deps.uiSettings), + indexPatternsCache, }); const loaded = typeof viewUnderlyingDataArgs !== 'undefined'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 256f452c2ab2ab..75ebbfdeb27a41 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -10,7 +10,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; -import { IndexPatternRef } from './types'; +import type { IndexPatternRef } from '../types'; export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & { label: string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 46ed51f35d9b03..31a4b91706dfac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -8,9 +8,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiSelect } from '@elastic/eui'; -import { IndexPatternLayer, IndexPatternField } from '../types'; +import { IndexPatternLayer } from '../types'; import { hasField } from '../pure_utils'; import { GenericIndexPatternColumn } from '../operations'; +import { IndexPatternField } from '../../types'; function nestColumn(columnOrder: string[], outer: string, inner: string) { const result = columnOrder.filter((c) => c !== inner); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 87f9c64075ef77..71d71f51fb34d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -63,6 +63,7 @@ import { WrappingHelpPopover } from '../help_popover'; import { isColumn } from '../operations/definitions/helpers'; import type { FieldChoiceWithOperationType } from './field_select'; import type { IndexPattern, IndexPatternField } from '../../types'; +import { documentField } from '../document_field'; export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { selectedColumn?: GenericIndexPatternColumn; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts index 40121cf99f5467..19dfaf3ac7c204 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { IndexPattern, IndexPatternLayer } from '../../types'; +import { IndexPatternLayer } from '../../types'; import { documentField } from '../../document_field'; -import { OperationMetadata } from '../../../types'; +import { IndexPatternMap, OperationMetadata } from '../../../types'; import { DateHistogramIndexPatternColumn, GenericIndexPatternColumn, @@ -17,7 +17,7 @@ import { import { getFieldByNameFactory } from '../../pure_helpers'; jest.mock('../../../id_generator'); -export const mockDataViews = (): Record => { +export const mockDataViews = (): IndexPatternMap => { const fields = [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.tsx index ad3aa97b2a0ea0..ec471b70de614e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.tsx @@ -62,7 +62,7 @@ export function FieldInput({ ; isInline?: boolean; - existingFields: IndexPatternPrivateState['existingFields']; + existingFields: ExistingFieldsMap; dateRange: DateRange; labelAppend?: EuiFormRowProps['labelAppend']; isFullscreen: boolean; @@ -305,7 +305,7 @@ export const ReferenceEditor = (props: ReferenceEditorProps) => { , - options?: InitializationOptions + indexPatterns?: Record ) { return loadInitialState({ persistedState, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index 5bae2f5a1865f4..f85bf0b194e2e7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import type { AstFunction } from '@kbn/interpreter'; import memoizeOne from 'memoize-one'; +import type { IndexPattern } from '../../../../types'; import { LayerType, layerTypes } from '../../../../../common'; import type { TimeScaleUnit } from '../../../../../common/expressions'; -import type { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index d3c0b9700379ce..9bf4522705f9f2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -14,7 +14,7 @@ import { buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { TimeScaleUnit } from '../../../../common/expressions'; import { OperationDefinition, ParamEditorProps } from '.'; import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types'; -import { IndexPatternField } from '../../types'; +import type { IndexPatternField } from '../../../types'; import { getInvalidFieldMessage, getFilter, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 3890494be8b464..cee188b6f483ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -15,12 +15,12 @@ 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 { IndexPattern } from '../../../../types'; import { updateColumnParam } from '../../layer_helpers'; import type { OperationDefinition } from '..'; import type { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; -import type { IndexPattern } from '../../../types'; -import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; +import { DragDropBuckets, DraggableBucketContainer, NewBucketButton } from '../shared_components'; const generateId = htmlIdGenerator(); const OPERATION_NAME = 'filters'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx index bdc774f9d15f38..c6fa7e56edb02b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_help.tsx @@ -21,7 +21,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { Markdown } from '@kbn/kibana-react-plugin/public'; -import { IndexPattern } from '../../../../types'; +import type { IndexPattern } from '../../../../../types'; import { tinymathFunctions } from '../util'; import { getPossibleFunctions } from './math_completion'; import { hasFunctionFieldArgument } from '../validation'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index 95833a4508fdf7..c3cf81b0e92b3e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -22,7 +22,7 @@ import type { } from '@kbn/unified-search-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; -import { IndexPattern } from '../../../../types'; +import type { IndexPattern } from '../../../../../types'; import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util'; import type { GenericOperationDefinition } from '../..'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index 72aace21479acf..c3562fb649665d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { BaseIndexPatternColumn, OperationDefinition } from '..'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; -import type { IndexPattern } from '../../../types'; +import type { IndexPattern } from '../../../../types'; import { runASTValidation, tryToParse } from './validation'; import { WrappedFormulaEditor } from './editor'; import { insertOrReplaceFormulaColumn } from './parse'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts index 8f431afa129e07..4085ec931d3a6d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts @@ -6,10 +6,11 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; -import type { IndexPattern, PersistedIndexPatternLayer } from '../../../types'; +import { convertDataViewIntoLensIndexPattern } from '../../../../data_views_service/loader'; +import type { IndexPattern } from '../../../../types'; +import type { PersistedIndexPatternLayer } from '../../../types'; import { insertOrReplaceFormulaColumn } from './parse'; -import { convertDataViewIntoLensIndexPattern } from '../../../loader'; /** @public **/ export interface FormulaPublicApi { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index d7f25275f63a27..b05c9f7c0fd218 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -8,7 +8,7 @@ import type { TinymathAST } from '@kbn/tinymath'; import { OperationDefinition } from '..'; import { ValueFormatConfig, ReferenceBasedIndexPatternColumn } from '../column_types'; -import { IndexPattern } from '../../../types'; +import { IndexPattern } from '../../../../types'; export interface MathIndexPatternColumn extends ReferenceBasedIndexPatternColumn { operationType: 'math'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 63e0935a3425b2..5c9a277070936d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -8,13 +8,14 @@ import { i18n } from '@kbn/i18n'; import { isObject } from 'lodash'; import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; +import type { IndexPattern } from '../../../../types'; import { OperationDefinition, GenericOperationDefinition, GenericIndexPatternColumn, operationDefinitionMap, } from '..'; -import type { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; import { mathOperation } from './math'; import { documentField } from '../../../document_field'; import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts index 28e015e4fc0b5d..788d00f1c14bf7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -27,7 +27,8 @@ import type { GenericIndexPatternColumn, GenericOperationDefinition, } from '..'; -import type { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { IndexPatternLayer } from '../../../types'; +import type { IndexPattern } from '../../../../types'; import type { TinymathNodeTypes } from './types'; interface ValidationErrors { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 4ca172df112e58..868d5e2557e49f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -6,13 +6,14 @@ */ import { i18n } from '@kbn/i18n'; +import type { IndexPattern, IndexPatternField } from '../../../types'; import { GenericIndexPatternColumn, operationDefinitionMap } from '.'; import { FieldBasedIndexPatternColumn, FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn, } from './column_types'; -import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; +import type { IndexPatternLayer } from '../../types'; import { hasField } from '../../pure_utils'; export function getInvalidFieldMessage( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 0af5ed4428ef78..ec140d0a7e597c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -19,7 +19,7 @@ import { AggFunctionsMapping } from '@kbn/data-plugin/public'; import { buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { OperationDefinition } from '.'; import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types'; -import { IndexPatternField, IndexPattern } from '../../types'; +import type { IndexPatternField, IndexPattern } from '../../../types'; import { DataType } from '../../../types'; import { getFormatFromPreviousColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index 06db3221bde34d..b1d023a189be96 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -17,7 +17,7 @@ import { FieldBasedIndexPatternColumn } from '../column_types'; import { updateColumnParam } from '../../layer_helpers'; import { supportedFormats } from '../../../../../common/expressions/format_column/supported_formats'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; -import { IndexPattern, IndexPatternField } from '../../../types'; +import { IndexPattern, IndexPatternField } from '../../../../types'; import { getInvalidFieldMessage, isValidNumber } from '../helpers'; type RangeType = Omit; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx index 49d913655af453..1dbd20d8767d07 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/index.tsx @@ -4,6 +4,6 @@ * 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'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index bb9b36a3d097b3..de6e662aead13e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -13,7 +13,7 @@ import { GenericIndexPatternColumn, ValueFormatConfig, } from './column_types'; -import type { IndexPattern } from '../../types'; +import type { IndexPattern } from '../../../types'; import { useDebouncedValue } from '../../../shared_components'; import { getFormatFromPreviousColumn, isValidNumber } from './helpers'; import { getColumnOrder } from '../layer_helpers'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx index a6b734d373849d..28e267b01bcd76 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx @@ -15,18 +15,13 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - DragDropBuckets, - NewBucketButton, - TooltipWrapper, - useDebouncedValue, -} from '../../../../shared_components'; +import { ExistingFieldsMap, IndexPattern } from '../../../../types'; +import { TooltipWrapper, useDebouncedValue } from '../../../../shared_components'; import { FieldSelect } from '../../../dimension_panel/field_select'; import type { TermsIndexPatternColumn } from './types'; -import type { IndexPatternPrivateState } from '../../../types'; import type { OperationSupportMatrix } from '../../../dimension_panel'; import { supportedTypes } from './constants'; -import type { IndexPattern } from '../../../../editor_frame_service/types'; +import { DragDropBuckets, NewBucketButton } from '../shared_components'; const generateId = htmlIdGenerator(); export const MAX_MULTI_FIELDS_SIZE = 3; @@ -34,7 +29,7 @@ export const MAX_MULTI_FIELDS_SIZE = 3; export interface FieldInputsProps { column: TermsIndexPatternColumn; indexPattern: IndexPattern; - existingFields: IndexPatternPrivateState['existingFields']; + existingFields: ExistingFieldsMap; invalidFields?: string[]; operationSupportMatrix: Pick; onChange: (newValues: string[]) => void; @@ -100,7 +95,7 @@ export function FieldInputs({ { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - EuiDragDropContext: 'eui-drag-drop-context', - EuiDroppable: 'eui-droppable', - EuiDraggable: (props: any) => props.children(), // eslint-disable-line @typescript-eslint/no-explicit-any - }; -}); - -describe('buckets shared components', () => { - describe('DragDropBuckets', () => { - it('should call onDragEnd when dragging ended with reordered items', () => { - const items = [
first
,
second
,
third
]; - const defaultProps = { - items, - onDragStart: jest.fn(), - onDragEnd: jest.fn(), - droppableId: 'TEST_ID', - children: items, - }; - const instance = shallow(); - act(() => { - // simulate dragging ending - instance.props().onDragEnd({ source: { index: 0 }, destination: { index: 1 } }); - }); - - expect(defaultProps.onDragEnd).toHaveBeenCalledWith([ -
second
, -
first
, -
third
, - ]); - }); - }); - describe('DraggableBucketContainer', () => { - const defaultProps = { - isInvalid: false, - invalidMessage: 'invalid', - onRemoveClick: jest.fn(), - removeTitle: 'remove', - children:
popover
, - id: '0', - idx: 0, - }; - it('should render valid component', () => { - const instance = mount(); - const popover = instance.find('[data-test-subj="popover"]'); - expect(popover).toHaveLength(1); - }); - it('should render invalid component', () => { - const instance = mount(); - const iconProps = instance.find(EuiIcon).first().props(); - expect(iconProps.color).toEqual('danger'); - expect(iconProps.type).toEqual('alert'); - expect(iconProps.title).toEqual('invalid'); - }); - it('should call onRemoveClick when remove icon is clicked', () => { - const instance = mount(); - const removeIcon = instance - .find('[data-test-subj="lns-customBucketContainer-remove"]') - .first(); - removeIcon.simulate('click'); - expect(defaultProps.onRemoveClick).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx b/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx deleted file mode 100644 index 0f4ba342348cf5..00000000000000 --- a/x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiButtonIcon, - EuiIcon, - EuiDragDropContext, - euiDragDropReorder, - EuiDraggable, - EuiDroppable, - EuiButtonEmpty, -} from '@elastic/eui'; - -export const NewBucketButton = ({ - label, - onClick, - ['data-test-subj']: dataTestSubj, - isDisabled, -}: { - label: string; - onClick: () => void; - 'data-test-subj'?: string; - isDisabled?: boolean; -}) => ( - - {label} - -); - -interface BucketContainerProps { - isInvalid?: boolean; - invalidMessage: string; - onRemoveClick: () => void; - removeTitle: string; - isNotRemovable?: boolean; - children: React.ReactNode; - dataTestSubj?: string; -} - -const BucketContainer = ({ - isInvalid, - invalidMessage, - onRemoveClick, - removeTitle, - children, - dataTestSubj, - isNotRemovable, -}: BucketContainerProps) => { - return ( - - - {/* Empty for spacing */} - - - - {children} - - - - - - ); -}; - -export const DraggableBucketContainer = ({ - id, - idx, - children, - ...bucketContainerProps -}: { - id: string; - idx: number; - children: React.ReactNode; -} & BucketContainerProps) => { - return ( - - {(provided) => {children}} - - ); -}; - -interface DraggableLocation { - droppableId: string; - index: number; -} - -export const DragDropBuckets = ({ - items, - onDragStart, - onDragEnd, - droppableId, - children, -}: { - items: any; // eslint-disable-line @typescript-eslint/no-explicit-any - onDragStart: () => void; - onDragEnd: (items: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - droppableId: string; - children: React.ReactElement[]; -}) => { - const handleDragEnd = ({ - source, - destination, - }: { - source?: DraggableLocation; - destination?: DraggableLocation; - }) => { - if (source && destination) { - const newItems = euiDragDropReorder(items, source.index, destination.index); - onDragEnd(newItems); - } - }; - return ( - - - {children} - - - ); -}; diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index a4e6bf046b36d1..c22f071ceede3e 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -12,11 +12,6 @@ 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 { - NewBucketButton, - DraggableBucketContainer, - DragDropBuckets, -} from './drag_drop_bucket/buckets'; export { RangeInputField } from './range_input_field'; export { BucketAxisBoundsControl, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 5141834d14e802..163174abe61257 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -262,8 +262,7 @@ export interface Datasource { savedObjectReferences?: SavedObjectReference[], initialContext?: VisualizeFieldContext | VisualizeEditorContext, indexPatternRefs?: IndexPatternRef[], - indexPatterns?: IndexPatternMap, - options?: InitializationOptions + indexPatterns?: IndexPatternMap ) => T; // Given the current state, which parts should be saved? From 12b91fc7e97f5658542326d62085bd5c265f7f29 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 28 Jul 2022 18:14:35 +0200 Subject: [PATCH 16/98] :label: Fix types and tests --- .../__snapshots__/app.test.tsx.snap | 8 + .../app_plugin/show_underlying_data.test.ts | 11 +- .../buttons/drop_targets_utils.test.tsx | 5 + .../config_panel/config_panel.test.tsx | 2 + .../config_panel/layer_panel.test.tsx | 3 + .../config_panel/layer_settings.test.tsx | 1 + .../editor_frame/data_panel_wrapper.test.tsx | 8 +- .../editor_frame/editor_frame.test.tsx | 4 + .../editor_frame/suggestion_helpers.test.ts | 27 +- .../public/embeddable/embeddable.test.tsx | 42 +- .../datapanel.test.tsx | 96 ++- .../bucket_nesting_editor.test.tsx | 2 +- .../dimension_panel/dimension_panel.test.tsx | 113 ++- .../droppable/get_drop_props.test.ts | 12 +- .../droppable/on_drop_handler.test.ts | 36 +- .../dimension_panel/field_input.test.tsx | 7 +- .../dimension_panel/filtering.tsx | 3 +- .../field_item.test.tsx | 2 +- .../fields_accordion.test.tsx | 2 +- .../indexpattern.test.ts | 279 +++---- .../indexpattern_suggestions.test.tsx | 774 ++++++++++-------- .../layerpanel.test.tsx | 71 +- .../public/indexpattern_datasource/mocks.ts | 2 +- .../operations/definitions.test.ts | 3 +- .../definitions/date_histogram.test.tsx | 3 +- .../formula/editor/math_completion.test.ts | 3 +- .../definitions/formula/formula.test.tsx | 2 +- .../formula/formula_public_api.test.ts | 2 +- .../operations/definitions/index.ts | 15 +- .../definitions/last_value.test.tsx | 3 +- .../definitions/percentile.test.tsx | 3 +- .../definitions/percentile_ranks.test.tsx | 3 +- .../definitions/ranges/ranges.test.tsx | 4 +- .../definitions/static_value.test.tsx | 3 +- .../definitions/terms/terms.test.tsx | 9 +- .../operations/layer_helpers.test.ts | 2 +- .../pure_helpers.test.ts | 16 - .../public/mocks/data_views_service_mock.ts | 19 + .../lens/public/mocks/datasource_mock.ts | 17 +- x-pack/plugins/lens/public/mocks/index.ts | 12 + .../plugins/lens/public/mocks/store_mocks.tsx | 6 + .../xy_visualization/to_expression.test.ts | 12 +- .../lens/public/xy_visualization/types.ts | 1 - .../xy_visualization/visualization.test.ts | 10 +- .../public/xy_visualization/visualization.tsx | 2 +- .../visualization_helpers.tsx | 1 - .../xy_visualization/xy_suggestions.test.ts | 9 +- 47 files changed, 921 insertions(+), 749 deletions(-) delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts create mode 100644 x-pack/plugins/lens/public/mocks/data_views_service_mock.ts diff --git a/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap b/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap index ca14bc908eb80d..bfdc82ff3b7c4d 100644 --- a/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap +++ b/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap @@ -4,6 +4,14 @@ exports[`Lens App renders the editor frame 1`] = ` Array [ Array [ Object { + "indexPatternService": Object { + "ensureIndexPattern": [Function], + "getDefaultIndex": [Function], + "loadIndexPatternRefs": [Function], + "loadIndexPatterns": [Function], + "refreshExistingFields": [Function], + "updateIndexPatternsCache": [Function], + }, "lensInspector": Object { "adapters": Object { "expression": ExpressionsInspectorAdapter { diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts index 99bc4fa0f9717b..8a8c7bbe1f9730 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts @@ -21,6 +21,7 @@ describe('getLayerMetaInfo', () => { createMockDatasource('testDatasource'), {}, undefined, + {}, undefined, capabilities ).error @@ -36,6 +37,7 @@ describe('getLayerMetaInfo', () => { datatable1: { type: 'datatable', columns: [], rows: [] }, datatable2: { type: 'datatable', columns: [], rows: [] }, }, + {}, undefined, capabilities ).error @@ -43,7 +45,7 @@ describe('getLayerMetaInfo', () => { }); it('should return error in case of missing activeDatasource', () => { - expect(getLayerMetaInfo(undefined, {}, undefined, undefined, capabilities).error).toBe( + expect(getLayerMetaInfo(undefined, {}, undefined, {}, undefined, capabilities).error).toBe( 'Visualization has no data available to show' ); }); @@ -54,6 +56,7 @@ describe('getLayerMetaInfo', () => { createMockDatasource('testDatasource'), undefined, {}, + {}, undefined, capabilities ).error @@ -81,7 +84,7 @@ describe('getLayerMetaInfo', () => { }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); expect( - getLayerMetaInfo(createMockDatasource('testDatasource'), {}, {}, undefined, capabilities) + getLayerMetaInfo(createMockDatasource('testDatasource'), {}, {}, {}, undefined, capabilities) .error ).toBe('Visualization has no data available to show'); }); @@ -105,6 +108,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + {}, undefined, capabilities ).error @@ -120,6 +124,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + {}, undefined, { navLinks: { discover: false }, @@ -134,6 +139,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + {}, undefined, { navLinks: { discover: true }, @@ -167,6 +173,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + {}, undefined, capabilities ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx index dd5ec847fb5b5c..17907ac19c4bc3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx @@ -27,6 +27,7 @@ describe('getDropProps', () => { id: 'annotationColumn2', humanData: { label: 'Event' }, }, + indexPatterns: {}, }, mockDatasource ); @@ -50,6 +51,7 @@ describe('getDropProps', () => { id: 'annotationColumn2', humanData: { label: 'Event' }, }, + indexPatterns: {}, }) ).toEqual({ dropTypes: ['reorder'] }); }); @@ -71,6 +73,7 @@ describe('getDropProps', () => { id: 'annotationColumn2', humanData: { label: 'Event' }, }, + indexPatterns: {}, }) ).toEqual({ dropTypes: ['duplicate_compatible'] }); }); @@ -91,6 +94,7 @@ describe('getDropProps', () => { id: 'annotationColumn2', humanData: { label: 'Event' }, }, + indexPatterns: {}, }) ).toEqual({ dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], @@ -114,6 +118,7 @@ describe('getDropProps', () => { id: 'annotationColumn2', humanData: { label: 'Event' }, }, + indexPatterns: {}, }) ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'], diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index a13b6b480a9008..ef461b7d9d3027 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -25,6 +25,7 @@ import { mountWithProvider } from '../../../mocks'; import { LayerType, layerTypes } from '../../../../common'; import { ReactWrapper } from 'enzyme'; import { addLayer } from '../../../state_management'; +import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; jest.mock('../../../id_generator'); @@ -107,6 +108,7 @@ describe('ConfigPanel', () => { state: 'state', }, }, + indexPatternService: createIndexPatternServiceMock(), visualizationState: 'state', updateVisualization: jest.fn(), updateDatasource: jest.fn(), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 49a2bec8cda7c3..0440700c7ed8b6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -20,6 +20,7 @@ import { DatasourceMock, mountWithProvider, } from '../../../mocks'; +import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; jest.mock('../../../id_generator'); @@ -96,6 +97,8 @@ describe('LayerPanel', () => { isFullscreen: false, toggleFullscreen: jest.fn(), onEmptyDimensionAdd: jest.fn(), + onChangeIndexPattern: jest.fn(), + indexPatternService: createIndexPatternServiceMock(), }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx index 04c430143a3c8e..3daea87dad39d3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.test.tsx @@ -28,6 +28,7 @@ describe('LayerSettings', () => { dateRange: { fromDate: 'now-7d', toDate: 'now' }, activeData: frame.activeData, setState: jest.fn(), + onChangeIndexPattern: jest.fn(), }, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx index cd7d325d5830dc..63e4f88d6d2346 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx @@ -10,9 +10,11 @@ import { DataPanelWrapper } from './data_panel_wrapper'; import { Datasource, DatasourceDataPanelProps } from '../../types'; import { DragDropIdentifier } from '../../drag_drop'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { mockStoreDeps, mountWithProvider } from '../../mocks'; +import { createMockFramePublicAPI, mockStoreDeps, mountWithProvider } from '../../mocks'; import { disableAutoApply } from '../../state_management/lens_slice'; import { selectTriggerApplyChanges } from '../../state_management'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; describe('Data Panel Wrapper', () => { describe('Datasource data panel properties', () => { @@ -34,7 +36,9 @@ describe('Data Panel Wrapper', () => { core={{} as DatasourceDataPanelProps['core']} dropOntoWorkspace={(field: DragDropIdentifier) => {}} hasSuggestionForField={(field: DragDropIdentifier) => true} - plugins={{ uiActions: {} as UiActionsStart }} + plugins={{ uiActions: {} as UiActionsStart, dataViews: {} as DataViewsPublicPluginStart }} + indexPatternService={createIndexPatternServiceMock()} + frame={createMockFramePublicAPI()} />, { preloadedState: { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 047d8b4deffaa5..035011b7d4a680 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -49,6 +49,8 @@ import { mockDataPlugin, mountWithProvider } from '../../mocks'; import { setState } from '../../state_management'; import { getLensInspectorService } from '../../lens_inspector_service'; import { toExpression } from '@kbn/interpreter'; +import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -80,10 +82,12 @@ function getDefaultProps() { data: mockDataPlugin(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), }, palettes: chartPluginMock.createPaletteRegistry(), lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()), showNoDataPopover: jest.fn(), + indexPatternService: createIndexPatternServiceMock(), }; return defaultProps; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index a265f534627b67..b739091ddff3b2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -7,14 +7,19 @@ import type { PaletteOutput } from '@kbn/coloring'; import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers'; -import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks'; +import { + createMockVisualization, + createMockDatasource, + DatasourceMock, + createMockFramePublicAPI, +} from '../../mocks'; import { TableSuggestion, DatasourceSuggestion, Visualization, VisualizeEditorContext, } from '../../types'; -import { DatasourceStates } from '../../state_management'; +import { DatasourceStates, DataViewsState } from '../../state_management'; const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({ state, @@ -29,6 +34,7 @@ const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSu let datasourceMap: Record; let datasourceStates: DatasourceStates; +let dataViews: DataViewsState; beforeEach(() => { datasourceMap = { @@ -41,6 +47,8 @@ beforeEach(() => { state: {}, }, }; + + dataViews = createMockFramePublicAPI().dataViews; }); describe('suggestion helpers', () => { @@ -69,6 +77,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(suggestions).toHaveLength(1); expect(suggestions[0].visualizationState).toBe(suggestedState); @@ -116,6 +125,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(suggestions).toHaveLength(3); }); @@ -133,6 +143,7 @@ describe('suggestion helpers', () => { datasourceMap, datasourceStates, field: droppedField, + dataViews, }); expect(datasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( datasourceStates.mock.state, @@ -168,6 +179,7 @@ describe('suggestion helpers', () => { datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, field: droppedField, + dataViews, }); expect(multiDatasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( multiDatasourceStates.mock.state, @@ -201,6 +213,7 @@ describe('suggestion helpers', () => { indexPatternId: '1', fieldName: 'test', }, + dataViews, }); expect(datasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( datasourceStates.mock.state, @@ -240,6 +253,7 @@ describe('suggestion helpers', () => { datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, visualizeTriggerFieldContext: visualizeTriggerField, + dataViews, }); expect(multiDatasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( multiDatasourceStates.mock.state, @@ -320,6 +334,7 @@ describe('suggestion helpers', () => { datasourceMap, datasourceStates, visualizeTriggerFieldContext: triggerContext, + dataViews, }); expect(datasourceMap.mock.getDatasourceSuggestionsForVisualizeCharts).toHaveBeenCalledWith( datasourceStates.mock.state, @@ -402,6 +417,7 @@ describe('suggestion helpers', () => { datasourceMap: multiDatasourceMap, datasourceStates: multiDatasourceStates, visualizeTriggerFieldContext: triggerContext, + dataViews, }); expect(multiDatasourceMap.mock.getDatasourceSuggestionsForVisualizeCharts).toHaveBeenCalledWith( datasourceStates.mock.state, @@ -458,6 +474,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(suggestions[0].score).toBe(0.8); expect(suggestions[1].score).toBe(0.6); @@ -493,6 +510,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(mockVisualization1.getSuggestions.mock.calls[0][0].table).toEqual(table1); expect(mockVisualization1.getSuggestions.mock.calls[1][0].table).toEqual(table2); @@ -553,6 +571,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(suggestions[0].datasourceState).toBe(tableState1); expect(suggestions[0].datasourceId).toBe('mock'); @@ -582,6 +601,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(mockVisualization1.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ @@ -614,6 +634,7 @@ describe('suggestion helpers', () => { datasourceMap, datasourceStates, mainPalette, + dataViews, }); expect(mockVisualization1.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ @@ -647,6 +668,7 @@ describe('suggestion helpers', () => { visualizationState: {}, datasourceMap, datasourceStates, + dataViews, }); expect(mockVisualization1.getMainPalette).toHaveBeenCalledWith({}); expect(mockVisualization2.getSuggestions).toHaveBeenCalledWith( @@ -716,6 +738,7 @@ describe('suggestion helpers', () => { { testVis: mockVisualization1 }, datasourceMap.mock, { id: 'myfield', humanData: { label: 'myfieldLabel' } }, + dataViews, ]; }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 6ee840d2f605f8..9ff0d5a664ad39 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -144,7 +144,7 @@ describe('embeddable', () => { data: dataMock, expressionRenderer, basePath, - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -196,7 +196,7 @@ describe('embeddable', () => { uiSettings: { get: () => undefined } as unknown as IUiSettingsClient, expressionRenderer, basePath, - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, inspector: inspectorPluginMock.createStartContract(), capabilities: { canSaveDashboards: true, @@ -249,7 +249,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -311,7 +311,7 @@ describe('embeddable', () => { inspector: inspectorPluginMock.createStartContract(), expressionRenderer, basePath, - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, spaces: spacesPluginStart, capabilities: { canSaveDashboards: true, @@ -362,7 +362,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: { + dataViews: { get: (id: string) => Promise.resolve({ id, isTimeBased: jest.fn(() => true) }), } as unknown as DataViewsContract, capabilities: { @@ -413,7 +413,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: { + dataViews: { get: (id: string) => Promise.resolve({ id, isTimeBased: () => false }), } as unknown as DataViewsContract, capabilities: { @@ -462,7 +462,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -515,7 +515,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -572,7 +572,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -627,7 +627,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -689,7 +689,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -752,7 +752,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -818,7 +818,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: { get: jest.fn() } as unknown as DataViewsContract, + dataViews: { get: jest.fn() } as unknown as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -869,7 +869,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -922,7 +922,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -972,7 +972,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -1037,7 +1037,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -1121,7 +1121,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -1180,7 +1180,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -1236,7 +1236,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, @@ -1313,7 +1313,7 @@ describe('embeddable', () => { expressionRenderer, basePath, inspector: inspectorPluginMock.createStartContract(), - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 2bd1601eefc85e..0578c3ac7a47a9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -27,6 +27,9 @@ import { getFieldByNameFactory } from './pure_helpers'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { TermsIndexPatternColumn } from './operations'; import { DOCUMENT_FIELD_NAME } from '../../common'; +import { createIndexPatternServiceMock } from '../mocks/data_views_service_mock'; +import { createMockFramePublicAPI } from '../mocks'; +import { DataViewsState } from '../state_management'; const fieldsOne = [ { @@ -153,8 +156,6 @@ const fieldsThree = [ ]; const initialState: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', layers: { first: { @@ -212,7 +213,11 @@ const initialState: IndexPatternPrivateState = { }, }, }, - indexPatterns: { +}; + +function getFrameAPIMock({ indexPatterns, existingFields, ...rest }: Partial = {}) { + const frameAPI = createMockFramePublicAPI(); + const defaultIndexPatterns = { '1': { id: '1', title: 'idx1', @@ -237,9 +242,18 @@ const initialState: IndexPatternPrivateState = { fields: fieldsThree, getFieldByName: getFieldByNameFactory(fieldsThree), }, - }, - isFirstExistenceFetch: false, -}; + }; + return { + ...frameAPI, + dataViews: { + ...frameAPI.dataViews, + indexPatterns: indexPatterns ?? defaultIndexPatterns, + existingFields: existingFields ?? {}, + isFirstExistenceFetch: false, + ...rest, + }, + }; +} const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } }; @@ -255,17 +269,14 @@ describe('IndexPattern Data Panel', () => { beforeEach(() => { core = coreMock.createStart(); defaultProps = { - indexPatternRefs: [], - existingFields: {}, data: dataPluginMock.createStartContract(), dataViews: dataViewPluginMocks.createStartContract(), fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), - onUpdateIndexPattern: jest.fn(), + onChangeIndexPattern: jest.fn(), + onIndexPatternRefresh: jest.fn(), dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', - indexPatterns: initialState.indexPatterns, - onChangeIndexPattern: jest.fn(), core, dateRange: { fromDate: 'now-7d', @@ -278,6 +289,8 @@ describe('IndexPattern Data Panel', () => { dropOntoWorkspace: jest.fn(), hasSuggestionForField: jest.fn(() => false), uiActions: uiActionsPluginMock.createStartContract(), + indexPatternService: createIndexPatternServiceMock(), + frame: getFrameAPIMock(), }; }); @@ -313,7 +326,6 @@ describe('IndexPattern Data Panel', () => { state={{ ...initialState, currentIndexPatternId: '', - indexPatterns: {}, }} setState={jest.fn()} dragDropContext={{ @@ -321,6 +333,7 @@ describe('IndexPattern Data Panel', () => { dragging: { id: '1', humanData: { label: 'Label' } }, }} changeIndexPattern={jest.fn()} + frame={createMockFramePublicAPI()} /> ); expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); @@ -623,12 +636,14 @@ describe('IndexPattern Data Panel', () => { beforeEach(() => { props = { ...defaultProps, - existingFields: { - idx1: { - bytes: true, - memory: true, + frame: getFrameAPIMock({ + existingFields: { + idx1: { + bytes: true, + memory: true, + }, }, - }, + }), }; }); it('should list all supported fields in the pattern sorted alphabetically in groups', async () => { @@ -658,22 +673,24 @@ describe('IndexPattern Data Panel', () => { const wrapper = mountWithIntl( ); wrapper @@ -692,7 +709,10 @@ describe('IndexPattern Data Panel', () => { it('should display NoFieldsCallout when all fields are empty', async () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find(NoFieldsCallout).length).toEqual(2); expect( @@ -726,7 +746,10 @@ describe('IndexPattern Data Panel', () => { it('should not allow field details when error', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find(FieldList).prop('fieldGroups')).toEqual( @@ -738,7 +761,10 @@ describe('IndexPattern Data Panel', () => { it('should allow field details when timeout', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find(FieldList).prop('fieldGroups')).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx index f876536ebba43b..248cc0f78756eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { GenericIndexPatternColumn } from '../indexpattern'; -import { IndexPatternField } from '../../editor_frame_service/types'; +import { IndexPatternField } from '../../types'; const fieldMap: Record = { a: { displayName: 'a' } as IndexPatternField, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 291ccb1ede76b8..a23f35f4cd53ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -157,18 +157,7 @@ describe('IndexPatternDimensionEditorPanel', () => { beforeEach(() => { state = { - indexPatternRefs: [], - indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', - isFirstExistenceFetch: false, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, layers: { first: { indexPatternId: '1', @@ -202,6 +191,15 @@ describe('IndexPatternDimensionEditorPanel', () => { }); defaultProps = { + indexPatterns: expectedIndexPatterns, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, state, setState, dateRange: { fromDate: 'now-1d', toDate: 'now' }, @@ -1309,13 +1307,10 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount( @@ -1636,34 +1631,31 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should select operation directly if only one field is possible', () => { - const initialState = { - ...state, - indexPatterns: { - 1: { - ...state.indexPatterns['1'], - fields: state.indexPatterns['1'].fields.filter((field) => field.name !== 'memory'), - }, - }, - }; - wrapper = mount( field.name !== 'memory' + ), + }, + }} /> ); wrapper.find('button[data-test-subj="lns-indexPatternDimension-average"]').simulate('click'); expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); - expect(setState.mock.calls[0][0](initialState)).toEqual({ - ...initialState, + expect(setState.mock.calls[0][0](state)).toEqual({ + ...state, layers: { first: { - ...initialState.layers.first, + ...state.layers.first, columns: { - ...initialState.layers.first.columns, + ...state.layers.first.columns, col2: expect.objectContaining({ sourceField: 'bytes', operationType: 'average', @@ -2071,35 +2063,38 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'bytes', }, }), - indexPatterns: { - 1: { - id: '1', - title: 'my-fake-index-pattern', - hasRestrictions: false, - fields: [ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - getFieldByName: getFieldByNameFactory([ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]), - }, - }, }; wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="lns-indexPatternDimension-differences"]')).toHaveLength( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts index f48e4d897a6370..2f4d8a0121aa11 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts @@ -18,19 +18,9 @@ import { import { generateId } from '../../../id_generator'; const getDefaultProps = () => ({ + indexPatterns: mockDataViews(), state: { - indexPatternRefs: [], - indexPatterns: mockDataViews(), currentIndexPatternId: 'first', - isFirstExistenceFetch: false, - existingFields: { - first: { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, layers: { first: mockedLayers.doubleColumnLayer(), second: mockedLayers.emptyLayer() }, }, target: mockedDndOperations.notFiltering, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts index 12acf46c583808..3ae1672d9e4640 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts @@ -69,18 +69,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { beforeEach(() => { state = { - indexPatternRefs: [], - indexPatterns: mockDataViews(), currentIndexPatternId: 'first', - isFirstExistenceFetch: false, - existingFields: { - first: { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, layers: { first: mockedLayers.singleColumnLayer(), second: mockedLayers.emptyLayer(), @@ -96,6 +85,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { state, setState, dimensionGroups: [], + indexPatterns: mockDataViews(), }; jest.clearAllMocks(); @@ -1506,19 +1496,9 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { setState = jest.fn(); props = { + indexPatterns: mockDataViews(), state: { - indexPatternRefs: [], - indexPatterns: mockDataViews(), currentIndexPatternId: 'first', - isFirstExistenceFetch: false, - existingFields: { - first: { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, layers: { first: mockedLayers.singleColumnLayer(), second: mockedLayers.multipleColumnsLayer('col2', 'col3', 'col4', 'col5'), @@ -2062,6 +2042,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { setState: jest.fn(), dropType: 'move_compatible', + indexPatterns: mockDataViews(), state: { layers: { first: { @@ -2114,18 +2095,7 @@ describe('IndexPatternDimensionEditorPanel: onDrop', () => { columnOrder: ['second', 'secondX0'], }, }, - indexPatternRefs: [], - indexPatterns: mockDataViews(), currentIndexPatternId: 'first', - isFirstExistenceFetch: false, - existingFields: { - first: { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, }, source: { columnId: 'firstColumn', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx index 730342ae694e89..8cdbc396cd9ab7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx @@ -120,14 +120,13 @@ function getDefaultOperationSupportMatrix( return getOperationSupportMatrix({ state: { layers: { layer1: layer }, - indexPatterns: { - [defaultProps.indexPattern.id]: defaultProps.indexPattern, - }, - existingFields, } as unknown as IndexPatternPrivateState, layerId: 'layer1', filterOperations: () => true, columnId, + indexPatterns: { + [defaultProps.indexPattern.id]: defaultProps.indexPattern, + }, }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx index 80e369526c8e4c..5117e935c59d62 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx @@ -21,8 +21,9 @@ 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 { IndexPattern, IndexPatternLayer } from '../types'; +import type { IndexPatternLayer } from '../types'; import { useDebouncedValue } from '../../shared_components'; +import type { IndexPattern } from '../../types'; const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', { defaultMessage: 'Filter by', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 11d3d9c6a7871d..3c3b81cd126df3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -13,7 +13,7 @@ import { InnerFieldItem, FieldItemProps } from './field_item'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { IndexPattern } from './types'; +import { IndexPattern } from '../types'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { documentField } from './document_field'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index cc79941d2cdfec..90be3a9f6e4708 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -10,7 +10,7 @@ import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { IndexPattern } from './types'; +import { IndexPattern } from '../types'; import { FieldItem } from './field_item'; import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 8693f8492aebe9..218d68a45de0d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -171,16 +171,7 @@ type DataViewBaseState = Omit< 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' >; -function enrichBaseState(baseState: DataViewBaseState): IndexPatternPrivateState { - return { - currentIndexPatternId: baseState.currentIndexPatternId, - layers: baseState.layers, - indexPatterns: expectedIndexPatterns, - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - }; -} +const indexPatterns = expectedIndexPatterns; describe('IndexPattern Data Source', () => { let baseState: Omit< @@ -275,9 +266,7 @@ describe('IndexPattern Data Source', () => { describe('#getPersistedState', () => { it('should persist from saved state', async () => { - const state = enrichBaseState(baseState); - - expect(indexPatternDatasource.getPersistableState(state)).toEqual({ + expect(indexPatternDatasource.getPersistableState(baseState)).toEqual({ state: { layers: { first: { @@ -310,8 +299,8 @@ describe('IndexPattern Data Source', () => { describe('#toExpression', () => { it('should generate an empty expression when no columns are selected', async () => { - const state = await indexPatternDatasource.initialize(); - expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); + const state = indexPatternDatasource.initialize(); + expect(indexPatternDatasource.toExpression(state, 'first', indexPatterns)).toEqual(null); }); it('should create a table when there is a formula without aggs', async () => { @@ -334,8 +323,7 @@ describe('IndexPattern Data Source', () => { }, }, }; - const state = enrichBaseState(queryBaseState); - expect(indexPatternDatasource.toExpression(state, 'first')).toEqual({ + expect(indexPatternDatasource.toExpression(queryBaseState, 'first', indexPatterns)).toEqual({ chain: [ { function: 'createTable', @@ -382,9 +370,8 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` + expect(indexPatternDatasource.toExpression(queryBaseState, 'first', indexPatterns)) + .toMatchInlineSnapshot(` Object { "chain": Array [ Object { @@ -558,9 +545,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); @@ -595,9 +584,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect((ast.chain[1].arguments.aggs[1] as Ast).chain[0].arguments.timeShift).toEqual(['1d']); }); @@ -644,9 +635,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(ast.chain[1].arguments.aggs[0]).toMatchInlineSnapshot(` Object { "chain": Array [ @@ -770,9 +763,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; const timeScaleCalls = ast.chain.filter((fn) => fn.function === 'lens_time_scale'); const formatCalls = ast.chain.filter((fn) => fn.function === 'lens_format_column'); expect(timeScaleCalls).toHaveLength(1); @@ -855,9 +850,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; const formatIndex = ast.chain.findIndex((fn) => fn.function === 'lens_format_column'); const calculationIndex = ast.chain.findIndex((fn) => fn.function === 'moving_average'); expect(calculationIndex).toBeLessThan(formatIndex); @@ -905,8 +902,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(ast.chain[1].arguments.metricsAtAllLevels).toEqual([false]); expect(JSON.parse(ast.chain[2].arguments.idMap[0] as string)).toEqual({ 'col-0-0': [expect.objectContaining({ id: 'bucket1' })], @@ -945,9 +945,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(ast.chain[1].arguments.timeFields).toEqual(['timestamp']); expect(ast.chain[1].arguments.timeFields).not.toContain('timefield'); }); @@ -1001,11 +1003,9 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - const optimizeMock = jest.spyOn(operationDefinitionMap.percentile, 'optimizeEsAggs'); - indexPatternDatasource.toExpression(state, 'first'); + indexPatternDatasource.toExpression(queryBaseState, 'first', indexPatterns); expect(operationDefinitionMap.percentile.optimizeEsAggs).toHaveBeenCalledTimes(1); @@ -1066,8 +1066,6 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - const optimizeMock = jest .spyOn(operationDefinitionMap.percentile, 'optimizeEsAggs') .mockImplementation((aggs, esAggsIdMap) => { @@ -1075,7 +1073,11 @@ describe('IndexPattern Data Source', () => { return { aggs: aggs.reverse(), esAggsIdMap }; }); - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(operationDefinitionMap.percentile.optimizeEsAggs).toHaveBeenCalledTimes(1); @@ -1126,9 +1128,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; // @ts-expect-error we can't isolate just the reference type expect(operationDefinitionMap.testReference.toExpression).toHaveBeenCalled(); expect(ast.chain[3]).toEqual('mock'); @@ -1161,9 +1165,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; expect(JSON.parse(ast.chain[2].arguments.idMap[0] as string)).toEqual({ 'col-0-0': [ @@ -1250,9 +1256,11 @@ describe('IndexPattern Data Source', () => { }, }; - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const ast = indexPatternDatasource.toExpression( + queryBaseState, + 'first', + indexPatterns + ) as Ast; const chainLength = ast.chain.length; expect(ast.chain[chainLength - 2].arguments.name).toEqual(['math']); expect(ast.chain[chainLength - 1].arguments.id).toEqual(['formula']); @@ -1333,10 +1341,6 @@ describe('IndexPattern Data Source', () => { it('should list the current layers', () => { expect( indexPatternDatasource.getLayers({ - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -1359,10 +1363,10 @@ describe('IndexPattern Data Source', () => { let publicAPI: DatasourcePublicAPI; beforeEach(async () => { - const initialState = enrichBaseState(baseState); publicAPI = indexPatternDatasource.getPublicAPI({ - state: initialState, + state: baseState, layerId: 'first', + indexPatterns, }); }); @@ -1378,7 +1382,7 @@ describe('IndexPattern Data Source', () => { it('should skip columns that are being referenced', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1407,6 +1411,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getTableSpec()).toEqual([expect.objectContaining({ columnId: 'col2' })]); @@ -1415,7 +1420,7 @@ describe('IndexPattern Data Source', () => { it('should collect all fields (also from referenced columns)', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1444,6 +1449,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); // The cumulative sum column has no field, but it references a sum column (hidden) which has it // The getTableSpec() should walk the reference tree and assign all fields to the root column @@ -1453,7 +1459,7 @@ describe('IndexPattern Data Source', () => { it('should collect and organize fields per visible column', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1494,6 +1500,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); // col1 is skipped as referenced but its field gets inherited by col2 @@ -1522,7 +1529,7 @@ describe('IndexPattern Data Source', () => { it('should return null for referenced columns', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1551,6 +1558,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getOperationForColumnId('col1')).toEqual(null); }); @@ -1566,7 +1574,7 @@ describe('IndexPattern Data Source', () => { it('should return all filters in metrics, grouped by language', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1595,6 +1603,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -1607,7 +1616,7 @@ describe('IndexPattern Data Source', () => { it('should ignore empty filtered metrics', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1627,6 +1636,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { kuery: [], lucene: [] }, @@ -1636,7 +1646,7 @@ describe('IndexPattern Data Source', () => { it('shuold collect top values fields as kuery existence filters if no data is provided', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1672,6 +1682,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -1690,7 +1701,7 @@ describe('IndexPattern Data Source', () => { it('shuold collect top values fields and terms as kuery filters if data is provided', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1726,6 +1737,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); const data = { first: { @@ -1760,7 +1772,7 @@ describe('IndexPattern Data Source', () => { it('shuold collect top values fields and terms and carefully handle empty string values', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1796,6 +1808,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); const data = { first: { @@ -1830,7 +1843,7 @@ describe('IndexPattern Data Source', () => { it('should ignore top values fields if other/missing option is enabled', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1867,6 +1880,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { kuery: [], lucene: [] }, @@ -1876,7 +1890,7 @@ describe('IndexPattern Data Source', () => { it('should collect custom ranges as kuery filters', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1912,6 +1926,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -1930,7 +1945,7 @@ describe('IndexPattern Data Source', () => { it('should collect custom ranges as kuery filters as partial', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -1974,6 +1989,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -1989,7 +2005,7 @@ describe('IndexPattern Data Source', () => { it('should collect filters within filters operation grouped by language', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -2035,6 +2051,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -2059,7 +2076,7 @@ describe('IndexPattern Data Source', () => { it('should ignore filtered metrics if at least one metric is unfiltered', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -2087,6 +2104,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { kuery: [], lucene: [] }, @@ -2096,7 +2114,7 @@ describe('IndexPattern Data Source', () => { it('should ignore filtered metrics if at least one metric is unfiltered in formula', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -2159,6 +2177,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { kuery: [], lucene: [] }, @@ -2168,7 +2187,7 @@ describe('IndexPattern Data Source', () => { it('should support complete scenarios', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -2225,6 +2244,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -2254,7 +2274,7 @@ describe('IndexPattern Data Source', () => { it('should avoid duplicate filters when formula has a global filter', () => { publicAPI = indexPatternDatasource.getPublicAPI({ state: { - ...enrichBaseState(baseState), + ...baseState, layers: { first: { indexPatternId: '1', @@ -2319,6 +2339,7 @@ describe('IndexPattern Data Source', () => { }, }, layerId: 'first', + indexPatterns, }); expect(publicAPI.getFilters()).toEqual({ enabled: { @@ -2360,10 +2381,6 @@ describe('IndexPattern Data Source', () => { (getErrorMessages as jest.Mock).mockClear(); (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2373,7 +2390,7 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ + expect(indexPatternDatasource.getErrorMessages(state, indexPatterns)).toEqual([ { longMessage: 'error 1', shortMessage: '' }, { longMessage: 'error 2', shortMessage: '' }, ]); @@ -2384,10 +2401,6 @@ describe('IndexPattern Data Source', () => { (getErrorMessages as jest.Mock).mockClear(); (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2402,7 +2415,7 @@ describe('IndexPattern Data Source', () => { }, currentIndexPatternId: '1', }; - expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ + expect(indexPatternDatasource.getErrorMessages(state, indexPatterns)).toEqual([ { longMessage: 'Layer 1 error: error 1', shortMessage: '' }, { longMessage: 'Layer 1 error: error 2', shortMessage: '' }, ]); @@ -2431,10 +2444,6 @@ describe('IndexPattern Data Source', () => { }; state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2570,10 +2579,6 @@ describe('IndexPattern Data Source', () => { (getErrorMessages as jest.Mock).mockReturnValueOnce(['error 1', 'error 2']); state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2589,7 +2594,7 @@ describe('IndexPattern Data Source', () => { currentIndexPatternId: '1', }; - expect(indexPatternDatasource.getErrorMessages(state)).toEqual([ + expect(indexPatternDatasource.getErrorMessages(state, indexPatterns)).toEqual([ { longMessage: 'Layer 1 error: error 1', shortMessage: '' }, { longMessage: 'Layer 1 error: error 2', shortMessage: '' }, ]); @@ -2602,10 +2607,6 @@ describe('IndexPattern Data Source', () => { expect( indexPatternDatasource.updateStateOnCloseDimension!({ state: { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2632,10 +2633,6 @@ describe('IndexPattern Data Source', () => { it('should clear all incomplete columns', () => { const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2670,7 +2667,7 @@ describe('IndexPattern Data Source', () => { }); describe('#isTimeBased', () => { it('should return true if date histogram exists in any layer', () => { - let state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2722,18 +2719,16 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); - state = { - ...state, - indexPatterns: { - ...state.indexPatterns, - '1': { ...state.indexPatterns['1'], timeFieldName: undefined }, - }, - }; - expect(indexPatternDatasource.isTimeBased(state)).toEqual(true); + } as IndexPatternPrivateState; + expect( + indexPatternDatasource.isTimeBased(state, { + ...indexPatterns, + '1': { ...indexPatterns['1'], timeFieldName: undefined }, + }) + ).toEqual(true); }); it('should return false if date histogram exists but is detached from global time range in every layer', () => { - let state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2786,18 +2781,16 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); - state = { - ...state, - indexPatterns: { - ...state.indexPatterns, - '1': { ...state.indexPatterns['1'], timeFieldName: undefined }, - }, - }; - expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); + } as IndexPatternPrivateState; + expect( + indexPatternDatasource.isTimeBased(state, { + ...indexPatterns, + '1': { ...indexPatterns['1'], timeFieldName: undefined }, + }) + ).toEqual(false); }); it('should return false if date histogram does not exist in any layer', () => { - let state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2814,18 +2807,16 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); - state = { - ...state, - indexPatterns: { - ...state.indexPatterns, - '1': { ...state.indexPatterns['1'], timeFieldName: undefined }, - }, - }; - expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); + } as IndexPatternPrivateState; + expect( + indexPatternDatasource.isTimeBased(state, { + ...indexPatterns, + '1': { ...indexPatterns['1'], timeFieldName: undefined }, + }) + ).toEqual(false); }); it('should return true if the index pattern is time based even if date histogram does not exist in any layer', () => { - const state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2842,14 +2833,14 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); - expect(indexPatternDatasource.isTimeBased(state)).toEqual(true); + } as IndexPatternPrivateState; + expect(indexPatternDatasource.isTimeBased(state, indexPatterns)).toEqual(true); }); }); describe('#initializeDimension', () => { it('should return the same state if no static value is passed', () => { - const state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2866,9 +2857,9 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); + } as IndexPatternPrivateState; expect( - indexPatternDatasource.initializeDimension!(state, 'first', { + indexPatternDatasource.initializeDimension!(state, 'first', indexPatterns, { columnId: 'newStatic', groupId: 'a', }) @@ -2876,7 +2867,7 @@ describe('IndexPattern Data Source', () => { }); it('should add a new static value column if a static value is passed', () => { - const state = enrichBaseState({ + const state = { currentIndexPatternId: '1', layers: { first: { @@ -2893,9 +2884,9 @@ describe('IndexPattern Data Source', () => { }, }, }, - }); + } as IndexPatternPrivateState; expect( - indexPatternDatasource.initializeDimension!(state, 'first', { + indexPatternDatasource.initializeDimension!(state, 'first', indexPatterns, { columnId: 'newStatic', groupId: 'a', staticValue: 0, // use a falsy value to check also this corner case diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index f96a6dbd3340e6..90f59f5470e377 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -163,9 +163,6 @@ const expectedIndexPatterns = { function testInitialState(): IndexPatternPrivateState { return { currentIndexPatternId: '1', - indexPatternRefs: [], - existingFields: {}, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -189,7 +186,6 @@ function testInitialState(): IndexPatternPrivateState { }, }, }, - isFirstExistenceFetch: false, }; } @@ -223,13 +219,18 @@ describe('IndexPattern Data Source suggestions', () => { } it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithoutLayer(), + '1', + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -272,13 +273,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('should apply a bucketed aggregation for a date field', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithoutLayer(), + '1', + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -317,13 +323,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('should select a metric for a number field', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithoutLayer(), + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -364,35 +375,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should make a metric suggestion for a number field if there is no time field', async () => { const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - isFirstExistenceFetch: false, - indexPatterns: { - 1: { - id: '1', - title: 'no timefield', - hasRestrictions: false, - fields: [ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - getFieldByName: getFieldByNameFactory([ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]), - }, - }, layers: { first: { indexPatternId: '1', @@ -402,13 +385,44 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsForField(state, '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const indexPatterns = { + 1: { + id: '1', + title: 'no timefield', + hasRestrictions: false, + fields: [ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), + }, + }; + + const suggestions = getDatasourceSuggestionsForField( + state, + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + indexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -446,13 +460,18 @@ describe('IndexPattern Data Source suggestions', () => { } it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithEmptyLayer(), + '1', + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -495,13 +514,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('should apply a bucketed aggregation for a date field', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithEmptyLayer(), + '1', + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -540,13 +564,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('should select a metric for a number field', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithEmptyLayer(), + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -587,36 +616,7 @@ describe('IndexPattern Data Source suggestions', () => { it('should make a metric suggestion for a number field if there is no time field', async () => { const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - isFirstExistenceFetch: false, - indexPatterns: { - 1: { - id: '1', - title: 'no timefield', - hasRestrictions: false, - fields: [ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - - getFieldByName: getFieldByNameFactory([ - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]), - }, - }, layers: { previousLayer: { indexPatternId: '1', @@ -626,13 +626,45 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsForField(state, '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const indexPatterns = { + 1: { + id: '1', + title: 'no timefield', + hasRestrictions: false, + fields: [ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), + }, + }; + + const suggestions = getDatasourceSuggestionsForField( + state, + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + indexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -654,13 +686,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('creates a new layer and replaces layer if no match is found', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '2', { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithEmptyLayer(), + '2', + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -794,7 +831,8 @@ describe('IndexPattern Data Source suggestions', () => { type: 'date', aggregatable: true, searchable: true, - } + }, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -821,13 +859,18 @@ describe('IndexPattern Data Source suggestions', () => { it('puts a date histogram column after the last bucket column on date field', () => { (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ @@ -867,13 +910,18 @@ describe('IndexPattern Data Source suggestions', () => { }); it('does not use the same field for bucketing multiple times', () => { - const suggestions = getDatasourceSuggestionsForField(stateWithNonEmptyTables(), '1', { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + stateWithNonEmptyTables(), + '1', + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toHaveLength(0); }); @@ -881,13 +929,18 @@ describe('IndexPattern Data Source suggestions', () => { it('appends a terms column with default size on string field', () => { (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ @@ -913,13 +966,18 @@ describe('IndexPattern Data Source suggestions', () => { it('suggests both replacing and adding metric if only one other metric is set', () => { (generateId as jest.Mock).mockReturnValue('newid'); const initialState = stateWithNonEmptyTables(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ state: expect.objectContaining({ @@ -988,13 +1046,18 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }; - const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + modifiedState, + '1', + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1019,26 +1082,36 @@ describe('IndexPattern Data Source suggestions', () => { it('skips duplicates when the field is already in use', () => { const initialState = stateWithNonEmptyTables(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' })); }); it('skips metric only suggestion when the field is already in use', () => { const initialState = stateWithNonEmptyTables(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect( suggestions.some( @@ -1070,7 +1143,12 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }; - const suggestions = getDatasourceSuggestionsForField(modifiedState, '1', documentField); + const suggestions = getDatasourceSuggestionsForField( + modifiedState, + '1', + documentField, + expectedIndexPatterns + ); expect(suggestions).not.toContain(expect.objectContaining({ changeType: 'extended' })); }); @@ -1114,7 +1192,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; const suggestions = getSuggestionSubset( - getDatasourceSuggestionsForField(modifiedState, '1', documentField) + getDatasourceSuggestionsForField(modifiedState, '1', documentField, expectedIndexPatterns) ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1174,7 +1252,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; const suggestions = getSuggestionSubset( - getDatasourceSuggestionsForField(modifiedState, '1', documentField) + getDatasourceSuggestionsForField(modifiedState, '1', documentField, expectedIndexPatterns) ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1261,6 +1339,7 @@ describe('IndexPattern Data Source suggestions', () => { modifiedState, '1', documentField, + expectedIndexPatterns, (layerId) => layerId !== 'referenceLineLayer' ) ); @@ -1323,21 +1402,26 @@ describe('IndexPattern Data Source suggestions', () => { it('suggests on the layer that matches by indexPatternId', () => { const initialState = stateWithCurrentIndexPattern(); - const suggestions = getDatasourceSuggestionsForField(initialState, '2', { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', + const suggestions = getDatasourceSuggestionsForField( + initialState, + '2', + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, }, }, - }); + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1378,13 +1462,18 @@ describe('IndexPattern Data Source suggestions', () => { it('suggests on the layer with the fewest columns that matches by indexPatternId', () => { const initialState = stateWithCurrentIndexPattern(); - const suggestions = getDatasourceSuggestionsForField(initialState, '1', { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }); + const suggestions = getDatasourceSuggestionsForField( + initialState, + '1', + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1450,14 +1539,19 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toStrictEqual([]); }); it('should apply a count metric, with a timeseries bucket', () => { - const suggestions = getDatasourceSuggestionsForVisualizeCharts(stateWithoutLayer(), context); + const suggestions = getDatasourceSuggestionsForVisualizeCharts( + stateWithoutLayer(), + context, + expectedIndexPatterns + ); expect(suggestions).toContainEqual( expect.objectContaining({ @@ -1505,7 +1599,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1555,7 +1650,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1621,7 +1717,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1703,7 +1800,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1787,7 +1885,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1856,7 +1955,8 @@ describe('IndexPattern Data Source suggestions', () => { ]; const suggestions = getDatasourceSuggestionsForVisualizeCharts( stateWithoutLayer(), - updatedContext + updatedContext, + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1906,7 +2006,8 @@ describe('IndexPattern Data Source suggestions', () => { const suggestions = getDatasourceSuggestionsForVisualizeField( stateWithoutLayer(), '1', - 'field_not_exist' + 'field_not_exist', + expectedIndexPatterns ); expect(suggestions).toEqual([]); @@ -1916,7 +2017,8 @@ describe('IndexPattern Data Source suggestions', () => { const suggestions = getDatasourceSuggestionsForVisualizeField( stateWithoutLayer(), '1', - 'source' + 'source', + expectedIndexPatterns ); expect(suggestions).toContainEqual( @@ -1961,20 +2063,19 @@ describe('IndexPattern Data Source suggestions', () => { describe('#getDatasourceSuggestionsFromCurrentState', () => { it('returns no suggestions if there are no columns', () => { expect( - getDatasourceSuggestionsFromCurrentState({ - isFirstExistenceFetch: false, - indexPatternRefs: [], - existingFields: {}, - indexPatterns: expectedIndexPatterns, - layers: { - first: { - indexPatternId: '1', - columnOrder: [], - columns: {}, + getDatasourceSuggestionsFromCurrentState( + { + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, }, + currentIndexPatternId: '1', }, - currentIndexPatternId: '1', - }) + expectedIndexPatterns + ) ).toEqual([]); }); @@ -2008,7 +2109,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getDatasourceSuggestionsFromCurrentState(state); + const result = getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns); expect(result).toContainEqual( expect.objectContaining({ @@ -2094,7 +2195,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( + expect( + getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns)) + ).toContainEqual( expect.objectContaining({ table: { isMultiRow: true, @@ -2167,7 +2270,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( + expect( + getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns)) + ).toContainEqual( expect.objectContaining({ table: { isMultiRow: true, @@ -2268,7 +2373,11 @@ describe('IndexPattern Data Source suggestions', () => { expect( getSuggestionSubset( - getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'referenceLine') + getDatasourceSuggestionsFromCurrentState( + state, + expectedIndexPatterns, + (layerId) => layerId !== 'referenceLine' + ) ) ).toContainEqual( expect.objectContaining({ @@ -2352,7 +2461,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getDatasourceSuggestionsFromCurrentState(state)).not.toContainEqual( + expect( + getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns) + ).not.toContainEqual( expect.objectContaining({ table: { isMultiRow: true, @@ -2398,7 +2509,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - expect(getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state))).toContainEqual( + expect( + getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns)) + ).toContainEqual( expect.objectContaining({ table: { changeType: 'extended', @@ -2467,9 +2580,8 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState({ - ...state, - indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } }, + const suggestions = getDatasourceSuggestionsFromCurrentState(state, { + 1: { ...expectedIndexPatterns['1'], timeFieldName: undefined }, }); suggestions.forEach((suggestion) => expect(suggestion.table.columns.length).toBe(1)); }); @@ -2477,9 +2589,8 @@ describe('IndexPattern Data Source suggestions', () => { it("should not propose an over time suggestion if there's a top values aggregation with an high size", () => { const initialState = testInitialState(); (initialState.layers.first.columns.col1 as TermsIndexPatternColumn).params!.size = 6; - const suggestions = getDatasourceSuggestionsFromCurrentState({ - ...initialState, - indexPatterns: { 1: { ...initialState.indexPatterns['1'], timeFieldName: undefined } }, + const suggestions = getDatasourceSuggestionsFromCurrentState(initialState, { + 1: { ...expectedIndexPatterns['1'], timeFieldName: undefined }, }); suggestions.forEach((suggestion) => expect(suggestion.table.columns.length).toBe(1)); }); @@ -2522,9 +2633,8 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState({ - ...state, - indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } }, + const suggestions = getDatasourceSuggestionsFromCurrentState(state, { + 1: { ...expectedIndexPatterns['1'], timeFieldName: undefined }, }); suggestions.forEach((suggestion) => { const firstBucket = suggestion.table.columns.find(({ columnId }) => columnId === 'col1'); @@ -2572,19 +2682,7 @@ describe('IndexPattern Data Source suggestions', () => { }, ]; const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - indexPatterns: { - 1: { - id: '1', - title: 'my-fake-index-pattern', - hasRestrictions: false, - fields, - getFieldByName: getFieldByNameFactory(fields), - }, - }, - isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -2655,7 +2753,15 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState(state); + const suggestions = getDatasourceSuggestionsFromCurrentState(state, { + 1: { + id: '1', + title: 'my-fake-index-pattern', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }, + }); // 3 bucket cols, 2 metric cols isTableWithBucketColumns(suggestions[0], ['col1', 'col2', 'col3', 'col4', 'col5'], 3); @@ -2681,50 +2787,7 @@ describe('IndexPattern Data Source suggestions', () => { it('returns an only metric version of a given table, but does not include current state as reduced', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - indexPatterns: { - 1: { - id: '1', - title: 'my-fake-index-pattern', - hasRestrictions: false, - fields: [ - { - name: 'field1', - displayName: 'field1', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'field2', - displayName: 'field2', - type: 'date', - aggregatable: true, - searchable: true, - }, - ], - - getFieldByName: getFieldByNameFactory([ - { - name: 'field1', - displayName: 'field1', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'field2', - displayName: 'field2', - type: 'date', - aggregatable: true, - searchable: true, - }, - ]), - }, - }, - isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -2754,7 +2817,50 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + const indexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + hasRestrictions: false, + fields: [ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ], + + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + displayName: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ]), + }, + }; + + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, indexPatterns) + ); expect(suggestions).not.toContainEqual( expect.objectContaining({ table: expect.objectContaining({ @@ -2787,35 +2893,7 @@ describe('IndexPattern Data Source suggestions', () => { it('returns an alternative metric for an only-metric table', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - indexPatterns: { - 1: { - id: '1', - title: 'my-fake-index-pattern', - hasRestrictions: false, - fields: [ - { - name: 'field1', - displayName: 'field1', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - getFieldByName: getFieldByNameFactory([ - { - name: 'field1', - displayName: 'field1', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]), - }, - }, - isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -2834,7 +2912,35 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + const indexPatterns = { + 1: { + id: '1', + title: 'my-fake-index-pattern', + hasRestrictions: false, + fields: [ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + getFieldByName: getFieldByNameFactory([ + { + name: 'field1', + displayName: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]), + }, + }; + + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, indexPatterns) + ); expect(suggestions).toContainEqual( expect.objectContaining({ table: expect.objectContaining({ @@ -2851,11 +2957,7 @@ describe('IndexPattern Data Source suggestions', () => { it('contains a reordering suggestion when there are exactly 2 buckets', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - indexPatterns: expectedIndexPatterns, - isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -2894,7 +2996,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getDatasourceSuggestionsFromCurrentState(state); + const suggestions = getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns); expect(suggestions).toContainEqual( expect.objectContaining({ table: expect.objectContaining({ @@ -2907,11 +3009,7 @@ describe('IndexPattern Data Source suggestions', () => { it('will generate suggestions even if there are errors from missing fields', () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { - indexPatternRefs: [], - existingFields: {}, currentIndexPatternId: '1', - indexPatterns: expectedIndexPatterns, - isFirstExistenceFetch: false, layers: { first: { ...initialState.layers.first, @@ -2932,7 +3030,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const suggestions = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + const suggestions = getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns) + ); expect(suggestions).toContainEqual( expect.objectContaining({ table: { @@ -3006,7 +3106,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + const result = getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns) + ); expect(result).toContainEqual( expect.objectContaining({ @@ -3091,7 +3193,9 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getSuggestionSubset(getDatasourceSuggestionsFromCurrentState(state)); + const result = getSuggestionSubset( + getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns) + ); expect(result).toContainEqual( expect.objectContaining({ @@ -3235,7 +3339,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getDatasourceSuggestionsFromCurrentState(state); + const result = getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns); // only generate suggestions for top level metrics + only first metric with all buckets expect( @@ -3316,7 +3420,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getDatasourceSuggestionsFromCurrentState(state); + const result = getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns); // only generate suggestions for top level metrics expect( @@ -3366,7 +3470,7 @@ describe('IndexPattern Data Source suggestions', () => { }, }; - const result = getDatasourceSuggestionsFromCurrentState(state); + const result = getDatasourceSuggestionsFromCurrentState(state, expectedIndexPatterns); expect( result.filter((suggestion) => suggestion.table.changeType === 'unchanged').length diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 3d366408595b0f..d481e77bd69a83 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -136,14 +136,7 @@ const fieldsThree = [ ]; const initialState: IndexPatternPrivateState = { - indexPatternRefs: [ - { id: '1', title: 'my-fake-index-pattern' }, - { id: '2', title: 'my-fake-restricted-pattern' }, - { id: '3', title: 'my-compatible-pattern' }, - ], - existingFields: {}, currentIndexPatternId: '1', - isFirstExistenceFetch: false, layers: { first: { indexPatternId: '1', @@ -173,32 +166,6 @@ const initialState: IndexPatternPrivateState = { }, }, }, - indexPatterns: { - '1': { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: fieldsOne, - getFieldByName: getFieldByNameFactory(fieldsOne), - }, - '2': { - id: '2', - title: 'my-fake-restricted-pattern', - hasRestrictions: true, - timeFieldName: 'timestamp', - fields: fieldsTwo, - getFieldByName: getFieldByNameFactory(fieldsTwo), - }, - '3': { - id: '3', - title: 'my-compatible-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: fieldsThree, - getFieldByName: getFieldByNameFactory(fieldsThree), - }, - }, }; describe('Layer Data Panel', () => { let defaultProps: IndexPatternLayerPanelProps; @@ -207,8 +174,42 @@ describe('Layer Data Panel', () => { defaultProps = { layerId: 'first', state: initialState, - setState: jest.fn(), - onChangeIndexPattern: jest.fn(async () => {}), + onChangeIndexPattern: jest.fn(), + dataViews: { + indexPatternRefs: [ + { id: '1', title: 'my-fake-index-pattern' }, + { id: '2', title: 'my-fake-restricted-pattern' }, + { id: '3', title: 'my-compatible-pattern' }, + ], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: { + '1': { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsOne, + getFieldByName: getFieldByNameFactory(fieldsOne), + }, + '2': { + id: '2', + title: 'my-fake-restricted-pattern', + hasRestrictions: true, + timeFieldName: 'timestamp', + fields: fieldsTwo, + getFieldByName: getFieldByNameFactory(fieldsTwo), + }, + '3': { + id: '3', + title: 'my-compatible-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: fieldsThree, + getFieldByName: getFieldByNameFactory(fieldsThree), + }, + }, + }, }; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 079a866676f04e..d76e6723c1be9e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -7,7 +7,7 @@ import { DragContextState } from '../drag_drop'; import { getFieldByNameFactory } from './pure_helpers'; -import type { IndexPattern, IndexPatternField } from './types'; +import type { IndexPattern, IndexPatternField } from '../types'; export const createMockedIndexPatternWithoutType = ( typeToFilter: IndexPatternField['type'] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts index dae677663d2895..d8e8af06b485af 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts @@ -18,7 +18,8 @@ import { } from './definitions'; import { getFieldByNameFactory } from '../pure_helpers'; import { documentField } from '../document_field'; -import { IndexPattern, IndexPatternLayer, IndexPatternField } from '../types'; +import { IndexPatternLayer } from '../types'; +import { IndexPattern, IndexPatternField } from '../../types'; import { GenericIndexPatternColumn } from '.'; import { DateHistogramIndexPatternColumn } from './definitions/date_histogram'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 73cb0a37ad563d..ddcb072cf76662 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -17,7 +17,8 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { dataPluginMock, getCalculateAutoTimeExpression } from '@kbn/data-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; -import type { IndexPatternLayer, IndexPattern } from '../../types'; +import type { IndexPatternLayer } from '../../types'; +import type { IndexPattern } from '../../../types'; import { getFieldByNameFactory } from '../../pure_helpers'; import { act } from 'react-dom/test-utils'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts index 8a59636d099526..d90a021a7cb12d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts @@ -11,8 +11,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../../../mocks'; import { GenericOperationDefinition } from '../..'; -import type { IndexPatternField } from '../../../../types'; -import type { OperationMetadata } from '../../../../../types'; +import type { OperationMetadata, IndexPatternField } from '../../../../../types'; import { tinymathFunctions } from '../util'; import { getSignatureHelp, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index 4a8c6d437f2ac8..e68bb0d3142ef6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -10,7 +10,7 @@ import { formulaOperation, GenericOperationDefinition, GenericIndexPatternColumn import { FormulaIndexPatternColumn } from './formula'; import { insertOrReplaceFormulaColumn } from './parse'; import type { IndexPatternLayer } from '../../../types'; -import { IndexPattern, IndexPatternField } from '../../../../editor_frame_service/types'; +import { IndexPattern, IndexPatternField } from '../../../../types'; import { tinymathFunctions } from './util'; import { TermsIndexPatternColumn } from '../terms'; import { MovingAverageIndexPatternColumn } from '../calculations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts index d1ee2023e1d1c9..fec643772cc767 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { convertDataViewIntoLensIndexPattern } from '../../../loader'; import { insertOrReplaceFormulaColumn } from './parse'; 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 '../../../../data_views_service/loader'; jest.mock('./parse', () => ({ insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 2cbe6f5e0c8272..c06c595a7bff20 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -48,19 +48,20 @@ import { countOperation } from './count'; import { mathOperation, formulaOperation } from './formula'; import { staticValueOperation } from './static_value'; import { lastValueOperation } from './last_value'; -import { FrameDatasourceAPI, OperationMetadata, ParamEditorCustomProps } from '../../../types'; +import { + FrameDatasourceAPI, + IndexPattern, + IndexPatternField, + OperationMetadata, + ParamEditorCustomProps, +} from '../../../types'; import type { BaseIndexPatternColumn, IncompleteColumn, GenericIndexPatternColumn, ReferenceBasedIndexPatternColumn, } from './column_types'; -import { - DataViewDragDropOperation, - IndexPattern, - IndexPatternField, - IndexPatternLayer, -} from '../../types'; +import { DataViewDragDropOperation, IndexPatternLayer } from '../../types'; import { DateRange, LayerType } from '../../../../common'; import { rangeOperation } from './ranges'; import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index f2248fcdf36c9c..ad7c9f107b7d02 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -16,7 +16,8 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { LastValueIndexPatternColumn } from './last_value'; import { lastValueOperation } from '.'; -import type { IndexPattern, IndexPatternLayer } from '../../types'; +import type { IndexPatternLayer } from '../../types'; +import type { IndexPattern } from '../../../types'; import { TermsIndexPatternColumn } from './terms'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index a359ae0b898201..a0723c9d55e237 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -17,7 +17,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { percentileOperation } from '.'; -import { IndexPattern, IndexPatternLayer } from '../../types'; +import { IndexPatternLayer } from '../../types'; import { PercentileIndexPatternColumn } from './percentile'; import { TermsIndexPatternColumn } from './terms'; import { @@ -26,6 +26,7 @@ import { ExpressionAstExpressionBuilder, } from '@kbn/expressions-plugin/public'; import type { OriginalColumn } from '../../to_expression'; +import { IndexPattern } from '../../../types'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx index 62c0bbd45be6cc..96766a2e95d3db 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile_ranks.test.tsx @@ -17,9 +17,10 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { percentileRanksOperation } from '.'; -import { IndexPattern, IndexPatternLayer } from '../../types'; +import { IndexPatternLayer } from '../../types'; import type { PercentileRanksIndexPatternColumn } from './percentile_ranks'; import { TermsIndexPatternColumn } from './terms'; +import { IndexPattern } from '../../../types'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index e0c1c4b3b84cce..ba93999e6c6f28 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -25,9 +25,9 @@ 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 '../../../../editor_frame_service/types'; +import { IndexPattern } from '../../../../types'; // mocking random id generator function jest.mock('@elastic/eui', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index ea870e68f563b3..66a4549bad3930 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -16,7 +16,8 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { staticValueOperation } from '.'; -import { IndexPattern, IndexPatternLayer } from '../../types'; +import { IndexPatternLayer } from '../../types'; +import { IndexPattern } from '../../../types'; import { StaticValueIndexPatternColumn } from './static_value'; import { TermsIndexPatternColumn } from './terms'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index f01ccebdabcafb..8d28e9a76274c4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -34,7 +34,7 @@ import { DateHistogramIndexPatternColumn } from '../date_histogram'; import { getOperationSupportMatrix } from '../../../dimension_panel/operation_support'; import { FieldSelect } from '../../../dimension_panel/field_select'; import { ReferenceEditor } from '../../../dimension_panel/reference_editor'; -import { IndexPattern } from '../../../../editor_frame_service/types'; +import { IndexPattern } from '../../../../types'; import { cloneDeep } from 'lodash'; import { IncludeExcludeRow } from './include_exclude_options'; @@ -1174,14 +1174,13 @@ describe('terms', () => { return getOperationSupportMatrix({ state: { layers: { layer1: layer }, - indexPatterns: { - [defaultProps.indexPattern.id]: defaultProps.indexPattern, - }, - existingFields, } as unknown as IndexPatternPrivateState, layerId: 'layer1', filterOperations: () => true, columnId, + indexPatterns: { + [defaultProps.indexPattern.id]: defaultProps.indexPattern, + }, }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index cfbc5d4455fd27..613c41ea74194b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -38,7 +38,7 @@ import { } from './definitions'; import { TinymathAST } from '@kbn/tinymath'; import { CoreStart } from '@kbn/core/public'; -import { IndexPattern } from '../../editor_frame_service/types'; +import { IndexPattern } from '../../types'; jest.mock('.'); jest.mock('../../id_generator'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts deleted file mode 100644 index 90673189b4a83a..00000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fieldExists } from './pure_helpers'; - -describe('fieldExists', () => { - it('returns whether or not a field exists', () => { - expect(fieldExists({ a: { b: true } }, 'a', 'b')).toBeTruthy(); - expect(fieldExists({ a: { b: true } }, 'a', 'c')).toBeFalsy(); - expect(fieldExists({ a: { b: true } }, 'b', 'b')).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts new file mode 100644 index 00000000000000..4c2e77078dd285 --- /dev/null +++ b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts @@ -0,0 +1,19 @@ +/* + * 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 { IndexPatternServiceAPI } from '../data_views_service/service'; + +export function createIndexPatternServiceMock(): IndexPatternServiceAPI { + return { + loadIndexPatterns: jest.fn(), + loadIndexPatternRefs: jest.fn(), + ensureIndexPattern: jest.fn(), + refreshExistingFields: jest.fn(), + getDefaultIndex: jest.fn(), + updateIndexPatternsCache: jest.fn(), + }; +} diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index 90bb1ea9f5cab2..cf8370efdece9b 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -25,10 +25,12 @@ export function createMockDatasource(id: string): DatasourceMock { return { id: 'testDatasource', clearLayer: jest.fn((state, _layerId) => state), - getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn) => []), - getDatasourceSuggestionsForVisualizeField: jest.fn((_state, _indexpatternId, _fieldName) => []), - getDatasourceSuggestionsForVisualizeCharts: jest.fn((_state, _context) => []), - getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []), + getDatasourceSuggestionsForField: jest.fn((_state, _item, filterFn, _indexPatterns) => []), + getDatasourceSuggestionsForVisualizeField: jest.fn( + (_state, _indexpatternId, _fieldName, _indexPatterns) => [] + ), + getDatasourceSuggestionsForVisualizeCharts: jest.fn((_state, _context, _indexPatterns) => []), + getDatasourceSuggestionsFromCurrentState: jest.fn((_state, _indexPatterns) => []), getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [{ type: 'index-pattern', id: 'mockip', name: 'mockip' }], @@ -39,7 +41,7 @@ export function createMockDatasource(id: string): DatasourceMock { renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), getCurrentIndexPatternId: jest.fn(), - toExpression: jest.fn((_frame, _state) => null), + toExpression: jest.fn((_frame, _state, _indexPatterns) => null), insertLayer: jest.fn((_state, _newLayerId) => ({})), removeLayer: jest.fn((_state, _layerId) => {}), removeColumn: jest.fn((props) => {}), @@ -53,12 +55,13 @@ export function createMockDatasource(id: string): DatasourceMock { // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, - getErrorMessages: jest.fn((_state) => undefined), - checkIntegrity: jest.fn((_state) => []), + getErrorMessages: jest.fn((_state, _indexPatterns) => undefined), + checkIntegrity: jest.fn((_state, _indexPatterns) => []), isTimeBased: jest.fn(), isValidColumn: jest.fn(), isEqual: jest.fn(), getUsedDataView: jest.fn(), + onRefreshIndexPattern: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index 58ef8ce05c613d..0d90e719ebadee 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -32,6 +32,12 @@ export type FrameMock = jest.Mocked; 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 type FrameDatasourceMock = jest.Mocked; @@ -41,4 +47,10 @@ export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({ 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: {}, + }, }); diff --git a/x-pack/plugins/lens/public/mocks/store_mocks.tsx b/x-pack/plugins/lens/public/mocks/store_mocks.tsx index 2e6bc86a9d33e2..5fab4602b0483b 100644 --- a/x-pack/plugins/lens/public/mocks/store_mocks.tsx +++ b/x-pack/plugins/lens/public/mocks/store_mocks.tsx @@ -58,6 +58,12 @@ export const defaultState = { activeId: 'testVis', }, datasourceStates: mockDatasourceStates(), + dataViews: { + indexPatterns: {}, + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + }, }; export function makeLensStore({ diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index f0a2b9f28623d9..b97b3e0679df4d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -8,7 +8,6 @@ import { Ast, fromExpression } from '@kbn/interpreter'; import { Position } from '@elastic/charts'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { getXyVisualization } from './xy_visualization'; import { OperationDescriptor } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; @@ -16,17 +15,21 @@ import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; import { defaultReferenceLineColor } from './color_assignment'; -import { themeServiceMock } from '@kbn/core/public/mocks'; +import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; import { LegendSize } from '@kbn/visualizations-plugin/common'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ - datatableUtilities: createDatatableUtilitiesMock(), paletteService: chartPluginMock.createPaletteRegistry(), fieldFormats: fieldFormatsServiceMock.createStartContract(), kibanaTheme: themeServiceMock.createStartContract(), useLegacyTimeAxis: false, eventAnnotationService: eventAnnotationServiceMock, + core: coreMock.createStart(), + storage: {} as IStorageWrapper, + data: dataPluginMock.createStartContract(), }); let mockDatasource: ReturnType; let frame: ReturnType; @@ -54,7 +57,8 @@ describe('#toExpression', () => { const datasourceExpression = mockDatasource.toExpression( frame.datasourceLayers.first, - 'first' + 'first', + frame.dataViews.indexPatterns ) ?? { type: 'expression', chain: [], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index a763053d724750..c5712938da5d0c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -114,7 +114,6 @@ export interface XYAnnotationLayerConfig { layerType: 'annotations'; annotations: EventAnnotationConfig[]; hide?: boolean; - indexPatternId: string; simpleView?: boolean; } diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index d16cbd86df3cf7..24df7ad7426663 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -17,7 +17,6 @@ import type { XYReferenceLineLayerConfig, SeriesType, } from './types'; -import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { layerTypes } from '../../common'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; @@ -25,9 +24,11 @@ import type { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/pu import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { Datatable } from '@kbn/expressions-plugin/common'; -import { themeServiceMock } from '@kbn/core/public/mocks'; +import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; const exampleAnnotation: EventAnnotationConfig = { id: 'an1', @@ -69,12 +70,14 @@ const paletteServiceMock = chartPluginMock.createPaletteRegistry(); const fieldFormatsMock = fieldFormatsServiceMock.createStartContract(); const xyVisualization = getXyVisualization({ - datatableUtilities: createDatatableUtilitiesMock(), paletteService: paletteServiceMock, fieldFormats: fieldFormatsMock, useLegacyTimeAxis: false, kibanaTheme: themeServiceMock.createStartContract(), eventAnnotationService: eventAnnotationServiceMock, + core: coreMock.createStart(), + storage: {} as IStorageWrapper, + data: dataPluginMock.createStartContract(), }); describe('xy_visualization', () => { @@ -335,6 +338,7 @@ describe('xy_visualization', () => { ]); frame = { + ...frame, datasourceLayers: { first: mockDatasource.publicAPIMock, }, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index aedbcbb648957b..4a318965265c6b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -206,7 +206,7 @@ export const getXyVisualization = ({ return state; } const newLayers = [...state.layers]; - newLayers[layerIndex] = { ...layer, indexPatternId }; + newLayers[layerIndex] = { ...layer }; return { ...state, layers: newLayers, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index fb364d7bd09f77..34d9bbd4adc8c3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -287,7 +287,6 @@ const newLayerFn = { layerId, layerType: layerTypes.ANNOTATIONS, annotations: [], - indexPatternId, }), }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 14750dd34a127e..6ea8b84abf9363 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -17,22 +17,25 @@ import { import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; import type { PaletteOutput } from '@kbn/coloring'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { themeServiceMock } from '@kbn/core/public/mocks'; +import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; jest.mock('../id_generator'); const xyVisualization = getXyVisualization({ - datatableUtilities: createDatatableUtilitiesMock(), paletteService: chartPluginMock.createPaletteRegistry(), fieldFormats: fieldFormatsServiceMock.createStartContract(), useLegacyTimeAxis: false, kibanaTheme: themeServiceMock.createStartContract(), eventAnnotationService: eventAnnotationServiceMock, + core: coreMock.createStart(), + storage: {} as IStorageWrapper, + data: dataPluginMock.createStartContract(), }); describe('xy_suggestions', () => { From 2ee6f6d9890cbfcfe66c1b1874d0771b579305c8 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 29 Jul 2022 17:12:46 +0200 Subject: [PATCH 17/98] :label: Fix types issues --- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- .../lens/public/app_plugin/mounter.tsx | 2 +- .../plugins/lens/public/app_plugin/types.ts | 2 +- .../editor_frame/config_panel/layer_panel.tsx | 2 +- .../editor_frame/config_panel/types.ts | 2 +- .../editor_frame/data_panel_wrapper.tsx | 2 +- .../editor_frame/editor_frame.test.tsx | 4 +- .../editor_frame/editor_frame.tsx | 2 +- .../editor_frame/state_helpers.ts | 2 +- .../lens/public/embeddable/embeddable.tsx | 2 +- .../__mocks__/loader.ts | 22 +- .../indexpattern_datasource/datapanel.tsx | 2 +- .../indexpattern_datasource/loader.test.ts | 913 +++--------------- .../public/indexpattern_datasource/loader.ts | 4 +- .../formula/formula_public_api.test.ts | 2 +- .../definitions/formula/formula_public_api.ts | 2 +- .../indexpattern_service/loader.test.ts | 445 +++++++++ .../loader.ts | 18 +- .../lens/public/indexpattern_service/mocks.ts | 217 +++++ .../service.ts | 5 +- .../public/mocks/data_views_service_mock.ts | 2 +- .../plugins/lens/public/mocks/store_mocks.tsx | 2 +- .../public/state_management/lens_slice.ts | 8 +- x-pack/plugins/lens/public/types.ts | 2 +- x-pack/plugins/lens/public/utils.ts | 2 +- .../annotations/helpers.test.ts | 8 +- 26 files changed, 831 insertions(+), 845 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_service/loader.test.ts rename x-pack/plugins/lens/public/{data_views_service => indexpattern_service}/loader.ts (94%) create mode 100644 x-pack/plugins/lens/public/indexpattern_service/mocks.ts rename x-pack/plugins/lens/public/{data_views_service => indexpattern_service}/service.ts (96%) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 93d14d4306fb04..7ff105adf43f75 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -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 '../data_views_service/service'; +import { IndexPatternServiceAPI, createIndexPatternService } from '../indexpattern_service/service'; export type SaveProps = Omit & { returnToOrigin: boolean; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 17e2ae3e0b2eda..4fcd426378e499 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -223,7 +223,7 @@ export async function mountApp( }; const lensStore: LensRootStore = makeConfigureStore(storeDeps, { lens: getPreloadedState(storeDeps) as LensAppState, - } as PreloadedState); + } as unknown as PreloadedState); const EditorRenderer = React.memo( (props: { id?: string; history: History; editByValue?: boolean }) => { diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index fe4df5f26593d1..6a2718fc498203 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -47,7 +47,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 '../data_views_service/service'; +import { IndexPatternServiceAPI } from '../indexpattern_service/service'; export interface RedirectToOriginProps { input?: LensEmbeddableInput; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 915a1e05e6ec69..ea33bbc2ac91a3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -18,7 +18,7 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternServiceAPI } from '../../../data_views_service/service'; +import { IndexPatternServiceAPI } from '../../../indexpattern_service/service'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index c4a2d77c30baba..3e1458d42c438e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -6,7 +6,7 @@ */ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { IndexPatternServiceAPI } from '../../../data_views_service/service'; +import { IndexPatternServiceAPI } from '../../../indexpattern_service/service'; import { Visualization, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 09f8233ef1bca3..4cabda4cc62dfb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -29,7 +29,7 @@ import { selectDatasourceStates, } from '../../state_management'; import { initializeSources } from './state_helpers'; -import type { IndexPatternServiceAPI } from '../../data_views_service/service'; +import type { IndexPatternServiceAPI } from '../../indexpattern_service/service'; import { changeIndexPattern } from '../../state_management/lens_slice'; interface DataPanelWrapperProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 035011b7d4a680..9c5b7740cacfff 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -390,7 +390,7 @@ describe('editor_frame', () => { describe('datasource public api communication', () => { it('should give access to the datasource state in the datasource factory function', async () => { const datasourceState = {}; - mockDatasource.initialize.mockResolvedValue(datasourceState); + mockDatasource.initialize.mockReturnValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); const props = { @@ -503,7 +503,7 @@ describe('editor_frame', () => { it('should call datasource render with new state on switch', async () => { const initialState = {}; - mockDatasource2.initialize.mockResolvedValue(initialState); + mockDatasource2.initialize.mockReturnValue(initialState); instance.find('button[data-test-subj="datasource-switch"]').simulate('click'); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index ed8357ea681ef5..b3812459948636 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -28,7 +28,7 @@ import { selectVisualization, } from '../../state_management'; import type { LensInspector } from '../../lens_inspector_service'; -import { IndexPatternServiceAPI } from '../../data_views_service/service'; +import { IndexPatternServiceAPI } from '../../indexpattern_service/service'; export interface EditorFrameProps { datasourceMap: DatasourceMap; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index f9136af24dde83..0a42917bb86aba 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -37,7 +37,7 @@ import { } from '../error_helper'; import type { DatasourceStates, DataViewsState } from '../../state_management'; import { readFromStorage } from '../../settings_storage'; -import { loadIndexPatternRefs, loadIndexPatterns } from '../../data_views_service/loader'; +import { loadIndexPatternRefs, loadIndexPatterns } from '../../indexpattern_service/loader'; function getIndexPatterns( references?: SavedObjectReference[], diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 75787ba08402fe..98dddaa0441bdb 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -76,7 +76,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 '../data_views_service/loader'; +import { convertDataViewIntoLensIndexPattern } from '../indexpattern_service/loader'; export type LensSavedObjectAttributes = Omit; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index 072087e0de65c0..1273d596576c40 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -10,17 +10,9 @@ import { IndexPatternPrivateState } from '../types'; export function loadInitialState() { const indexPattern = createMockedIndexPattern(); - const restricted = createMockedRestrictedIndexPattern(); const result: IndexPatternPrivateState = { currentIndexPatternId: indexPattern.id, - indexPatternRefs: [], - existingFields: {}, - indexPatterns: { - [indexPattern.id]: indexPattern, - [restricted.id]: restricted, - }, layers: {}, - isFirstExistenceFetch: false, }; return result; } @@ -30,3 +22,17 @@ const originalLoader = jest.requireActual('../loader'); export const extractReferences = originalLoader.extractReferences; export const injectReferences = originalLoader.injectReferences; + +export function loadInitialDataViews() { + const indexPattern = createMockedIndexPattern(); + const restricted = createMockedRestrictedIndexPattern(); + return { + indexPatternRefs: [], + existingFields: {}, + indexPatterns: { + [indexPattern.id]: indexPattern, + [restricted.id]: restricted, + }, + isFirstExistenceFetch: false, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 17f065c4f05f58..8bac417fb53ae3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -47,7 +47,7 @@ import { Loader } from '../loader'; import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon'; import { FieldGroups, FieldList } from './field_list'; import { fieldContainsData, fieldExists } from '../shared_components'; -import { IndexPatternServiceAPI } from '../data_views_service/service'; +import { IndexPatternServiceAPI } from '../indexpattern_service/service'; export type Props = Omit< DatasourceDataPanelProps, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index b3806e5c55b15f..c31f9167e2b24f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -5,30 +5,16 @@ * 2.0. */ -import { HttpHandler } from '@kbn/core/public'; -import { last } from 'lodash'; import { loadInitialState, - loadIndexPatterns, changeIndexPattern, changeLayerIndexPattern, - syncExistingFields, extractReferences, injectReferences, } from './loader'; -import { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; -import { createHttpFetchError } from '@kbn/core-http-browser-mocks'; -import { - IndexPatternPersistedState, - IndexPatternPrivateState, - IndexPatternField, - IndexPattern, -} from './types'; -import { createMockedRestrictedIndexPattern, createMockedIndexPattern } from './mocks'; -import { documentField } from './document_field'; -import { DateHistogramIndexPatternColumn } from './operations'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; +import { DateHistogramIndexPatternColumn, TermsIndexPatternColumn } from './operations'; +import { sampleIndexPatterns } from '../indexpattern_service/mocks'; const createMockStorage = (lastData?: Record) => { return { @@ -39,351 +25,29 @@ const createMockStorage = (lastData?: Record) => { }; }; -const indexPattern1 = { - id: '1', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasRestrictions: false, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'start_date', - displayName: 'start_date', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - esTypes: ['keyword'], - }, - { - name: 'unsupported', - displayName: 'unsupported', - type: 'geo', - aggregatable: true, - searchable: true, - }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - esTypes: ['keyword'], - }, - { - name: 'geo.src', - displayName: 'geo.src', - type: 'string', - aggregatable: true, - searchable: true, - esTypes: ['keyword'], - }, - { - name: 'scripted', - displayName: 'Scripted', - type: 'string', - searchable: true, - aggregatable: true, - scripted: true, - lang: 'painless', - script: '1234', - }, - documentField, - ], -} as unknown as IndexPattern; - -const sampleIndexPatternsFromService = { - '1': createMockedIndexPattern(), - '2': createMockedRestrictedIndexPattern(), -}; - -const indexPattern2 = { - id: '2', - title: 'my-fake-restricted-pattern', - timeFieldName: 'timestamp', - hasRestrictions: true, - fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - fixed_interval: '1d', - delay: '7d', - time_zone: 'UTC', - }, - }, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - aggregationRestrictions: { - // Ignored in the UI - histogram: { - agg: 'histogram', - interval: 1000, - }, - average: { - agg: 'avg', - }, - max: { - agg: 'max', - }, - min: { - agg: 'min', - }, - sum: { - agg: 'sum', - }, - }, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - scripted: true, - lang: 'painless', - script: '1234', - aggregationRestrictions: { - terms: { - agg: 'terms', - }, - }, - }, - documentField, - ], -} as unknown as IndexPattern; - -const sampleIndexPatterns = { - '1': indexPattern1, - '2': indexPattern2, -}; - -function mockIndexPatternsService() { - return { - get: jest.fn(async (id: '1' | '2') => { - const result = { ...sampleIndexPatternsFromService[id], metaFields: [] }; - if (!result.fields) { - result.fields = []; - } - return result; - }), - getIdsWithTitle: jest.fn(async () => { - return [ - { - id: sampleIndexPatterns[1].id, - title: sampleIndexPatterns[1].title, - }, - { - id: sampleIndexPatterns[2].id, - title: sampleIndexPatterns[2].title, - }, - ]; - }), - } as unknown as Pick; -} +const indexPatternRefs = [ + { + id: sampleIndexPatterns[1].id, + title: sampleIndexPatterns[1].title, + }, + { + id: sampleIndexPatterns[2].id, + title: sampleIndexPatterns[2].title, + }, +]; describe('loader', () => { - describe('loadIndexPatterns', () => { - it('should not load index patterns that are already loaded', async () => { - const cache = await loadIndexPatterns({ - cache: sampleIndexPatterns, - patterns: ['1', '2'], - indexPatternsService: { - get: jest.fn(() => - Promise.reject('mockIndexPatternService.get should not have been called') - ), - getIdsWithTitle: jest.fn(), - } as unknown as Pick, - }); - - expect(cache).toEqual(sampleIndexPatterns); - }); - - it('should load index patterns that are not loaded', async () => { - const cache = await loadIndexPatterns({ - cache: { - '2': sampleIndexPatterns['2'], - }, - patterns: ['1', '2'], - indexPatternsService: mockIndexPatternsService(), - }); - - expect(cache).toMatchObject(sampleIndexPatterns); - }); - - it('should allow scripted, but not full text fields', async () => { - const cache = await loadIndexPatterns({ - cache: {}, - patterns: ['1', '2'], - indexPatternsService: mockIndexPatternsService(), - }); - - expect(cache).toMatchObject(sampleIndexPatterns); - }); - - it('should apply field restrictions from typeMeta', async () => { - const cache = await loadIndexPatterns({ - cache: {}, - patterns: ['foo'], - indexPatternsService: { - get: jest.fn(async () => ({ - id: 'foo', - title: 'Foo index', - metaFields: [], - typeMeta: { - aggs: { - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: 'm', - }, - }, - sum: { - bytes: { - agg: 'sum', - }, - }, - }, - }, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - })), - getIdsWithTitle: jest.fn(async () => ({ - id: 'foo', - title: 'Foo index', - })), - } as unknown as Pick, - }); - - expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ - sum: { agg: 'sum' }, - }); - expect(cache.foo.getFieldByName('timestamp')!.aggregationRestrictions).toEqual({ - date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, - }); - }); - - it('should map meta flag', async () => { - const cache = await loadIndexPatterns({ - cache: {}, - patterns: ['foo'], - indexPatternsService: { - get: jest.fn(async () => ({ - id: 'foo', - title: 'Foo index', - metaFields: ['timestamp'], - typeMeta: { - aggs: { - date_histogram: { - timestamp: { - agg: 'date_histogram', - fixed_interval: 'm', - }, - }, - sum: { - bytes: { - agg: 'sum', - }, - }, - }, - }, - fields: [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - })), - getIdsWithTitle: jest.fn(async () => ({ - id: 'foo', - title: 'Foo index', - })), - } as unknown as Pick, - }); - - expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); - }); - }); - describe('loadInitialState', () => { it('should load a default state', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - indexPatternsService: mockIndexPatternsService(), + indexPatterns: sampleIndexPatterns, + indexPatternRefs, storage, - options: { isFullEditor: true }, }); expect(state).toMatchObject({ currentIndexPatternId: '1', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -393,41 +57,30 @@ describe('loader', () => { it('should load a default state without loading the indexPatterns when embedded', async () => { const storage = createMockStorage(); - const indexPatternsService = mockIndexPatternsService(); const state = await loadInitialState({ - indexPatternsService, storage, - options: { isFullEditor: false }, + indexPatterns: {}, + indexPatternRefs: [], }); expect(state).toMatchObject({ currentIndexPatternId: undefined, - indexPatternRefs: [], - indexPatterns: {}, layers: {}, }); expect(storage.set).not.toHaveBeenCalled(); - expect(indexPatternsService.getIdsWithTitle).not.toHaveBeenCalled(); }); it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { const storage = createMockStorage({ indexPatternId: 'c' }); const state = await loadInitialState({ - indexPatternsService: mockIndexPatternsService(), storage, - options: { isFullEditor: true }, + indexPatternRefs, + indexPatterns: sampleIndexPatterns, }); expect(state).toMatchObject({ currentIndexPatternId: '1', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -437,20 +90,13 @@ describe('loader', () => { it('should load lastUsedIndexPatternId if in localStorage', async () => { const state = await loadInitialState({ - indexPatternsService: mockIndexPatternsService(), storage: createMockStorage({ indexPatternId: '2' }), - options: { isFullEditor: true }, + indexPatternRefs, + indexPatterns: sampleIndexPatterns, }); expect(state).toMatchObject({ currentIndexPatternId: '2', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '2': sampleIndexPatterns['2'], - }, layers: {}, }); }); @@ -459,20 +105,13 @@ describe('loader', () => { const storage = createMockStorage(); const state = await loadInitialState({ defaultIndexPatternId: '2', - indexPatternsService: mockIndexPatternsService(), storage, - options: { isFullEditor: true }, + indexPatternRefs, + indexPatterns: sampleIndexPatterns, }); expect(state).toMatchObject({ currentIndexPatternId: '2', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '2': sampleIndexPatterns['2'], - }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -483,24 +122,17 @@ describe('loader', () => { it('should use the indexPatternId of the visualize trigger field, if provided', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - indexPatternsService: mockIndexPatternsService(), storage, initialContext: { indexPatternId: '1', fieldName: '', }, - options: { isFullEditor: true }, + indexPatternRefs, + indexPatterns: sampleIndexPatterns, }); expect(state).toMatchObject({ currentIndexPatternId: '1', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -511,7 +143,6 @@ describe('loader', () => { it('should use the indexPatternId of the visualize trigger chart context, if provided', async () => { const storage = createMockStorage(); const state = await loadInitialState({ - indexPatternsService: mockIndexPatternsService(), storage, initialContext: { layers: [ @@ -541,18 +172,12 @@ describe('loader', () => { savedObjectId: '', isVisualizeAction: true, }, - options: { isFullEditor: true }, + indexPatternRefs, + indexPatterns: sampleIndexPatterns, }); expect(state).toMatchObject({ currentIndexPatternId: '1', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, layers: {}, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -594,17 +219,15 @@ describe('loader', () => { { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], - indexPatternsService: mockIndexPatternsService(), + indexPatternRefs: [], + indexPatterns: { + '2': sampleIndexPatterns['2'], + }, storage, - options: { isFullEditor: false }, }); expect(state).toMatchObject({ currentIndexPatternId: undefined, - indexPatternRefs: [], - indexPatterns: { - '2': sampleIndexPatterns['2'], - }, layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); expect(storage.set).not.toHaveBeenCalled(); @@ -644,111 +267,28 @@ describe('loader', () => { { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, { name: 'another-reference', id: 'c', type: 'index-pattern' }, ], - indexPatternsService: mockIndexPatternsService(), - storage, - options: { isFullEditor: true }, - }); - expect(state).toMatchObject({ - currentIndexPatternId: '2', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], + indexPatternRefs, indexPatterns: { '2': sampleIndexPatterns['2'], }, - layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, - }); - - expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: '2', - }); - }); - - it('should default to the first loaded index pattern if could not load any used one or one from the storage', async () => { - function mockIndexPatternsServiceWithConflict() { - return { - get: jest.fn(async (id: '1' | '2' | 'conflictId') => { - if (id === 'conflictId') { - return Promise.reject(new Error('Oh noes conflict boom')); - } - const result = { ...sampleIndexPatternsFromService[id], metaFields: [] }; - if (!result.fields) { - result.fields = []; - } - return result; - }), - getIdsWithTitle: jest.fn(async () => { - return [ - { - id: sampleIndexPatterns[1].id, - title: sampleIndexPatterns[1].title, - }, - { - id: sampleIndexPatterns[2].id, - title: sampleIndexPatterns[2].title, - }, - { - id: 'conflictId', - title: 'conflictId title', - }, - ]; - }), - } as unknown as Pick; - } - const savedState: IndexPatternPersistedState = { - layers: { - layerb: { - columnOrder: ['col1', 'col2'], - columns: { - col1: { - dataType: 'date', - isBucketed: true, - label: 'My date', - operationType: 'date_histogram', - params: { - interval: 'm', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - col2: { - dataType: 'number', - isBucketed: false, - label: 'Sum of bytes', - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }, - }, - }; - const storage = createMockStorage({ indexPatternId: 'conflictId' }); - const state = await loadInitialState({ - persistedState: savedState, - references: [ - { name: 'indexpattern-datasource-layer-layerb', id: 'conflictId', type: 'index-pattern' }, - ], - indexPatternsService: mockIndexPatternsServiceWithConflict(), storage, - options: { isFullEditor: true }, }); expect(state).toMatchObject({ - currentIndexPatternId: '1', + currentIndexPatternId: '2', indexPatternRefs: [ - { id: 'conflictId', title: 'conflictId title' }, { id: '1', title: sampleIndexPatterns['1'].title }, { id: '2', title: sampleIndexPatterns['2'].title }, ], indexPatterns: { - '1': sampleIndexPatterns['1'], + '2': sampleIndexPatterns['2'], }, - layers: { layerb: { ...savedState.layers.layerb, indexPatternId: 'conflictId' } }, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { - indexPatternId: '1', + indexPatternId: '2', }); }); }); @@ -756,9 +296,6 @@ describe('loader', () => { describe('saved object references', () => { const state: IndexPatternPrivateState = { currentIndexPatternId: 'b', - indexPatternRefs: [], - indexPatterns: {}, - existingFields: {}, layers: { a: { indexPatternId: 'id-index-pattern-a', @@ -787,7 +324,6 @@ describe('loader', () => { }, }, }, - isFirstExistenceFetch: false, }; it('should create a reference for each layer and for current index pattern', () => { @@ -815,95 +351,111 @@ describe('loader', () => { }); describe('changeIndexPattern', () => { - it('loads the index pattern and then sets it as current', async () => { - const setState = jest.fn(); + it('sets the given indexpattern as current', () => { const state: IndexPatternPrivateState = { currentIndexPatternId: '2', - indexPatternRefs: [], - indexPatterns: {}, - existingFields: {}, layers: {}, - isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: '2' }); - await changeIndexPattern({ + const newState = changeIndexPattern({ + indexPatternId: '1', state, - setState, - id: '1', - indexPatternsService: mockIndexPatternsService(), - onError: jest.fn(), storage, + indexPatterns: { + '1': sampleIndexPatterns['1'], + }, }); - expect(setState).toHaveBeenCalledTimes(1); - const [fn, options] = setState.mock.calls[0]; - expect(options).toEqual({ applyImmediately: true }); - expect(fn(state)).toMatchObject({ + expect(newState).toMatchObject({ currentIndexPatternId: '1', - indexPatterns: { - '1': { - ...sampleIndexPatterns['1'], - fields: [...sampleIndexPatterns['1'].fields], - }, - }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { indexPatternId: '1', }); }); - it('handles errors', async () => { - const setState = jest.fn(); - const onError = jest.fn(); - const err = new Error('NOPE!'); + it('should update an empty layer on indexpattern change', () => { const state: IndexPatternPrivateState = { currentIndexPatternId: '2', - indexPatternRefs: [], - existingFields: {}, - indexPatterns: {}, - layers: {}, - isFirstExistenceFetch: false, + layers: { layerId: { columnOrder: [], columns: {}, indexPatternId: '2' } }, }; - const storage = createMockStorage({ indexPatternId: '2' }); - await changeIndexPattern({ + const newState = changeIndexPattern({ + indexPatternId: '1', state, - setState, - id: '1', - indexPatternsService: { - get: jest.fn(async () => { - throw err; - }), - getIdsWithTitle: jest.fn(), - }, - onError, storage, + indexPatterns: sampleIndexPatterns, }); - expect(setState).not.toHaveBeenCalled(); - expect(storage.set).not.toHaveBeenCalled(); - expect(onError).toHaveBeenCalledWith(Error('Missing indexpatterns')); + expect(newState.layers.layerId).toEqual({ + columnOrder: [], + columns: {}, + indexPatternId: '1', + }); }); - }); - describe('changeLayerIndexPattern', () => { - let uiActions: UiActionsStart; + it('should keep layer indexpattern on change if not empty', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: '2', + layers: { + layerId: { + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'string', + isBucketed: true, + label: '', + operationType: 'terms', + sourceField: 'bytes', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 3, + }, + } as TermsIndexPatternColumn, + }, + indexPatternId: '2', + }, + }, + }; + const storage = createMockStorage({ indexPatternId: '2' }); - beforeEach(() => { - uiActions = uiActionsPluginMock.createStartContract(); + const newState = changeIndexPattern({ + indexPatternId: '1', + state, + storage, + indexPatterns: sampleIndexPatterns, + }); + + expect(newState.layers).toEqual({ + layerId: { + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'string', + isBucketed: true, + label: '', + operationType: 'terms', + sourceField: 'bytes', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 3, + }, + }, + }, + indexPatternId: '2', + }, + }); }); + }); + describe('changeLayerIndexPattern', () => { it('loads the index pattern and then changes the specified layer', async () => { - const setState = jest.fn(); const state: IndexPatternPrivateState = { - currentIndexPatternId: '2', - indexPatternRefs: [], - existingFields: {}, - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, + currentIndexPatternId: '1', layers: { l0: { columnOrder: ['col1'], @@ -927,29 +479,20 @@ describe('loader', () => { indexPatternId: '1', }, }, - isFirstExistenceFetch: false, }; const storage = createMockStorage({ indexPatternId: '1' }); - await changeLayerIndexPattern({ + const newState = changeLayerIndexPattern({ state, - setState, indexPatternId: '2', layerId: 'l1', - indexPatternsService: mockIndexPatternsService(), - onError: jest.fn(), storage, - uiActions, + indexPatterns: sampleIndexPatterns, }); - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0](state)).toMatchObject({ + expect(newState).toMatchObject({ currentIndexPatternId: '2', - indexPatterns: { - 1: sampleIndexPatterns['1'], - 2: sampleIndexPatterns['2'], - }, layers: { l0: { columnOrder: ['col1'], @@ -978,239 +521,5 @@ describe('loader', () => { indexPatternId: '2', }); }); - - it('handles errors', async () => { - const setState = jest.fn(); - const onError = jest.fn(); - const err = new Error('NOPE!'); - const state: IndexPatternPrivateState = { - currentIndexPatternId: '2', - indexPatternRefs: [], - existingFields: {}, - indexPatterns: { - '1': sampleIndexPatterns['1'], - }, - layers: { - l0: { - columnOrder: ['col1'], - columns: {}, - indexPatternId: '1', - }, - }, - isFirstExistenceFetch: false, - }; - - const storage = createMockStorage({ indexPatternId: '2' }); - - await changeLayerIndexPattern({ - state, - setState, - indexPatternId: '2', - layerId: 'l0', - indexPatternsService: { - get: jest.fn(async () => { - throw err; - }), - getIdsWithTitle: jest.fn(), - }, - onError, - storage, - uiActions, - }); - - expect(setState).not.toHaveBeenCalled(); - expect(storage.set).not.toHaveBeenCalled(); - expect(onError).toHaveBeenCalledWith(Error('Missing indexpatterns')); - }); - }); - - describe('syncExistingFields', () => { - const dslQuery = { - bool: { - must: [], - filter: [{ match_all: {} }], - should: [], - must_not: [], - }, - }; - - it('should call once for each index pattern', async () => { - const setState = jest.fn(); - const fetchJson = jest.fn((path: string) => { - const indexPatternTitle = last(path.split('/')); - return { - indexPatternTitle, - existingFieldNames: ['field_1', 'field_2'].map( - (fieldName) => `ip${indexPatternTitle}_${fieldName}` - ), - }; - }) as unknown as HttpHandler; - - await syncExistingFields({ - dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, - indexPatterns: [ - { id: '1', title: '1', fields: [], hasRestrictions: false }, - { id: '2', title: '1', fields: [], hasRestrictions: false }, - { id: '3', title: '1', fields: [], hasRestrictions: false }, - ], - setState, - dslQuery, - showNoDataPopover: jest.fn(), - currentIndexPatternTitle: 'abc', - isFirstExistenceFetch: false, - }); - - expect(fetchJson).toHaveBeenCalledTimes(3); - expect(setState).toHaveBeenCalledTimes(1); - - const [fn, options] = setState.mock.calls[0]; - expect(options).toEqual({ applyImmediately: true }); - const newState = fn({ - foo: 'bar', - existingFields: {}, - }); - - expect(newState).toEqual({ - foo: 'bar', - isFirstExistenceFetch: false, - existenceFetchFailed: false, - existenceFetchTimeout: false, - existingFields: { - '1': { ip1_field_1: true, ip1_field_2: true }, - '2': { ip2_field_1: true, ip2_field_2: true }, - '3': { ip3_field_1: true, ip3_field_2: true }, - }, - }); - }); - - it('should call showNoDataPopover callback if current index pattern returns no fields', async () => { - const setState = jest.fn(); - const showNoDataPopover = jest.fn(); - const fetchJson = jest.fn((path: string) => { - const indexPatternTitle = last(path.split('/')); - return { - indexPatternTitle, - existingFieldNames: - indexPatternTitle === '1' - ? ['field_1', 'field_2'].map((fieldName) => `${indexPatternTitle}_${fieldName}`) - : [], - }; - }) as unknown as HttpHandler; - - const args = { - dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, - indexPatterns: [ - { id: '1', title: '1', fields: [], hasRestrictions: false }, - { id: '2', title: '1', fields: [], hasRestrictions: false }, - { id: 'c', title: '1', fields: [], hasRestrictions: false }, - ], - setState, - dslQuery, - showNoDataPopover: jest.fn(), - currentIndexPatternTitle: 'abc', - isFirstExistenceFetch: false, - }; - - await syncExistingFields(args); - - expect(showNoDataPopover).not.toHaveBeenCalled(); - - await syncExistingFields({ ...args, isFirstExistenceFetch: true }); - expect(showNoDataPopover).not.toHaveBeenCalled(); - }); - - it('should set all fields to available and existence error flag if the request fails', async () => { - const setState = jest.fn(); - const fetchJson = jest.fn((path: string) => { - return new Promise((resolve, reject) => { - reject(new Error()); - }); - }) as unknown as HttpHandler; - - const args = { - dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, - indexPatterns: [ - { - id: '1', - title: '1', - hasRestrictions: false, - fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], - }, - ], - setState, - dslQuery, - showNoDataPopover: jest.fn(), - currentIndexPatternTitle: 'abc', - isFirstExistenceFetch: false, - }; - - await syncExistingFields(args); - - const [fn, options] = setState.mock.calls[0]; - expect(options).toEqual({ applyImmediately: true }); - const newState = fn({ - foo: 'bar', - existingFields: {}, - }) as IndexPatternPrivateState; - - expect(newState.existenceFetchFailed).toEqual(true); - expect(newState.existenceFetchTimeout).toEqual(false); - expect(newState.existingFields['1']).toEqual({ - field1: true, - field2: true, - }); - }); - - it('should set all fields to available and existence error flag if the request times out', async () => { - const setState = jest.fn(); - const fetchJson = jest.fn((path: string) => { - return new Promise((resolve, reject) => { - const error = createHttpFetchError( - 'timeout', - 'error', - {} as Request, - { status: 408 } as Response - ); - reject(error); - }); - }) as unknown as HttpHandler; - - const args = { - dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, - indexPatterns: [ - { - id: '1', - title: '1', - hasRestrictions: false, - fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], - }, - ], - setState, - dslQuery, - showNoDataPopover: jest.fn(), - currentIndexPatternTitle: 'abc', - isFirstExistenceFetch: false, - }; - - await syncExistingFields(args); - - const [fn, options] = setState.mock.calls[0]; - expect(options).toEqual({ applyImmediately: true }); - const newState = fn({ - foo: 'bar', - existingFields: {}, - }) as IndexPatternPrivateState; - - expect(newState.existenceFetchFailed).toEqual(false); - expect(newState.existenceFetchTimeout).toEqual(true); - expect(newState.existingFields['1']).toEqual({ - field1: true, - field2: true, - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 204e5761404112..f7a6912390027c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -77,7 +77,7 @@ export function injectReferences( }; } -export function createStateFromPersisted({ +function createStateFromPersisted({ persistedState, references, }: { @@ -87,7 +87,7 @@ export function createStateFromPersisted({ return persistedState && references ? injectReferences(persistedState, references) : undefined; } -export function getUsedIndexPatterns({ +function getUsedIndexPatterns({ state, indexPatternRefs, storage, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts index fec643772cc767..0582d5fcf7693f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts @@ -10,7 +10,7 @@ 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 '../../../../data_views_service/loader'; +import { convertDataViewIntoLensIndexPattern } from '../../../../indexpattern_service/loader'; jest.mock('./parse', () => ({ insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts index 4085ec931d3a6d..f5080de84d67d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts @@ -6,7 +6,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; -import { convertDataViewIntoLensIndexPattern } from '../../../../data_views_service/loader'; +import { convertDataViewIntoLensIndexPattern } from '../../../../indexpattern_service/loader'; import type { IndexPattern } from '../../../../types'; import type { PersistedIndexPatternLayer } from '../../../types'; diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts new file mode 100644 index 00000000000000..e43b79c82cfbf5 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts @@ -0,0 +1,445 @@ +/* + * 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 { HttpHandler } from '@kbn/core/public'; +import { last } from 'lodash'; +import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { createHttpFetchError } from '@kbn/core-http-browser-mocks'; +import { IndexPattern, IndexPatternField } from '../types'; +import { + ensureIndexPattern, + loadIndexPatternRefs, + loadIndexPatterns, + syncExistingFields, +} from './loader'; +import { sampleIndexPatterns, mockDataViewsService } from './mocks'; +import { documentField } from '../indexpattern_datasource/document_field'; + +describe('loader', () => { + describe('loadIndexPatternRefs', () => { + it('should return a list of sorted indexpattern refs', async () => { + const refs = await loadIndexPatternRefs(mockDataViewsService()); + expect(refs[0].title < refs[1].title).toBeTruthy(); + }); + }); + + describe('loadIndexPatterns', () => { + it('should not load index patterns that are already loaded', async () => { + const dataViewsService = mockDataViewsService(); + dataViewsService.get = jest.fn(() => + Promise.reject('mockIndexPatternService.get should not have been called') + ); + + const cache = await loadIndexPatterns({ + cache: sampleIndexPatterns, + patterns: ['1', '2'], + dataViews: dataViewsService, + }); + + expect(cache).toEqual(sampleIndexPatterns); + }); + + it('should load index patterns that are not loaded', async () => { + const cache = await loadIndexPatterns({ + cache: { + '2': sampleIndexPatterns['2'], + }, + patterns: ['1', '2'], + dataViews: mockDataViewsService(), + }); + + expect(cache).toMatchObject(sampleIndexPatterns); + }); + + it('should allow scripted, but not full text fields', async () => { + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['1', '2'], + dataViews: mockDataViewsService(), + }); + + expect(cache).toMatchObject(sampleIndexPatterns); + }); + + it('should apply field restrictions from typeMeta', async () => { + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['foo'], + dataViews: { + get: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + metaFields: [], + typeMeta: { + aggs: { + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: 'm', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, + }, + }, + }, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + })), + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown as Pick, + }); + + expect(cache.foo.getFieldByName('bytes')!.aggregationRestrictions).toEqual({ + sum: { agg: 'sum' }, + }); + expect(cache.foo.getFieldByName('timestamp')!.aggregationRestrictions).toEqual({ + date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, + }); + }); + + it('should map meta flag', async () => { + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['foo'], + dataViews: { + get: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + metaFields: ['timestamp'], + typeMeta: { + aggs: { + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: 'm', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, + }, + }, + }, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + })), + getIdsWithTitle: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + })), + } as unknown as Pick, + }); + + expect(cache.foo.getFieldByName('timestamp')!.meta).toEqual(true); + }); + + it('should call the refresh callback when loading new indexpatterns', async () => { + const onIndexPatternRefresh = jest.fn(); + await loadIndexPatterns({ + cache: { + '2': sampleIndexPatterns['2'], + }, + patterns: ['1', '2'], + dataViews: mockDataViewsService(), + onIndexPatternRefresh, + }); + + expect(onIndexPatternRefresh).toHaveBeenCalled(); + }); + + it('should not call the refresh callback when using the cache', async () => { + const onIndexPatternRefresh = jest.fn(); + await loadIndexPatterns({ + cache: sampleIndexPatterns, + patterns: ['1', '2'], + dataViews: mockDataViewsService(), + onIndexPatternRefresh, + }); + + expect(onIndexPatternRefresh).not.toHaveBeenCalled(); + }); + + it('should load one of the not used indexpatterns if all used ones are not available', async () => { + const dataViewsService = { + get: jest.fn(async (id: string) => { + if (id === '3') { + return { + id: '3', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: [], + }; + } + return Promise.reject(); + }), + getIdsWithTitle: jest.fn(), + } as unknown as Pick; + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['1', '2'], + notUsedPatterns: ['3'], + dataViews: dataViewsService, + }); + + expect(cache).toEqual({ + 3: expect.objectContaining({ + id: '3', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: [documentField], + }), + }); + }); + }); + + describe('ensureIndexPattern', () => { + it('should throw if the requested indexPattern cannot be loaded', async () => { + const err = Error('NOPE!'); + const onError = jest.fn(); + const cache = await ensureIndexPattern({ + id: '3', + dataViews: { + get: jest.fn(async () => { + throw err; + }), + getIdsWithTitle: jest.fn(), + } as unknown as Pick, + onError, + }); + + expect(cache).toBeUndefined(); + expect(onError).toHaveBeenCalledWith(Error('Missing indexpatterns')); + }); + + it('should ensure the requested indexpattern is loaded into the cache', async () => { + const onError = jest.fn(); + const cache = await ensureIndexPattern({ + id: '2', + dataViews: mockDataViewsService(), + onError, + }); + expect(cache).toMatchObject({ 2: sampleIndexPatterns['2'] }); + expect(onError).not.toHaveBeenCalled(); + }); + }); + + describe('syncExistingFields', () => { + const dslQuery = { + bool: { + must: [], + filter: [{ match_all: {} }], + should: [], + must_not: [], + }, + }; + + function getIndexPatternList() { + return [ + { id: '1', title: '1', fields: [], hasRestrictions: false }, + { id: '2', title: '1', fields: [], hasRestrictions: false }, + { id: '3', title: '1', fields: [], hasRestrictions: false }, + ] as unknown as IndexPattern[]; + } + + it('should call once for each index pattern', async () => { + const updateIndexPatterns = jest.fn(); + const fetchJson = jest.fn((path: string) => { + const indexPatternTitle = last(path.split('/')); + return { + indexPatternTitle, + existingFieldNames: ['field_1', 'field_2'].map( + (fieldName) => `ip${indexPatternTitle}_${fieldName}` + ), + }; + }) as unknown as HttpHandler; + + await syncExistingFields({ + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatternList: getIndexPatternList(), + updateIndexPatterns, + dslQuery, + onNoData: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + existingFields: {}, + }); + + expect(fetchJson).toHaveBeenCalledTimes(3); + expect(updateIndexPatterns).toHaveBeenCalledTimes(1); + + const [newState, options] = updateIndexPatterns.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); + + expect(newState).toEqual({ + isFirstExistenceFetch: false, + existingFields: { + '1': { ip1_field_1: true, ip1_field_2: true }, + '2': { ip2_field_1: true, ip2_field_2: true }, + '3': { ip3_field_1: true, ip3_field_2: true }, + }, + }); + }); + + it('should call onNoData callback if current index pattern returns no fields', async () => { + const updateIndexPatterns = jest.fn(); + const onNoData = jest.fn(); + const fetchJson = jest.fn((path: string) => { + const indexPatternTitle = last(path.split('/')); + return { + indexPatternTitle, + existingFieldNames: + indexPatternTitle === '1' + ? ['field_1', 'field_2'].map((fieldName) => `${indexPatternTitle}_${fieldName}`) + : [], + }; + }) as unknown as HttpHandler; + + const args = { + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatternList: getIndexPatternList(), + updateIndexPatterns, + dslQuery, + onNoData, + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + existingFields: {}, + }; + + await syncExistingFields(args); + + expect(onNoData).not.toHaveBeenCalled(); + + await syncExistingFields({ ...args, isFirstExistenceFetch: true }); + expect(onNoData).not.toHaveBeenCalled(); + }); + + it('should set all fields to available and existence error flag if the request fails', async () => { + const updateIndexPatterns = jest.fn(); + const fetchJson = jest.fn((path: string) => { + return new Promise((resolve, reject) => { + reject(new Error()); + }); + }) as unknown as HttpHandler; + + const args = { + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatternList: [ + { + id: '1', + title: '1', + hasRestrictions: false, + fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], + }, + ] as IndexPattern[], + updateIndexPatterns, + dslQuery, + onNoData: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + existingFields: {}, + }; + + await syncExistingFields(args); + + const [newState, options] = updateIndexPatterns.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); + + expect(newState.existenceFetchFailed).toEqual(true); + expect(newState.existenceFetchTimeout).toEqual(false); + expect(newState.existingFields['1']).toEqual({ + field1: true, + field2: true, + }); + }); + + it('should set all fields to available and existence error flag if the request times out', async () => { + const updateIndexPatterns = jest.fn(); + const fetchJson = jest.fn((path: string) => { + return new Promise((resolve, reject) => { + const error = createHttpFetchError( + 'timeout', + 'error', + {} as Request, + { status: 408 } as Response + ); + reject(error); + }); + }) as unknown as HttpHandler; + + const args = { + dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, + fetchJson, + indexPatternList: [ + { + id: '1', + title: '1', + hasRestrictions: false, + fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[], + }, + ] as IndexPattern[], + updateIndexPatterns, + dslQuery, + onNoData: jest.fn(), + currentIndexPatternTitle: 'abc', + isFirstExistenceFetch: false, + existingFields: {}, + }; + + await syncExistingFields(args); + + const [newState, options] = updateIndexPatterns.mock.calls[0]; + expect(options).toEqual({ applyImmediately: true }); + + expect(newState.existenceFetchFailed).toEqual(false); + expect(newState.existenceFetchTimeout).toEqual(true); + expect(newState.existingFields['1']).toEqual({ + field1: true, + field2: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.ts similarity index 94% rename from x-pack/plugins/lens/public/data_views_service/loader.ts rename to x-pack/plugins/lens/public/indexpattern_service/loader.ts index ac1a894ed5809f..a45c6ecf5cda7f 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/loader.ts @@ -15,6 +15,7 @@ import { BASE_API_URL, DateRange, ExistingFields } from '../../common'; import { DataViewsState } from '../state_management'; type ErrorHandler = (err: Error) => void; +type MinimalDataViewsContract = Pick; /** * All these functions will be used by the Embeddable instance too, @@ -84,6 +85,7 @@ export function convertDataViewIntoLensIndexPattern( Object.fromEntries( Object.entries(fieldFormatMap).map(([id, format]) => [ id, + // @ts-ignore 'toJSON' in format ? format.toJSON() : format, ]) ), @@ -94,7 +96,7 @@ export function convertDataViewIntoLensIndexPattern( } export async function loadIndexPatternRefs( - indexPatternsService: DataViewsContract + indexPatternsService: MinimalDataViewsContract ): Promise { const indexPatterns = await indexPatternsService.getIdsWithTitle(); @@ -122,7 +124,7 @@ export async function loadIndexPatterns({ cache, onIndexPatternRefresh, }: { - dataViews: DataViewsContract; + dataViews: MinimalDataViewsContract; patterns: string[]; notUsedPatterns?: string[]; cache: Record; @@ -167,7 +169,7 @@ export async function loadIndexPatterns({ return indexPatternsObject; } -export async function loadIndexPattern({ +export async function ensureIndexPattern({ id, onError, dataViews, @@ -175,7 +177,7 @@ export async function loadIndexPattern({ }: { id: string; onError: ErrorHandler; - dataViews: DataViewsContract; + dataViews: MinimalDataViewsContract; cache?: IndexPatternMap; }) { const indexPatterns = await loadIndexPatterns({ @@ -288,13 +290,13 @@ export async function syncExistingFields({ updateIndexPatterns( { - isFirstExistenceFetch: status !== 200, existingFields: newExistingFields, ...(result - ? {} + ? { isFirstExistenceFetch: status !== 200 } : { - existenceFetchFailed: status !== 418, - existenceFetchTimeout: status === 418, + isFirstExistenceFetch, + existenceFetchFailed: status !== 408, + existenceFetchTimeout: status === 408, }), }, { applyImmediately: true } diff --git a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts new file mode 100644 index 00000000000000..4429d735057b33 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts @@ -0,0 +1,217 @@ +/* + * 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 { DataViewsContract } from '@kbn/data-views-plugin/common'; +import { documentField } from '../indexpattern_datasource/document_field'; +import { + createMockedIndexPattern, + createMockedRestrictedIndexPattern, +} from '../indexpattern_datasource/mocks'; +import { IndexPattern } from '../types'; + +export function loadInitialDataViews() { + const indexPattern = createMockedIndexPattern(); + const restricted = createMockedRestrictedIndexPattern(); + return { + indexPatternRefs: [], + existingFields: {}, + indexPatterns: { + [indexPattern.id]: indexPattern, + [restricted.id]: restricted, + }, + isFirstExistenceFetch: false, + }; +} + +export const createMockStorage = (lastData?: Record) => { + return { + get: jest.fn().mockImplementation(() => lastData), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }; +}; + +const indexPattern1 = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'start_date', + displayName: 'start_date', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + { + name: 'unsupported', + displayName: 'unsupported', + type: 'geo', + aggregatable: true, + searchable: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + { + name: 'geo.src', + displayName: 'geo.src', + type: 'string', + aggregatable: true, + searchable: true, + esTypes: ['keyword'], + }, + { + name: 'scripted', + displayName: 'Scripted', + type: 'string', + searchable: true, + aggregatable: true, + scripted: true, + lang: 'painless', + script: '1234', + }, + documentField, + ], +} as unknown as IndexPattern; + +const sampleIndexPatternsFromService = { + '1': createMockedIndexPattern(), + '2': createMockedRestrictedIndexPattern(), +}; + +const indexPattern2 = { + id: '2', + title: 'my-fake-restricted-pattern', + timeFieldName: 'timestamp', + hasRestrictions: true, + fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + // Ignored in the UI + histogram: { + agg: 'histogram', + interval: 1000, + }, + average: { + agg: 'avg', + }, + max: { + agg: 'max', + }, + min: { + agg: 'min', + }, + sum: { + agg: 'sum', + }, + }, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + scripted: true, + lang: 'painless', + script: '1234', + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }, + documentField, + ], +} as unknown as IndexPattern; + +export const sampleIndexPatterns = { + '1': indexPattern1, + '2': indexPattern2, +}; + +export function mockDataViewsService() { + return { + get: jest.fn(async (id: '1' | '2') => { + const result = { ...sampleIndexPatternsFromService[id], metaFields: [] }; + if (!result.fields) { + result.fields = []; + } + return result; + }), + getIdsWithTitle: jest.fn(async () => { + return [ + { + id: sampleIndexPatterns[1].id, + title: sampleIndexPatterns[1].title, + }, + { + id: sampleIndexPatterns[2].id, + title: sampleIndexPatterns[2].title, + }, + ]; + }), + } as unknown as Pick; +} diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/indexpattern_service/service.ts similarity index 96% rename from x-pack/plugins/lens/public/data_views_service/service.ts rename to x-pack/plugins/lens/public/indexpattern_service/service.ts index 047013525266f1..1a27be7e00fb85 100644 --- a/x-pack/plugins/lens/public/data_views_service/service.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/service.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import type { DateRange } from '../../common'; import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; import { - loadIndexPattern, + ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns, syncExistingFields, @@ -103,7 +103,8 @@ export function createIndexPatternService({ ...args, }); }, - ensureIndexPattern: (args) => loadIndexPattern({ onError: onChangeError, dataViews, ...args }), + ensureIndexPattern: (args) => + ensureIndexPattern({ onError: onChangeError, dataViews, ...args }), refreshExistingFields: (args) => syncExistingFields({ updateIndexPatterns, diff --git a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts index 4c2e77078dd285..41abd1a0e8f8dc 100644 --- a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IndexPatternServiceAPI } from '../data_views_service/service'; +import type { IndexPatternServiceAPI } from '../indexpattern_service/service'; export function createIndexPatternServiceMock(): IndexPatternServiceAPI { return { diff --git a/x-pack/plugins/lens/public/mocks/store_mocks.tsx b/x-pack/plugins/lens/public/mocks/store_mocks.tsx index 5fab4602b0483b..e63a79693cadb5 100644 --- a/x-pack/plugins/lens/public/mocks/store_mocks.tsx +++ b/x-pack/plugins/lens/public/mocks/store_mocks.tsx @@ -84,7 +84,7 @@ export function makeLensStore({ resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), ...preloadedState, }, - } as PreloadedState); + } as unknown as PreloadedState); const origDispatch = store.dispatch; store.dispatch = jest.fn(dispatch || origDispatch); diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 7bee1b25e2ae68..9b32f91e7b1393 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -15,13 +15,13 @@ import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import type { VisualizeEditorContext, Suggestion, DatasourceMap } from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; -import { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; -import { Datasource, Visualization } from '../types'; +import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; +import type { Datasource, Visualization } from '../types'; import { generateId } from '../id_generator'; import type { LayerType } from '../../common/types'; import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer'; import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers'; -import { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types'; +import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types'; export const initialState: LensAppState = { persistedDoc: undefined, @@ -80,7 +80,7 @@ export const getPreloadedState = ({ activeDatasourceId: initialDatasourceId, datasourceStates, visualization: { - state: null as unknown, + state: null, activeId: Object.keys(visualizationMap)[0] || null, }, }; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 163174abe61257..d72061e1aba4bf 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -45,7 +45,7 @@ import { } from './datatable_visualization/components/constants'; import type { LensInspector } from './lens_inspector_service'; import { DataViewsState } from './state_management/types'; -import { IndexPatternServiceAPI } from './data_views_service/service'; +import { IndexPatternServiceAPI } from './indexpattern_service/service'; export interface IndexPatternRef { id: string; diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 2c848758b0975d..6ebf1cb7d3f835 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -22,7 +22,7 @@ import type { IndexPatternRef, } from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; -import { IndexPatternServiceAPI } from './data_views_service/service'; +import { IndexPatternServiceAPI } from './indexpattern_service/service'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts index 1e02f929e90ceb..e259e1a675a472 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.test.ts @@ -8,9 +8,15 @@ import { FramePublicAPI } from '../../types'; import { getStaticDate } from './helpers'; -const frame = { +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', () => { From ce7bddd233d76fb137a086789a6a9c6fdaf3d3b4 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 29 Jul 2022 20:00:38 +0200 Subject: [PATCH 18/98] :white_check_mark: Fix more tests --- .../config_panel/config_panel.test.tsx | 17 ++++--- .../editor_frame/data_panel_wrapper.tsx | 12 +++-- .../editor_frame/editor_frame.test.tsx | 14 +++++- .../editor_frame/suggestion_helpers.test.ts | 33 +++++++++----- .../indexpattern_datasource/loader.test.ts | 44 ++++++++----------- .../indexpattern_datasource/utils.test.tsx | 12 ++--- .../indexpattern_service/loader.test.ts | 6 +-- .../public/indexpattern_service/loader.ts | 4 +- .../lens/public/indexpattern_service/mocks.ts | 4 ++ 9 files changed, 92 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index ef461b7d9d3027..5c1b5c4d9d8a64 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -356,11 +356,16 @@ describe('ConfigPanel', () => { await clickToAddLayer(instance); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); - expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith({}, 'newId', { - columnId: 'myColumn', - groupId: 'testGroup', - staticValue: 100, - }); + expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith( + {}, + 'newId', + frame.dataViews.indexPatterns, + { + columnId: 'myColumn', + groupId: 'testGroup', + staticValue: 100, + } + ); }); it('should add an initial dimension value when clicking on the empty dimension button', async () => { @@ -390,6 +395,7 @@ describe('ConfigPanel', () => { expect(datasourceMap.testDatasource.initializeDimension).toHaveBeenCalledWith( 'state', 'first', + frame.dataViews.indexPatterns, { groupId: 'a', columnId: 'newId', @@ -447,6 +453,7 @@ describe('ConfigPanel', () => { a: expect.anything(), }, dateRange: expect.anything(), + dataViews: expect.anything(), }, groupId: 'a', layerId: 'newId', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 4cabda4cc62dfb..553a61ba2c1c55 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -31,6 +31,7 @@ import { import { initializeSources } from './state_helpers'; import type { IndexPatternServiceAPI } from '../../indexpattern_service/service'; import { changeIndexPattern } from '../../state_management/lens_slice'; +import { getInitialDataViewsObject } from '../../utils'; interface DataPanelWrapperProps { datasourceMap: DatasourceMap; @@ -83,8 +84,8 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { { isFullEditor: true, } - ).then((result) => { - const newDatasourceStates = Object.entries(result).reduce( + ).then(({ states, indexPatterns, indexPatternRefs }) => { + const newDatasourceStates = Object.entries(states).reduce( (state, [datasourceId, datasourceState]) => ({ ...state, [datasourceId]: { @@ -94,7 +95,12 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { }), {} ); - dispatchLens(setState({ datasourceStates: newDatasourceStates })); + dispatchLens( + setState({ + datasourceStates: newDatasourceStates, + dataViews: getInitialDataViewsObject(indexPatterns, indexPatternRefs), + }) + ); }); } }, [ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 9c5b7740cacfff..8d4cbade98e416 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -65,6 +65,17 @@ function generateSuggestion(state = {}): DatasourceSuggestion { }; } +function wrapDataViewsContract() { + const dataViewsContract = dataViewPluginMocks.createStartContract(); + return { + ...dataViewsContract, + getIdsWithTitle: jest.fn(async () => [ + { id: '1', title: 'IndexPatternTitle' }, + { id: '2', title: 'OtherIndexPatternTitle' }, + ]), + }; +} + function getDefaultProps() { const defaultProps = { store: { @@ -82,7 +93,7 @@ function getDefaultProps() { data: mockDataPlugin(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), - dataViews: dataViewPluginMocks.createStartContract(), + dataViews: wrapDataViewsContract(), }, palettes: chartPluginMock.createPaletteRegistry(), lensInspector: getLensInspectorService(inspectorPluginMock.createStartContract()), @@ -418,6 +429,7 @@ describe('editor_frame', () => { expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ state: datasourceState, layerId: 'first', + indexPatterns: {}, }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index b739091ddff3b2..5cc62012a50949 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -148,7 +148,8 @@ describe('suggestion helpers', () => { expect(datasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( datasourceStates.mock.state, droppedField, - expect.any(Function) + expect.any(Function), + dataViews.indexPatterns ); }); @@ -184,12 +185,14 @@ describe('suggestion helpers', () => { expect(multiDatasourceMap.mock.getDatasourceSuggestionsForField).toHaveBeenCalledWith( multiDatasourceStates.mock.state, droppedField, - expect.any(Function) + expect.any(Function), + dataViews.indexPatterns ); expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForField).toHaveBeenCalledWith( multiDatasourceStates.mock2.state, droppedField, - expect.any(Function) + expect.any(Function), + dataViews.indexPatterns ); expect(multiDatasourceMap.mock3.getDatasourceSuggestionsForField).not.toHaveBeenCalled(); }); @@ -218,7 +221,8 @@ describe('suggestion helpers', () => { expect(datasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( datasourceStates.mock.state, '1', - 'test' + 'test', + dataViews.indexPatterns ); }); @@ -258,12 +262,14 @@ describe('suggestion helpers', () => { expect(multiDatasourceMap.mock.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( multiDatasourceStates.mock.state, '1', - 'test' + 'test', + dataViews.indexPatterns ); expect(multiDatasourceMap.mock2.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalledWith( multiDatasourceStates.mock2.state, '1', - 'test' + 'test', + dataViews.indexPatterns ); expect( multiDatasourceMap.mock3.getDatasourceSuggestionsForVisualizeField @@ -338,7 +344,8 @@ describe('suggestion helpers', () => { }); expect(datasourceMap.mock.getDatasourceSuggestionsForVisualizeCharts).toHaveBeenCalledWith( datasourceStates.mock.state, - triggerContext.layers + triggerContext.layers, + dataViews.indexPatterns ); }); @@ -421,12 +428,17 @@ describe('suggestion helpers', () => { }); expect(multiDatasourceMap.mock.getDatasourceSuggestionsForVisualizeCharts).toHaveBeenCalledWith( datasourceStates.mock.state, - triggerContext.layers + triggerContext.layers, + dataViews.indexPatterns ); expect( multiDatasourceMap.mock2.getDatasourceSuggestionsForVisualizeCharts - ).toHaveBeenCalledWith(multiDatasourceStates.mock2.state, triggerContext.layers); + ).toHaveBeenCalledWith( + multiDatasourceStates.mock2.state, + triggerContext.layers, + dataViews.indexPatterns + ); expect( multiDatasourceMap.mock3.getDatasourceSuggestionsForVisualizeCharts ).not.toHaveBeenCalled(); @@ -753,7 +765,8 @@ describe('suggestion helpers', () => { label: 'myfieldLabel', }, }, - expect.any(Function) + expect.any(Function), + dataViews.indexPatterns ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index c31f9167e2b24f..3a85a317eec0f3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -38,9 +38,9 @@ const indexPatternRefs = [ describe('loader', () => { describe('loadInitialState', () => { - it('should load a default state', async () => { + it('should load a default state', () => { const storage = createMockStorage(); - const state = await loadInitialState({ + const state = loadInitialState({ indexPatterns: sampleIndexPatterns, indexPatternRefs, storage, @@ -55,9 +55,9 @@ describe('loader', () => { }); }); - it('should load a default state without loading the indexPatterns when embedded', async () => { + it('should load a default state without loading the indexPatterns when embedded', () => { const storage = createMockStorage(); - const state = await loadInitialState({ + const state = loadInitialState({ storage, indexPatterns: {}, indexPatternRefs: [], @@ -71,9 +71,9 @@ describe('loader', () => { expect(storage.set).not.toHaveBeenCalled(); }); - it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { + it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', () => { const storage = createMockStorage({ indexPatternId: 'c' }); - const state = await loadInitialState({ + const state = loadInitialState({ storage, indexPatternRefs, indexPatterns: sampleIndexPatterns, @@ -88,8 +88,8 @@ describe('loader', () => { }); }); - it('should load lastUsedIndexPatternId if in localStorage', async () => { - const state = await loadInitialState({ + it('should load lastUsedIndexPatternId if in localStorage', () => { + const state = loadInitialState({ storage: createMockStorage({ indexPatternId: '2' }), indexPatternRefs, indexPatterns: sampleIndexPatterns, @@ -101,9 +101,9 @@ describe('loader', () => { }); }); - it('should use the default index pattern id, if provided', async () => { + it('should use the default index pattern id, if provided', () => { const storage = createMockStorage(); - const state = await loadInitialState({ + const state = loadInitialState({ defaultIndexPatternId: '2', storage, indexPatternRefs, @@ -119,9 +119,9 @@ describe('loader', () => { }); }); - it('should use the indexPatternId of the visualize trigger field, if provided', async () => { + it('should use the indexPatternId of the visualize trigger field, if provided', () => { const storage = createMockStorage(); - const state = await loadInitialState({ + const state = loadInitialState({ storage, initialContext: { indexPatternId: '1', @@ -140,9 +140,9 @@ describe('loader', () => { }); }); - it('should use the indexPatternId of the visualize trigger chart context, if provided', async () => { + it('should use the indexPatternId of the visualize trigger chart context, if provided', () => { const storage = createMockStorage(); - const state = await loadInitialState({ + const state = loadInitialState({ storage, initialContext: { layers: [ @@ -185,7 +185,7 @@ describe('loader', () => { }); }); - it('should initialize all the embeddable references without local storage', async () => { + it('should initialize all the embeddable references without local storage', () => { const savedState: IndexPatternPersistedState = { layers: { layerb: { @@ -213,7 +213,7 @@ describe('loader', () => { }, }; const storage = createMockStorage({}); - const state = await loadInitialState({ + const state = loadInitialState({ persistedState: savedState, references: [ { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, @@ -233,7 +233,7 @@ describe('loader', () => { expect(storage.set).not.toHaveBeenCalled(); }); - it('should initialize from saved state', async () => { + it('should initialize from saved state', () => { const savedState: IndexPatternPersistedState = { layers: { layerb: { @@ -261,7 +261,7 @@ describe('loader', () => { }, }; const storage = createMockStorage({ indexPatternId: '1' }); - const state = await loadInitialState({ + const state = loadInitialState({ persistedState: savedState, references: [ { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' }, @@ -277,13 +277,6 @@ describe('loader', () => { expect(state).toMatchObject({ currentIndexPatternId: '2', - indexPatternRefs: [ - { id: '1', title: sampleIndexPatterns['1'].title }, - { id: '2', title: sampleIndexPatterns['2'].title }, - ], - indexPatterns: { - '2': sampleIndexPatterns['2'], - }, layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); @@ -489,6 +482,7 @@ describe('loader', () => { layerId: 'l1', storage, indexPatterns: sampleIndexPatterns, + replaceIfPossible: true, }); expect(newState).toMatchObject({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.test.tsx index 79b87628bd2136..3bfcd51fb39074 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.test.tsx @@ -41,11 +41,6 @@ describe('indexpattern_datasource utils', () => { }, }, }, - indexPatterns: { - one: { - getFieldByName: (x: string) => ({ name: x, displayName: x }), - }, - }, } as unknown as IndexPatternPrivateState; framePublicAPI = { activeData: { @@ -62,6 +57,13 @@ describe('indexpattern_datasource utils', () => { ], }, }, + dataViews: { + indexPatterns: { + one: { + getFieldByName: (x: string) => ({ name: x, displayName: x }), + }, + }, + }, } as unknown as FramePublicAPI; docLinks = { diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts index e43b79c82cfbf5..610236ea12d05a 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts @@ -52,7 +52,7 @@ describe('loader', () => { dataViews: mockDataViewsService(), }); - expect(cache).toMatchObject(sampleIndexPatterns); + expect(cache).toEqual(sampleIndexPatterns); }); it('should allow scripted, but not full text fields', async () => { @@ -62,7 +62,7 @@ describe('loader', () => { dataViews: mockDataViewsService(), }); - expect(cache).toMatchObject(sampleIndexPatterns); + expect(cache).toEqual(sampleIndexPatterns); }); it('should apply field restrictions from typeMeta', async () => { @@ -259,7 +259,7 @@ describe('loader', () => { dataViews: mockDataViewsService(), onError, }); - expect(cache).toMatchObject({ 2: sampleIndexPatterns['2'] }); + expect(cache).toEqual({ 2: expect.anything() }); expect(onError).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.ts index a45c6ecf5cda7f..45dd7eb9db8f32 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/loader.ts @@ -96,9 +96,9 @@ export function convertDataViewIntoLensIndexPattern( } export async function loadIndexPatternRefs( - indexPatternsService: MinimalDataViewsContract + dataViews: MinimalDataViewsContract ): Promise { - const indexPatterns = await indexPatternsService.getIdsWithTitle(); + const indexPatterns = await dataViews.getIdsWithTitle(); return indexPatterns.sort((a, b) => { return a.title.localeCompare(b.title); diff --git a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts index 4429d735057b33..a96236e7819848 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts @@ -12,6 +12,7 @@ import { createMockedRestrictedIndexPattern, } from '../indexpattern_datasource/mocks'; import { IndexPattern } from '../types'; +import { getFieldByNameFactory } from './loader'; export function loadInitialDataViews() { const indexPattern = createMockedIndexPattern(); @@ -113,7 +114,9 @@ const indexPattern1 = { }, documentField, ], + getFieldByName: jest.fn(), } as unknown as IndexPattern; +indexPattern1.getFieldByName = getFieldByNameFactory(indexPattern1.fields); const sampleIndexPatternsFromService = { '1': createMockedIndexPattern(), @@ -186,6 +189,7 @@ const indexPattern2 = { documentField, ], } as unknown as IndexPattern; +indexPattern2.getFieldByName = getFieldByNameFactory(indexPattern2.fields); export const sampleIndexPatterns = { '1': indexPattern1, From 80647befcf7bf4e821e4e8692291df24229e2b02 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 3 Aug 2022 11:59:59 +0200 Subject: [PATCH 19/98] :white_check_mark: Fix with new dataViews structure --- .../indexpattern.test.ts | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 218d68a45de0d7..e91aa1b286bec3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -38,6 +38,7 @@ import { import { createMockedFullReference } from './operations/mocks'; import { cloneDeep } from 'lodash'; import { DatatableColumn } from '@kbn/expressions-plugin/common'; +import { createMockFramePublicAPI } from '../mocks'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -166,18 +167,10 @@ const expectedIndexPatterns = { }, }; -type DataViewBaseState = Omit< - IndexPatternPrivateState, - 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' ->; - const indexPatterns = expectedIndexPatterns; describe('IndexPattern Data Source', () => { - let baseState: Omit< - IndexPatternPrivateState, - 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' - >; + let baseState: IndexPatternPrivateState; let indexPatternDatasource: Datasource; beforeEach(() => { @@ -304,7 +297,7 @@ describe('IndexPattern Data Source', () => { }); it('should create a table when there is a formula without aggs', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -341,7 +334,7 @@ describe('IndexPattern Data Source', () => { }); it('should generate an expression for an aggregated query', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -495,7 +488,7 @@ describe('IndexPattern Data Source', () => { }); it('should put all time fields used in date_histograms to the esaggs timeFields parameter if not ignoring global time range', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -554,7 +547,7 @@ describe('IndexPattern Data Source', () => { }); it('should pass time shift parameter to metric agg functions', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -593,7 +586,7 @@ describe('IndexPattern Data Source', () => { }); it('should wrap filtered metrics in filtered metric aggregation', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -725,7 +718,7 @@ describe('IndexPattern Data Source', () => { }); it('should add time_scale and format function if time scale is set and supported', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -810,7 +803,7 @@ describe('IndexPattern Data Source', () => { }); it('should put column formatters after calculated columns', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -861,7 +854,7 @@ describe('IndexPattern Data Source', () => { }); it('should rename the output from esaggs when using flat query', () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -916,7 +909,7 @@ describe('IndexPattern Data Source', () => { }); it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -955,7 +948,7 @@ describe('IndexPattern Data Source', () => { }); it('should call optimizeEsAggs once per operation for which it is available', () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -1013,7 +1006,7 @@ describe('IndexPattern Data Source', () => { }); it('should update anticipated esAggs column IDs based on the order of the optimized agg expression builders', () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -1102,7 +1095,7 @@ describe('IndexPattern Data Source', () => { }); it('should collect expression references and append them', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -1139,7 +1132,7 @@ describe('IndexPattern Data Source', () => { }); it('should keep correct column mapping keys with reference columns present', async () => { - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -1182,7 +1175,7 @@ describe('IndexPattern Data Source', () => { it('should topologically sort references', () => { // This is a real example of count() + count() - const queryBaseState: DataViewBaseState = { + const queryBaseState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { first: { @@ -1271,9 +1264,6 @@ describe('IndexPattern Data Source', () => { describe('#insertLayer', () => { it('should insert an empty layer into the previous state', () => { const state = { - indexPatternRefs: [], - existingFields: {}, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -1287,7 +1277,6 @@ describe('IndexPattern Data Source', () => { }, }, currentIndexPatternId: '1', - isFirstExistenceFetch: false, }; expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({ ...state, @@ -1306,10 +1295,6 @@ describe('IndexPattern Data Source', () => { describe('#removeLayer', () => { it('should remove a layer', () => { const state = { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - indexPatterns: expectedIndexPatterns, layers: { first: { indexPatternId: '1', @@ -2539,6 +2524,14 @@ describe('IndexPattern Data Source', () => { ], }, }, + dataViews: { + ...createMockFramePublicAPI().dataViews, + indexPatterns: expectedIndexPatterns, + indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({ + id, + title, + })), + }, } as unknown as FramePublicAPI; }); From 63bf020d331aa200000fa31c90fc41dd2dba8d53 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 3 Aug 2022 17:26:49 +0200 Subject: [PATCH 20/98] :white_check_mark: Fix more test mocks --- .../operations/definitions/formula/formula_public_api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts index 0582d5fcf7693f..3286696308d51d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts @@ -16,7 +16,7 @@ jest.mock('./parse', () => ({ insertOrReplaceFormulaColumn: jest.fn().mockReturnValue({}), })); -jest.mock('../../../loader', () => ({ +jest.mock('../../../../indexpattern_service/loader', () => ({ convertDataViewIntoLensIndexPattern: jest.fn((v) => v), })); From 3a7f9fbc6a4a0089c78ca69d9bd597877e221a1d Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 3 Aug 2022 20:05:32 +0200 Subject: [PATCH 21/98] :white_check_mark: More tests fixed --- .../datapanel.test.tsx | 94 ++++++++----------- .../indexpattern_datasource/datapanel.tsx | 10 +- .../dimension_panel/dimension_panel.test.tsx | 11 +-- .../dimension_panel/field_input.test.tsx | 26 ++--- .../definitions/terms/terms.test.tsx | 2 +- 5 files changed, 63 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 0578c3ac7a47a9..a9d68644df689e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -10,7 +10,7 @@ import ReactDOM from 'react-dom'; import { createMockedDragDropContext } from './mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel'; +import { InnerIndexPatternDataPanel, IndexPatternDataPanel } from './datapanel'; import { FieldList } from './field_list'; import { FieldItem } from './field_item'; import { NoFieldsCallout } from './no_fields_callout'; @@ -30,6 +30,7 @@ import { DOCUMENT_FIELD_NAME } from '../../common'; import { createIndexPatternServiceMock } from '../mocks/data_views_service_mock'; import { createMockFramePublicAPI } from '../mocks'; import { DataViewsState } from '../state_management'; +import { ExistingFieldsMap, FramePublicAPI, IndexPattern } from '../types'; const fieldsOne = [ { @@ -155,6 +156,18 @@ const fieldsThree = [ documentField, ]; +function getExistingFields(indexPatterns: Record) { + const existingFields: ExistingFieldsMap = {}; + for (const { title, fields } of Object.values(indexPatterns)) { + const fieldsMap: Record = {}; + for (const { displayName, name } of fields) { + fieldsMap[displayName ?? name] = true; + } + existingFields[title] = fieldsMap; + } + return existingFields; +} + const initialState: IndexPatternPrivateState = { currentIndexPatternId: '1', layers: { @@ -248,7 +261,7 @@ function getFrameAPIMock({ indexPatterns, existingFields, ...rest }: Partial { dataViews: dataViewPluginMocks.createStartContract(), fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), - onChangeIndexPattern: jest.fn(), onIndexPatternRefresh: jest.fn(), dragDropContext: createMockedDragDropContext(), currentIndexPatternId: '1', @@ -294,31 +306,6 @@ describe('IndexPattern Data Panel', () => { }; }); - it('should call change index pattern callback', async () => { - const setStateSpy = jest.fn(); - const state = { - ...initialState, - layers: { first: { indexPatternId: '1', columnOrder: [], columns: {} } }, - }; - const changeIndexPattern = jest.fn(); - const wrapper = shallowWithIntl( - - ); - - wrapper.find(MemoizedDataPanel).prop('onChangeIndexPattern')!('2'); - - expect(changeIndexPattern).toHaveBeenCalledWith('2', state, setStateSpy); - }); - it('should render a warning if there are no index patterns', () => { const wrapper = shallowWithIntl( { ...createMockedDragDropContext(), dragging: { id: '1', humanData: { label: 'Label' } }, }} - changeIndexPattern={jest.fn()} frame={createMockFramePublicAPI()} /> ); @@ -341,7 +327,6 @@ describe('IndexPattern Data Panel', () => { describe('loading existence data', () => { function testProps() { - const setState = jest.fn(); core.http.post.mockImplementation(async (path) => { const parts = (path as unknown as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; @@ -354,36 +339,39 @@ describe('IndexPattern Data Panel', () => { }); return { ...defaultProps, - changeIndexPattern: jest.fn(), - setState, + setState: jest.fn(), dragDropContext: { ...createMockedDragDropContext(), dragging: { id: '1', humanData: { label: 'Label' } }, }, dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - state: { - indexPatternRefs: [], - existingFields: {}, - isFirstExistenceFetch: false, - currentIndexPatternId: 'a', - indexPatterns: { - a: { - id: 'a', - title: 'aaa', - timeFieldName: 'atime', - fields: [], - getFieldByName: getFieldByNameFactory([]), - hasRestrictions: false, - }, - b: { - id: 'b', - title: 'bbb', - timeFieldName: 'btime', - fields: [], - getFieldByName: getFieldByNameFactory([]), - hasRestrictions: false, + frame: { + dataViews: { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: { + a: { + id: 'a', + title: 'aaa', + timeFieldName: 'atime', + fields: [], + getFieldByName: getFieldByNameFactory([]), + hasRestrictions: false, + }, + b: { + id: 'b', + title: 'bbb', + timeFieldName: 'btime', + fields: [], + getFieldByName: getFieldByNameFactory([]), + hasRestrictions: false, + }, }, }, + } as unknown as FramePublicAPI, + state: { + currentIndexPatternId: 'a', layers: { 1: { indexPatternId: 'a', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 8bac417fb53ae3..5a5b6c5f793970 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -56,7 +56,6 @@ export type Props = Omit< data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; - changeIndexPattern: (id: string) => void; charts: ChartsPluginSetup; core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; @@ -129,7 +128,6 @@ export function IndexPatternDataPanel({ query, filters, dateRange, - changeIndexPattern, charts, indexPatternFieldEditor, showNoDataPopover, @@ -221,7 +219,6 @@ export function IndexPatternDataPanel({ fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={indexPatternFieldEditor} - onChangeIndexPattern={changeIndexPattern} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} uiActions={uiActions} @@ -268,7 +265,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dateRange, filters, dragDropContext, - onChangeIndexPattern, core, data, dataViews, @@ -281,14 +277,16 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ indexPatternService, frame, onIndexPatternRefresh, -}: Omit & { +}: Omit< + DatasourceDataPanelProps, + 'state' | 'setState' | 'showNoDataPopover' | 'core' | 'onChangeIndexPattern' +> & { data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; dragDropContext: DragContextState; - onChangeIndexPattern: (newId: string) => void; charts: ChartsPluginSetup; frame: FramePublicAPI; indexPatternFieldEditor: IndexPatternFieldEditorStart; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index a23f35f4cd53ae..d42da137296659 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -333,13 +333,10 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should hide fields that have no data', () => { const props = { ...defaultProps, - state: { - ...defaultProps.state, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - source: true, - }, + existingFields: { + 'my-fake-index-pattern': { + timestamp: true, + source: true, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx index 8cdbc396cd9ab7..b7ab501f34d169 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx @@ -130,13 +130,13 @@ function getDefaultOperationSupportMatrix( }); } -function getExistingFields(layer: IndexPatternLayer) { +function getExistingFields() { const fields: Record = {}; for (const field of defaultProps.indexPattern.fields) { fields[field.name] = true; } return { - [layer.indexPatternId]: fields, + [defaultProps.indexPattern.title]: fields, }; } @@ -144,7 +144,7 @@ describe('FieldInput', () => { it('should render a field select box', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should render an error message when incomplete operation is on', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { (_, col: ReferenceBasedIndexPatternColumn) => { const updateLayerSpy = jest.fn(); const layer = getLayer(col); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix( layer, 'col1', @@ -234,7 +234,7 @@ describe('FieldInput', () => { (_, col: ReferenceBasedIndexPatternColumn) => { const updateLayerSpy = jest.fn(); const layer = getLayer(col); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix( layer, 'col1', @@ -269,7 +269,7 @@ describe('FieldInput', () => { it('should render an error message for invalid fields', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should render a help message when passed and no errors are found', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should prioritize errors over help messages', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should update the layer on field selection', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should not trigger when the same selected field is selected again', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { it('should prioritize incomplete fields over selected column field to display', () => { const updateLayerSpy = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { const updateLayerSpy = jest.fn(); const onDeleteColumn = jest.fn(); const layer = getLayer(); - const existingFields = getExistingFields(layer); + const existingFields = getExistingFields(); const operationSupportMatrix = getDefaultOperationSupportMatrix(layer, 'col1', existingFields); const instance = mount( { fields[field.name] = true; } return { - [layer.indexPatternId]: fields, + [defaultProps.indexPattern.title]: fields, }; } From 2fc1f0f13e327317dd2f6eb184833eb763fb7347 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 4 Aug 2022 09:55:18 +0200 Subject: [PATCH 22/98] :fire: Removed unused prop --- .../lens/public/indexpattern_datasource/indexpattern.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 803efd672073e8..ee887459bb2c5a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -249,9 +249,6 @@ export function getIndexPatternDatasource({ { - onChangeIndexPattern(indexPattern, DATASOURCE_ID); - }} data={data} dataViews={dataViews} fieldFormats={fieldFormats} From e9abe5a987bd74e14ea0945b7079b21f0bc648a5 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 4 Aug 2022 15:42:23 +0200 Subject: [PATCH 23/98] :white_check_mark: Down to single broken test suite --- x-pack/plugins/lens/public/app_plugin/app.test.tsx | 2 +- .../editor_frame/state_helpers.ts | 4 +--- .../lens/public/indexpattern_service/loader.test.ts | 12 +----------- .../lens/public/indexpattern_service/mocks.ts | 1 - .../lens/public/mocks/data_views_service_mock.ts | 12 ++++++------ x-pack/plugins/lens/public/mocks/datasource_mock.ts | 2 +- x-pack/plugins/lens/public/mocks/services_mock.tsx | 3 ++- .../__snapshots__/load_initial.test.tsx.snap | 6 ++++++ .../state_management/init_middleware/load_initial.ts | 5 ++--- .../public/state_management/load_initial.test.tsx | 10 ++++------ 10 files changed, 24 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 54e1c5eda5d588..a2e247cc427c82 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -477,7 +477,7 @@ describe('Lens App', () => { expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', - indexPatterns: [{ id: 'mockip', isTimeBased: expect.any(Function) }], + indexPatterns: [{ id: 'mockip', isTimeBased: expect.any(Function), fields: [] }], }), {} ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 0a42917bb86aba..91ff19de95f33e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -162,9 +162,7 @@ export async function initializeSources( defaultIndexPatternId, references, }, - { - isFullEditor: true, - } + options ); return { indexPatterns, diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts index 610236ea12d05a..912028fba81a29 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts @@ -52,17 +52,7 @@ describe('loader', () => { dataViews: mockDataViewsService(), }); - expect(cache).toEqual(sampleIndexPatterns); - }); - - it('should allow scripted, but not full text fields', async () => { - const cache = await loadIndexPatterns({ - cache: {}, - patterns: ['1', '2'], - dataViews: mockDataViewsService(), - }); - - expect(cache).toEqual(sampleIndexPatterns); + expect(Object.keys(cache)).toEqual(['1', '2']); }); it('should apply field restrictions from typeMeta', async () => { diff --git a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts index a96236e7819848..6eb7156a91cffd 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts @@ -114,7 +114,6 @@ const indexPattern1 = { }, documentField, ], - getFieldByName: jest.fn(), } as unknown as IndexPattern; indexPattern1.getFieldByName = getFieldByNameFactory(indexPattern1.fields); diff --git a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts index 41abd1a0e8f8dc..798217e9fb6739 100644 --- a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts @@ -9,11 +9,11 @@ import type { IndexPatternServiceAPI } from '../indexpattern_service/service'; export function createIndexPatternServiceMock(): IndexPatternServiceAPI { return { - loadIndexPatterns: jest.fn(), - loadIndexPatternRefs: jest.fn(), - ensureIndexPattern: jest.fn(), - refreshExistingFields: jest.fn(), - getDefaultIndex: jest.fn(), - updateIndexPatternsCache: jest.fn(), + loadIndexPatterns: jest.fn(async () => ({})), + loadIndexPatternRefs: jest.fn(async () => []), + ensureIndexPattern: jest.fn(async () => ({})), + refreshExistingFields: jest.fn(async () => {}), + getDefaultIndex: jest.fn(() => 'fake-index'), + updateIndexPatternsCache: jest.fn(async () => {}), }; } diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index cf8370efdece9b..8111270cc5d124 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -37,7 +37,7 @@ export function createMockDatasource(id: string): DatasourceMock { })), getRenderEventCounters: jest.fn((_state) => []), getPublicAPI: jest.fn().mockReturnValue(publicAPIMock), - initialize: jest.fn((_state?) => Promise.resolve()), + initialize: jest.fn((_state?) => {}), renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), getCurrentIndexPatternId: jest.fn(), diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index 800ec3dee25b1d..e9d0c7cd14f280 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -86,9 +86,10 @@ export function makeDefaultServices( const dataViewsMock = dataViewPluginMocks.createStartContract(); dataViewsMock.get.mockImplementation( jest.fn((id) => - Promise.resolve({ id, isTimeBased: () => true }) + Promise.resolve({ id, isTimeBased: () => true, fields: [] }) ) as unknown as DataViewsPublicPluginStart['get'] ); + dataViewsMock.getIdsWithTitle.mockImplementation(jest.fn(async () => [])); const navigationStartMock = navigationPluginMock.createStartContract(); diff --git a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap index 7f70508dc423ff..a6759521f562e7 100644 --- a/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap +++ b/x-pack/plugins/lens/public/state_management/__snapshots__/load_initial.test.tsx.snap @@ -4,6 +4,12 @@ exports[`Initializing the store should initialize all datasources with state fro Object { "lens": Object { "activeDatasourceId": "testDatasource", + "dataViews": Object { + "existingFields": Object {}, + "indexPatternRefs": Array [], + "indexPatterns": Object {}, + "isFirstExistenceFetch": true, + }, "datasourceStates": Object { "testDatasource": Object { "isLoading": false, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 1e39fa55f4b046..5ee4c82d7ce5fd 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -159,7 +159,7 @@ export function loadInitial( }); } - getPersisted({ initialInput, lensServices, history }) + return getPersisted({ initialInput, lensServices, history }) .then( (persisted) => { if (persisted) { @@ -186,8 +186,7 @@ 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); - - initializeSources( + return initializeSources( { datasourceMap, datasourceStates: docDatasourceStates, diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index 9ae27a9c0073e1..2d8ce9405f12ff 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -120,17 +120,15 @@ describe('Initializing the store', () => { datasource1State, [], undefined, - { - isFullEditor: true, - } + [], + {} ); expect(datasourceMap.testDatasource2.initialize).toHaveBeenCalledWith( datasource2State, [], undefined, - { - isFullEditor: true, - } + [], + {} ); expect(datasourceMap.testDatasource3.initialize).not.toHaveBeenCalled(); expect(store.getState()).toMatchSnapshot(); From 13a669ec4899286633794dcf88b0debca9cea7be Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 4 Aug 2022 19:45:37 +0200 Subject: [PATCH 24/98] :label: Fix type issue --- .../public/indexpattern_datasource/dimension_panel/window.tsx | 3 ++- .../lens/public/indexpattern_datasource/window_utils.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/window.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/window.tsx index c0d561d694274b..00799af410ee0a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/window.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/window.tsx @@ -17,7 +17,8 @@ import { GenericIndexPatternColumn, operationDefinitionMap, } from '../operations'; -import { IndexPattern, IndexPatternLayer } from '../types'; +import type { IndexPatternLayer } from '../types'; +import type { IndexPattern } from '../../types'; import { windowOptions } from '../window_utils'; export function setWindow(columnId: string, layer: IndexPatternLayer, window: string | undefined) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/window_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/window_utils.tsx index 14cc09fcdec8b9..26b1feaa45eaa0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/window_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/window_utils.tsx @@ -6,7 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPattern, IndexPatternLayer } from './types'; +import type { IndexPatternLayer } from './types'; +import type { IndexPattern } from '../types'; export const windowOptions = [ { From 58b873ae85928cfbbdc6e71d90a9178048e04a41 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 10 Aug 2022 11:58:17 +0200 Subject: [PATCH 25/98] :ok_hand: Integrate selector feedback --- .../public/state_management/lens_slice.ts | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 9b32f91e7b1393..7833264a7c56e6 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -11,9 +11,8 @@ import { mapValues } from 'lodash'; import { Query } from '@kbn/es-query'; import { History } from 'history'; import { LensEmbeddableInput } from '..'; -import { getDatasourceLayers } from '../editor_frame_service/editor_frame'; import { TableInspectorAdapter } from '../editor_frame_service/types'; -import type { VisualizeEditorContext, Suggestion, DatasourceMap } from '../types'; +import type { VisualizeEditorContext, Suggestion } from '../types'; import { getInitialDatasourceId, getResolvedDateRange, getRemoveOperation } from '../utils'; import type { DataViewsState, LensAppState, LensStoreDeps, VisualizationState } from './types'; import type { Datasource, Visualization } from '../types'; @@ -22,6 +21,7 @@ import type { LayerType } from '../../common/types'; import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer'; import { getVisualizeFieldSuggestions } from '../editor_frame_service/editor_frame/suggestion_helpers'; import type { FramePublicAPI, LensEditContextMapping, LensEditEvent } from '../types'; +import { selectFramePublicAPI } from './selectors'; export const initialState: LensAppState = { persistedDoc: undefined, @@ -337,7 +337,10 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { } if (datasourceIds?.length) { newState.datasourceStates = { ...state.datasourceStates }; - const frame = createFrameAPI(state, datasourceMap, newState.dataViews); + const frame = selectFramePublicAPI( + { lens: { ...current(state), dataViews: newState.dataViews! } }, + datasourceMap + ); const datasourceLayers = frame.datasourceLayers; for (const datasourceId of datasourceIds) { @@ -742,7 +745,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { layerType ); - const framePublicAPI = createFrameAPI(state, datasourceMap); + const framePublicAPI = selectFramePublicAPI({ lens: current(state) }, datasourceMap); const activeDatasource = datasourceMap[state.activeDatasourceId]; const { noDatasource } = @@ -794,7 +797,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { const { activeDatasourceState, activeVisualizationState } = addInitialValueIfAvailable({ datasourceState: state.datasourceStates[state.activeDatasourceId].state, visualizationState: state.visualization.state, - framePublicAPI: createFrameAPI(state, datasourceMap), + framePublicAPI: selectFramePublicAPI({ lens: current(state) }, datasourceMap), activeVisualization, activeDatasource, layerId, @@ -809,24 +812,6 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { }); }; -function createFrameAPI( - state: LensAppState, - datasourceMap: DatasourceMap, - dataViews: DataViewsState = current(state.dataViews) -) { - return { - // any better idea to avoid `as`? - activeData: state.activeData ? (current(state.activeData) as TableInspectorAdapter) : undefined, - datasourceLayers: getDatasourceLayers( - state.datasourceStates, - datasourceMap, - dataViews.indexPatterns - ), - dateRange: current(state.resolvedDateRange), - dataViews, - }; -} - function addInitialValueIfAvailable({ visualizationState, datasourceState, From 70cab644fa43edbc32d0eada383b47b6a05db400 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 10 Aug 2022 16:14:15 +0200 Subject: [PATCH 26/98] :white_check_mark: Fix remaining unit tests --- .../datapanel.test.tsx | 168 ++++++++++-------- .../public/indexpattern_service/service.ts | 2 +- .../public/mocks/data_views_service_mock.ts | 25 +-- 3 files changed, 112 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index a9d68644df689e..7a3986ec3f5ba8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -10,7 +10,7 @@ import ReactDOM from 'react-dom'; import { createMockedDragDropContext } from './mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { InnerIndexPatternDataPanel, IndexPatternDataPanel } from './datapanel'; +import { InnerIndexPatternDataPanel, IndexPatternDataPanel, Props } from './datapanel'; import { FieldList } from './field_list'; import { FieldItem } from './field_item'; import { NoFieldsCallout } from './no_fields_callout'; @@ -31,6 +31,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'; const fieldsOne = [ { @@ -326,7 +327,7 @@ describe('IndexPattern Data Panel', () => { }); describe('loading existence data', () => { - function testProps() { + function testProps(updateIndexPatterns: IndexPatternServiceProps['updateIndexPatterns']) { core.http.post.mockImplementation(async (path) => { const parts = (path as unknown as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; @@ -339,6 +340,7 @@ describe('IndexPattern Data Panel', () => { }); return { ...defaultProps, + indexPatternService: createIndexPatternServiceMock({ updateIndexPatterns, core }), setState: jest.fn(), dragDropContext: { ...createMockedDragDropContext(), @@ -384,9 +386,9 @@ describe('IndexPattern Data Panel', () => { } async function testExistenceLoading( - stateChanges?: unknown, - propChanges?: unknown, - props = testProps() + props: Props, + stateChanges?: Partial, + propChanges?: Partial ) { const inst = mountWithIntl(); @@ -396,7 +398,7 @@ describe('IndexPattern Data Panel', () => { if (stateChanges || propChanges) { await act(async () => { - (inst.setProps as unknown as (props: unknown) => {})({ + inst.setProps({ ...props, ...((propChanges as object) || {}), state: { @@ -407,63 +409,70 @@ describe('IndexPattern Data Panel', () => { inst.update(); }); } - - return props.setState; } it('loads existence data', async () => { - const setState = await testExistenceLoading(); - - expect(setState).toHaveBeenCalledTimes(1); - - const nextState = setState.mock.calls[0][0]({ - existingFields: {}, - }); + const updateIndexPatterns = jest.fn(); + await testExistenceLoading(testProps(updateIndexPatterns)); - expect(nextState.existingFields).toEqual({ - a_testtitle: { - a_field_1: true, - a_field_2: true, + expect(updateIndexPatterns).toHaveBeenCalledWith( + { + existingFields: { + a_testtitle: { + a_field_1: true, + a_field_2: true, + }, + }, + isFirstExistenceFetch: false, }, - }); + { applyImmediately: true } + ); }); it('loads existence data for current index pattern id', async () => { - const setState = await testExistenceLoading({ currentIndexPatternId: 'b' }); - - expect(setState).toHaveBeenCalledTimes(2); - - const nextState = setState.mock.calls[1][0]({ - existingFields: {}, + const updateIndexPatterns = jest.fn(); + await testExistenceLoading(testProps(updateIndexPatterns), { + currentIndexPatternId: 'b', }); - expect(nextState.existingFields).toEqual({ - a_testtitle: { - a_field_1: true, - a_field_2: true, - }, - b_testtitle: { - b_field_1: true, - b_field_2: true, + expect(updateIndexPatterns).toHaveBeenCalledWith( + { + existingFields: { + a_testtitle: { + a_field_1: true, + a_field_2: true, + }, + b_testtitle: { + b_field_1: true, + b_field_2: true, + }, + }, + isFirstExistenceFetch: false, }, - }); + { applyImmediately: true } + ); }); it('does not load existence data if date and index pattern ids are unchanged', async () => { - const setState = await testExistenceLoading({ - currentIndexPatternId: 'a', - dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' }, - }); + const updateIndexPatterns = jest.fn(); + await testExistenceLoading( + testProps(updateIndexPatterns), + { + currentIndexPatternId: 'a', + }, + { dateRange: { fromDate: '2019-01-01', toDate: '2020-01-01' } } + ); - expect(setState).toHaveBeenCalledTimes(1); + expect(updateIndexPatterns).toHaveBeenCalledTimes(1); }); it('loads existence data if date range changes', async () => { - const setState = await testExistenceLoading(undefined, { + const updateIndexPatterns = jest.fn(); + await testExistenceLoading(testProps(updateIndexPatterns), undefined, { dateRange: { fromDate: '2019-01-01', toDate: '2020-01-02' }, }); - expect(setState).toHaveBeenCalledTimes(2); + expect(updateIndexPatterns).toHaveBeenCalledTimes(2); expect(core.http.post).toHaveBeenCalledTimes(2); expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { @@ -484,28 +493,33 @@ describe('IndexPattern Data Panel', () => { }), }); - const nextState = setState.mock.calls[1][0]({ - existingFields: {}, - }); - - expect(nextState.existingFields).toEqual({ - a_testtitle: { - a_field_1: true, - a_field_2: true, + expect(updateIndexPatterns).toHaveBeenCalledWith( + { + existingFields: { + a_testtitle: { + a_field_1: true, + a_field_2: true, + }, + }, + isFirstExistenceFetch: false, }, - }); + { applyImmediately: true } + ); }); it('loads existence data if layer index pattern changes', async () => { - const setState = await testExistenceLoading({ + const updateIndexPatterns = jest.fn(); + await testExistenceLoading(testProps(updateIndexPatterns), { layers: { 1: { indexPatternId: 'b', + columnOrder: [], + columns: {}, }, }, }); - expect(setState).toHaveBeenCalledTimes(2); + expect(updateIndexPatterns).toHaveBeenCalledTimes(2); expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { body: JSON.stringify({ @@ -525,25 +539,28 @@ describe('IndexPattern Data Panel', () => { }), }); - const nextState = setState.mock.calls[1][0]({ - existingFields: {}, - }); - - expect(nextState.existingFields).toEqual({ - a_testtitle: { - a_field_1: true, - a_field_2: true, - }, - b_testtitle: { - b_field_1: true, - b_field_2: true, + expect(updateIndexPatterns).toHaveBeenCalledWith( + { + existingFields: { + a_testtitle: { + a_field_1: true, + a_field_2: true, + }, + b_testtitle: { + b_field_1: true, + b_field_2: true, + }, + }, + isFirstExistenceFetch: false, }, - }); + { applyImmediately: true } + ); }); it('shows a loading indicator when loading', async () => { + const updateIndexPatterns = jest.fn(); const load = async () => {}; - const inst = mountWithIntl(); + const inst = mountWithIntl(); expect(inst.find(EuiProgress).length).toEqual(1); await act(load); inst.update(); @@ -551,9 +568,10 @@ describe('IndexPattern Data Panel', () => { }); it('does not perform multiple queries at once', async () => { + const updateIndexPatterns = jest.fn(); let queryCount = 0; let overlapCount = 0; - const props = testProps(); + const props = testProps(updateIndexPatterns); core.http.post.mockImplementation((path) => { if (queryCount) { @@ -600,14 +618,15 @@ describe('IndexPattern Data Panel', () => { }); it("should default to empty dsl if query can't be parsed", async () => { + const updateIndexPatterns = jest.fn(); const props = { - ...testProps(), + ...testProps(updateIndexPatterns), query: { language: 'kuery', query: '@timestamp : NOT *', }, }; - await testExistenceLoading(undefined, undefined, props); + await testExistenceLoading(props, undefined, undefined); expect((props.core.http.post as jest.Mock).mock.calls[0][1].body).toContain( JSON.stringify({ @@ -723,12 +742,17 @@ describe('IndexPattern Data Panel', () => { }); it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect( wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner) .length ).toEqual(1); - wrapper.setProps({ existingFields: { idx1: {} } }); + wrapper.setProps({ frame: getFrameAPIMock({ existingFields: { idx1: {} } }) }); expect(wrapper.find(NoFieldsCallout).length).toEqual(2); }); diff --git a/x-pack/plugins/lens/public/indexpattern_service/service.ts b/x-pack/plugins/lens/public/indexpattern_service/service.ts index 1a27be7e00fb85..cbbba5ab14c8ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/service.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/service.ts @@ -18,7 +18,7 @@ import { } from './loader'; import type { DataViewsState } from '../state_management'; -interface IndexPatternServiceProps { +export interface IndexPatternServiceProps { core: Pick; dataViews: DataViewsContract; uiSettings: IUiSettingsClient; diff --git a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts index 798217e9fb6739..56ff7015bdb556 100644 --- a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts @@ -5,15 +5,20 @@ * 2.0. */ -import type { IndexPatternServiceAPI } from '../indexpattern_service/service'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { + createIndexPatternService, + IndexPatternServiceProps, + IndexPatternServiceAPI, +} from '../indexpattern_service/service'; -export function createIndexPatternServiceMock(): IndexPatternServiceAPI { - return { - loadIndexPatterns: jest.fn(async () => ({})), - loadIndexPatternRefs: jest.fn(async () => []), - ensureIndexPattern: jest.fn(async () => ({})), - refreshExistingFields: jest.fn(async () => {}), - getDefaultIndex: jest.fn(() => 'fake-index'), - updateIndexPatternsCache: jest.fn(async () => {}), - }; +export function createIndexPatternServiceMock({ + core = coreMock.createStart(), + uiSettings = uiSettingsServiceMock.createStartContract(), + dataViews = dataViewPluginMocks.createStartContract(), + updateIndexPatterns = jest.fn(), +}: Partial = {}): IndexPatternServiceAPI { + return createIndexPatternService({ core, uiSettings, updateIndexPatterns, dataViews }); } From e68a013e12b03c996ac143f4698420b5e1e530a4 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 10 Aug 2022 16:47:03 +0200 Subject: [PATCH 27/98] :label: fix type issues --- .../public/embeddable/embeddable.test.tsx | 2 +- .../public/state_management/lens_slice.ts | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index a6fc6a79f16e38..8dcd91dca41842 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -195,7 +195,7 @@ describe('embeddable', () => { data: dataMock, expressionRenderer, basePath, - indexPatternService: {} as DataViewsContract, + dataViews: {} as DataViewsContract, capabilities: { canSaveDashboards: true, canSaveVisualizations: true, diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 7833264a7c56e6..b5f9ec00287d51 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -368,23 +368,26 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { const nextTable = new Set( nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) ); - const removed = datasourceLayers[layerId] - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - const nextVisState = (newState.visualization || state.visualization).state; - const activeVisualization = visualizationMap[state.visualization.activeId]; - removed.forEach((columnId) => { - newState.visualization = { - ...state.visualization, - state: activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame, - }), - }; - }); + const datasourcePublicAPI = datasourceLayers[layerId]; + if (datasourcePublicAPI) { + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + const nextVisState = (newState.visualization || state.visualization).state; + const activeVisualization = visualizationMap[state.visualization.activeId]; + removed.forEach((columnId) => { + newState.visualization = { + ...state.visualization, + state: activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + frame, + }), + }; + }); + } } } } From d594e09465376f4e808cf9fd757e938bf258d17c Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 10 Aug 2022 17:01:43 +0200 Subject: [PATCH 28/98] :bug: Fix bug when creating dataview in place --- .../lens/public/app_plugin/lens_top_nav.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 12c8c76306246f..452453934c2df3 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -253,13 +253,21 @@ export const LensTopNavMenu = ({ ); const dispatchChangeIndexPattern = React.useCallback( async (indexPatternId) => { - const newIndexPatterns = await indexPatternService.ensureIndexPattern({ - id: indexPatternId, - cache: dataViews.indexPatterns, - }); + const [newIndexPatternRefs, newIndexPatterns] = await Promise.all([ + // Reload refs in case it's a new indexPattern created on the spot + dataViews.indexPatternRefs[indexPatternId] + ? dataViews.indexPatternRefs + : indexPatternService.loadIndexPatternRefs({ + isFullEditor: true, + }), + indexPatternService.ensureIndexPattern({ + id: indexPatternId, + cache: dataViews.indexPatterns, + }), + ]); dispatch( changeIndexPattern({ - dataViews: { indexPatterns: newIndexPatterns }, + dataViews: { indexPatterns: newIndexPatterns, indexPatternRefs: newIndexPatternRefs }, datasourceIds: Object.keys(datasourceStates), visualizationIds: visualization.activeId ? [visualization.activeId] : [], indexPatternId, @@ -267,6 +275,7 @@ export const LensTopNavMenu = ({ ); }, [ + dataViews.indexPatternRefs, dataViews.indexPatterns, datasourceStates, dispatch, From e5945371358c493aa9e9a314533c1b2b09ddced3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 10 Aug 2022 19:10:44 +0200 Subject: [PATCH 29/98] :sparkles: Update with latest dataview state + fix dataviews picker for annotations --- .../editor_frame/config_panel/layer_panel.tsx | 2 ++ .../lens/public/state_management/lens_slice.ts | 6 +++--- x-pack/plugins/lens/public/types.ts | 7 +++---- .../lens/public/visualizations/xy/visualization.tsx | 11 ++++++++--- .../xy/xy_config_panel/layer_header.tsx | 9 ++------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 86e896c29910c3..443910ce54fa5d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -322,6 +322,7 @@ export function LayerPanel( layerId, visualizationId: activeVisualization.id, }), + defaultIndexPatternId: props.indexPatternService.getDefaultIndex(), }} activeVisualization={activeVisualization} /> @@ -364,6 +365,7 @@ export function LayerPanel( layerId, visualizationId: activeVisualization.id, }), + defaultIndexPatternId: props.indexPatternService.getDefaultIndex(), }} /> )} diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index b5f9ec00287d51..93e2de4ceb97e9 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -321,15 +321,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 ), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 5a3d6989108aea..31826c41371dd1 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -639,13 +639,12 @@ export interface VisualizationConfigProps { export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; - onChangeIndexPattern: (indexPatternId: string, layerId: string) => void; -}; - -export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps & { + onChangeIndexPattern: (indexPatternId: string) => void; defaultIndexPatternId: string; }; +export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps; + export interface VisualizationToolbarProps { setState: (newState: T) => void; frame: FramePublicAPI; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index fa12caf857fdf3..7cbb0274d32c26 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -79,6 +79,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, @@ -98,7 +99,7 @@ export const getXyVisualization = ({ useLegacyTimeAxis: boolean; kibanaTheme: ThemeServiceStart; }): Visualization => ({ - id: 'lnsXY', + id: XY_ID, visualizationTypes, getVisualizationTypeId(state) { const type = getVisualizationType(state); @@ -520,12 +521,16 @@ export const getXyVisualization = ({ }, renderLayerPanel(domElement, props) { + const { onChangeIndexPattern, ...otherProps } = props; render( { + // TODO: should it trigger an action as in the datasource? + onChangeIndexPattern(indexPatternId); + }} /> , diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index b2f72c0acd4c08..14feda9f828c60 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -72,7 +72,7 @@ function AnnotationLayerHeaderContent({ frame, state, layerId, - setState, + onChangeIndexPattern, defaultIndexPatternId, }: VisualizationLayerHeaderContentProps) { const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { @@ -97,12 +97,7 @@ function AnnotationLayerHeaderContent({ indexPatternId={indexPatternId} indexPatternRefs={frame.dataViews.indexPatternRefs} isMissingCurrent={false} - onChangeIndexPattern={(newIndexPatternId) => { - const newLayer = { ...layer, indexPatternId: newIndexPatternId }; - const newLayers = [...state.layers]; - newLayers[layerIndex] = newLayer; - setState({ ...state, layers: newLayers }); - }} + onChangeIndexPattern={onChangeIndexPattern} /> ); } From 5a5d3b9111a44a6fa228e15864187a7372b7ae10 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 11 Aug 2022 12:01:15 +0200 Subject: [PATCH 30/98] :bug: Fix edit + remove field flow --- .../lens/public/app_plugin/lens_top_nav.tsx | 2 +- .../indexpattern_datasource/datapanel.tsx | 18 +++++++++--------- .../public/indexpattern_service/service.ts | 6 +++--- x-pack/plugins/lens/public/utils.ts | 5 ++++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 452453934c2df3..88643b52aab776 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -639,7 +639,7 @@ export const LensTopNavMenu = ({ }, [data.query.filterManager, data.query.queryString, dispatchSetState]); const refreshFieldList = useCallback(async () => { - if (currentIndexPattern && currentIndexPattern.id) { + if (currentIndexPattern?.id) { refreshIndexPatternsList({ activeDatasources: Object.keys(datasourceStates).reduce( (acc, datasourceId) => ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 5a5b6c5f793970..300a96fb846c5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -178,6 +178,8 @@ export function IndexPatternDataPanel({ dateRange.fromDate, dateRange.toDate, indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','), + // important here to rerun the fields existence on indexPattern change (i.e. add new fields in place) + frame.dataViews.indexPatterns, ]} /> @@ -495,9 +497,11 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ cache: {}, onIndexPatternRefresh, }); - indexPatternService.updateIndexPatternsCache({ - ...frame.dataViews.indexPatterns, - [currentIndexPattern.id]: newlyMappedIndexPattern[currentIndexPattern.id], + indexPatternService.updateDataViewsState({ + indexPatterns: { + ...frame.dataViews.indexPatterns, + [currentIndexPattern.id]: newlyMappedIndexPattern[currentIndexPattern.id], + }, }); // start a new session so all charts are refreshed data.search.session.start(); @@ -519,9 +523,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dataView: indexPatternInstance, }, fieldName, - onSave: async () => { - await refreshFieldList(); - }, + onSave: () => refreshFieldList(), }); } : undefined, @@ -538,9 +540,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ dataView: indexPatternInstance, }, fieldName, - onDelete: async () => { - await refreshFieldList(); - }, + onDelete: () => refreshFieldList(), }); } : undefined, diff --git a/x-pack/plugins/lens/public/indexpattern_service/service.ts b/x-pack/plugins/lens/public/indexpattern_service/service.ts index cbbba5ab14c8ab..bbce7f83cf25ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/service.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/service.ts @@ -75,9 +75,9 @@ export interface IndexPatternServiceAPI { getDefaultIndex: () => string; /** - * Update the Lens state cache of indexPatterns + * Update the Lens dataViews state */ - updateIndexPatternsCache: ( + updateDataViewsState: ( newState: Partial, options?: { applyImmediately: boolean } ) => void; @@ -96,7 +96,7 @@ export function createIndexPatternService({ }), }); return { - updateIndexPatternsCache: updateIndexPatterns, + updateDataViewsState: updateIndexPatterns, loadIndexPatterns: (args) => { return loadIndexPatterns({ dataViews, diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 6ebf1cb7d3f835..e5995762b55055 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -102,7 +102,10 @@ export async function refreshIndexPatternsList({ indexPatternService.loadIndexPatternRefs({ isFullEditor: true }), ]); const indexPattern = newlyMappedIndexPattern[indexPatternId]; - indexPatternService.updateIndexPatternsCache({ + // But what about existingFields here? + // When the indexPatterns cache object gets updated, the data panel will + // notice it and refetch the fields list existence map + indexPatternService.updateDataViewsState({ indexPatterns: { ...indexPatternsCache, [indexPatternId]: indexPattern, From 0ed3fdf49d50263c494cf271ae8e0652fd0a802c Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Thu, 11 Aug 2022 14:47:40 +0200 Subject: [PATCH 31/98] Update x-pack/plugins/lens/public/visualizations/xy/types.ts --- x-pack/plugins/lens/public/visualizations/xy/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 37176797b5d4c0..c2ca25c61c42ed 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -113,7 +113,6 @@ export interface XYAnnotationLayerConfig { layerId: string; layerType: 'annotations'; annotations: EventAnnotationConfig[]; - hide?: boolean; simpleView?: boolean; } From 07e3136c5372ae3d4f1dff9dbeda4ddb3fd7d944 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 11 Aug 2022 15:02:24 +0200 Subject: [PATCH 32/98] :camera_flash: Fix snapshot --- .../lens/public/app_plugin/__snapshots__/app.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap b/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap index bfdc82ff3b7c4d..9f65fa549e03c5 100644 --- a/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap +++ b/x-pack/plugins/lens/public/app_plugin/__snapshots__/app.test.tsx.snap @@ -10,7 +10,7 @@ Array [ "loadIndexPatternRefs": [Function], "loadIndexPatterns": [Function], "refreshExistingFields": [Function], - "updateIndexPatternsCache": [Function], + "updateDataViewsState": [Function], }, "lensInspector": Object { "adapters": Object { From d3175135848b078596de1b95570683d1647e2314 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 11 Aug 2022 16:28:52 +0200 Subject: [PATCH 33/98] :bug: Fix the dataViews switch bug --- .../public/state_management/lens_slice.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index b5f9ec00287d51..b880b9478c16a4 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -374,19 +374,20 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { .getTableSpec() .map(({ columnId }) => columnId) .filter((columnId) => !nextTable.has(columnId)); - const nextVisState = (newState.visualization || state.visualization).state; const activeVisualization = visualizationMap[state.visualization.activeId]; + let nextVisState = (newState.visualization || state.visualization).state; removed.forEach((columnId) => { - newState.visualization = { - ...state.visualization, - state: activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - frame, - }), - }; + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + frame, + }); }); + newState.visualization = { + ...state.visualization, + state: nextVisState, + }; } } } From 1b64dda6316320f9d68869e5aca9430944284c33 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 10:36:27 +0200 Subject: [PATCH 34/98] :fire: remove old cruft --- x-pack/plugins/lens/public/types.ts | 2 +- .../lens/public/visualizations/xy/visualization.tsx | 4 +--- .../visualizations/xy/visualization_helpers.tsx | 12 ++---------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 39c0d3c2a30fee..0a76ceec5d3153 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -900,7 +900,7 @@ export interface Visualization { /** 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) => T; /** Retrieve a list of supported layer types with initialization data */ getSupportedLayers: ( diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 40e357933ab031..e0d0a19cebec87 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -121,7 +121,7 @@ export const getXyVisualization = ({ }; }, - appendLayer(state, layerId, layerType, indexPatternId) { + appendLayer(state, layerId, layerType) { const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType; return { ...state, @@ -131,7 +131,6 @@ export const getXyVisualization = ({ seriesType: firstUsedSeriesType || state.preferredSeriesType, layerId, layerType, - indexPatternId: indexPatternId ?? core.uiSettings.get('defaultIndex'), }), ], }; @@ -146,7 +145,6 @@ export const getXyVisualization = ({ : newLayerState({ seriesType: state.preferredSeriesType, layerId, - indexPatternId: core.uiSettings.get('defaultIndex'), }) ), }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx index 90956b573c0b3b..c36916f7f03061 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx @@ -277,13 +277,7 @@ const newLayerFn = { layerType: layerTypes.REFERENCELINE, accessors: [], }), - [layerTypes.ANNOTATIONS]: ({ - layerId, - indexPatternId, - }: { - layerId: string; - indexPatternId: string; - }): XYAnnotationLayerConfig => ({ + [layerTypes.ANNOTATIONS]: ({ layerId }: { layerId: string }): XYAnnotationLayerConfig => ({ layerId, layerType: layerTypes.ANNOTATIONS, annotations: [], @@ -294,14 +288,12 @@ export function newLayerState({ layerId, layerType = layerTypes.DATA, seriesType, - indexPatternId, }: { layerId: string; layerType?: LayerType; seriesType: SeriesType; - indexPatternId: string; }) { - return newLayerFn[layerType]({ layerId, seriesType, indexPatternId }); + return newLayerFn[layerType]({ layerId, seriesType }); } export function getLayersByType(state: State, byType?: string) { From f6a0f4ae1eaf30fd14fbf651aa75ef775cd3a110 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 11:27:23 +0200 Subject: [PATCH 35/98] :recycle: Revert removal from dataviews state branch --- x-pack/plugins/lens/public/types.ts | 4 ++-- .../lens/public/visualizations/xy/visualization.tsx | 6 ++++-- .../visualizations/xy/visualization_helpers.tsx | 12 ++++++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 4777f78ce565db..9480422923e294 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -904,11 +904,11 @@ export interface Visualization { /** 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) => T; + appendLayer?: (state: T, layerId: string, type: LayerType, indexPatternId: string) => T; /** Retrieve a list of supported layer types with initialization data */ getSupportedLayers: ( diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index e715b99b8b7244..4cf9f05943bc35 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -122,7 +122,7 @@ export const getXyVisualization = ({ }; }, - appendLayer(state, layerId, layerType) { + appendLayer(state, layerId, layerType, indexPatternId) { const firstUsedSeriesType = getDataLayers(state.layers)?.[0]?.seriesType; return { ...state, @@ -132,12 +132,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) => @@ -146,6 +147,7 @@ export const getXyVisualization = ({ : newLayerState({ seriesType: state.preferredSeriesType, layerId, + indexPatternId, }) ), }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx index bef05d63647add..b64e6eccd21c5c 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization_helpers.tsx @@ -277,7 +277,13 @@ const newLayerFn = { layerType: layerTypes.REFERENCELINE, accessors: [], }), - [layerTypes.ANNOTATIONS]: ({ layerId }: { layerId: string }): XYAnnotationLayerConfig => ({ + [layerTypes.ANNOTATIONS]: ({ + layerId, + indexPatternId, + }: { + layerId: string; + indexPatternId: string; + }): XYAnnotationLayerConfig => ({ layerId, layerType: layerTypes.ANNOTATIONS, annotations: [], @@ -289,12 +295,14 @@ export function newLayerState({ layerId, layerType = layerTypes.DATA, seriesType, + indexPatternId, }: { layerId: string; layerType?: LayerType; seriesType: SeriesType; + indexPatternId: string; }) { - return newLayerFn[layerType]({ layerId, seriesType }); + return newLayerFn[layerType]({ layerId, seriesType, indexPatternId }); } export function getLayersByType(state: State, byType?: string) { From efbf3f0fee8961137c1ea596933c0b2c8038dced Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 12:17:38 +0200 Subject: [PATCH 36/98] :recycle: Load all at once --- x-pack/plugins/lens/public/visualizations/xy/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/index.ts b/x-pack/plugins/lens/public/visualizations/xy/index.ts index 37d4f46a5bcc72..9dcd1dab635938 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/index.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/index.ts @@ -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, From ad68d402b295fe694f5748e614fa2b4b388433cc Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 16:53:39 +0200 Subject: [PATCH 37/98] :wrench: working on persistent state + fix new layer bug --- .../public/state_management/lens_slice.ts | 16 +++++-- x-pack/plugins/lens/public/types.ts | 4 +- .../public/visualizations/xy/state_helpers.ts | 42 +++++++++++++++++++ .../lens/public/visualizations/xy/types.ts | 4 ++ .../visualizations/xy/visualization.tsx | 6 ++- 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 2a554ef282c8df..2f7ab59c76b52f 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -275,6 +275,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { } ) => { const activeVisualization = visualizationMap[visualizationId]; + const activeDataSource = datasourceMap[state.activeDatasourceId!]; const isOnlyLayer = getRemoveOperation( activeVisualization, @@ -296,9 +297,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]: ( @@ -743,15 +748,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) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 9480422923e294..28a10def5f04d1 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -857,7 +857,7 @@ export interface VisualizationDisplayOptions { noPadding?: boolean; } -export interface Visualization { +export interface Visualization { /** Plugin ID, such as "lnsXY" */ id: string; @@ -900,6 +900,8 @@ export interface Visualization { 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[] }; /** Frame needs to know which layers the visualization is currently using */ getLayerIds: (state: T) => string[]; diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts index d1fcfdc1c7997b..06dcd4ede59482 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts @@ -6,6 +6,7 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import type { SavedObjectReference } from '@kbn/core/public'; import type { FramePublicAPI, DatasourcePublicAPI } from '../../types'; import { visualizationTypes, @@ -14,6 +15,8 @@ import { XYReferenceLineLayerConfig, SeriesType, YConfig, + XYState, + XYPersistedState, } from './types'; import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers'; @@ -102,3 +105,42 @@ export function hasHistogramSeries( ); }); } + +function getLayerReferenceName(layerId: string) { + return `xy-visualization-layer-${layerId}`; +} + +export function extractReferences({ layers }: XYState) { + const savedObjectReferences: SavedObjectReference[] = []; + const persistableLayers: Array> = []; + 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: { layers: persistableLayers } }; +} + +export function injectReferences(state: XYPersistedState, references: SavedObjectReference[]) { + return { + layers: state.layers.map((layer) => { + if (!isAnnotationsLayer(layer)) { + return layer; + } + return { + ...layer, + indexPatternId: references.find( + ({ name }) => name === getLayerReferenceName(layer.layerId) + )!.id, + }; + }), + }; +} diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 3c560045cae30f..0be0a644969863 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -159,6 +159,10 @@ export interface XYState { export type State = XYState; +export type XYPersistedState = Omit & { + layers: Array>; +}; + const groupLabelForBar = i18n.translate('xpack.lens.xyVisualization.barGroupLabel', { defaultMessage: 'Bar', }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 4cf9f05943bc35..2d33675f352d5b 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -35,7 +35,7 @@ import { SeriesType, } from './types'; import { layerTypes } from '../../../common'; -import { isHorizontalChart } from './state_helpers'; +import { extractReferences, isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { getAccessorColorConfigs, getColorAssignments } from './color_assignment'; import { getColumnToLabelMap } from './state_helpers'; @@ -153,6 +153,10 @@ export const getXyVisualization = ({ }; }, + getPersistableState(state) { + return extractReferences(state); + }, + getDescription, switchVisualizationType(seriesType: string, state: State) { From bcea9c6e14ec56d96b0b76fd49289238b72ef312 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 17:44:45 +0200 Subject: [PATCH 38/98] :fire: remove unused stuff --- .../editor_frame/config_panel/layer_panel.tsx | 2 -- x-pack/plugins/lens/public/types.ts | 1 - .../visualizations/xy/xy_config_panel/layer_header.tsx | 10 +++------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 443910ce54fa5d..86e896c29910c3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -322,7 +322,6 @@ export function LayerPanel( layerId, visualizationId: activeVisualization.id, }), - defaultIndexPatternId: props.indexPatternService.getDefaultIndex(), }} activeVisualization={activeVisualization} /> @@ -365,7 +364,6 @@ export function LayerPanel( layerId, visualizationId: activeVisualization.id, }), - defaultIndexPatternId: props.indexPatternService.getDefaultIndex(), }} /> )} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 28a10def5f04d1..5483bd00f07b78 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -640,7 +640,6 @@ export interface VisualizationConfigProps { export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; onChangeIndexPattern: (indexPatternId: string) => void; - defaultIndexPatternId: string; }; export type VisualizationLayerHeaderContentProps = VisualizationLayerWidgetProps; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 14feda9f828c60..8ec4a11ba8744d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -73,7 +73,6 @@ function AnnotationLayerHeaderContent({ state, layerId, onChangeIndexPattern, - defaultIndexPatternId, }: VisualizationLayerHeaderContentProps) { const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', @@ -81,20 +80,17 @@ function AnnotationLayerHeaderContent({ const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[layerIndex] as XYAnnotationLayerConfig; - // what if no indexPatternId has been set? Fallback to uiSettings default one for now - const indexPatternId = layer.indexPatternId ?? defaultIndexPatternId; - return ( Date: Fri, 12 Aug 2022 19:02:54 +0200 Subject: [PATCH 39/98] :label: Fix some typings --- src/plugins/event_annotation/common/types.ts | 7 ++- .../definitions/filters/filters.tsx | 6 ++- .../definitions/ranges/advanced_editor.tsx | 12 ++--- .../definitions/terms/field_inputs.tsx | 8 +++- .../lens/public/mocks/visualization_mock.ts | 2 +- .../datatable/visualization.test.tsx | 2 +- .../legacy_metric/visualization.test.ts | 2 +- .../metric/visualization.test.ts | 2 +- .../visualizations/xy/visualization.test.ts | 47 +++++++++++++++---- .../annotations_config_panel/index.test.tsx | 7 ++- .../visualizations/xy/xy_suggestions.test.ts | 2 + 11 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 49b82f52a615a9..79bc8cc14fa950 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -15,7 +15,10 @@ import { ManualPointEventAnnotationOutput, ManualRangeEventAnnotationOutput, } from './manual_event_annotation/types'; -import { QueryPointEventAnnotationOutput } from './query_event_annotation/types'; +import { + QueryPointEventAnnotationArgs, + QueryPointEventAnnotationOutput, +} from './query_event_annotation/types'; export type LineStyle = 'solid' | 'dashed' | 'dotted'; export type Fill = 'inside' | 'outside' | 'none'; @@ -27,7 +30,7 @@ export type AvailableAnnotationIcon = $Values; export type EventAnnotationArgs = | ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs - | QueryPointEventAnnotationOutput; + | QueryPointEventAnnotationArgs; export type EventAnnotationOutput = | ManualPointEventAnnotationOutput | ManualRangeEventAnnotationOutput diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index cee188b6f483ab..f3dc6490865c33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -15,12 +15,16 @@ 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, + 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'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 60d5a76a3085e5..9afd9311f026c6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -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(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx index 5ae10751e64c46..f140fdb9807ac6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/field_inputs.tsx @@ -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; diff --git a/x-pack/plugins/lens/public/mocks/visualization_mock.ts b/x-pack/plugins/lens/public/mocks/visualization_mock.ts index f002af43f88c8e..23700094dc7c6f 100644 --- a/x-pack/plugins/lens/public/mocks/visualization_mock.ts +++ b/x-pack/plugins/lens/public/mocks/visualization_mock.ts @@ -11,7 +11,7 @@ import { Visualization, VisualizationMap } from '../types'; export function createMockVisualization(id = 'testVis'): jest.Mocked { 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' }]), diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx index 494be445670ae1..85d1bc3f1a5753 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.test.tsx @@ -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: [], diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts index 399c29797ff831..88e2e47184bb2b 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts @@ -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, diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts index 91a7e0ffc2053a..397e0d02eff59d 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts @@ -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 { "color": "static-color", "layerId": "first", diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index b1313d765ae926..68e95dc89ca7ee 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -38,6 +38,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; const exampleAnnotation: EventAnnotationConfig = { id: 'an1', + type: 'manual', label: 'Event 1', key: { type: 'point_in_time', @@ -47,6 +48,7 @@ const exampleAnnotation: EventAnnotationConfig = { }; const exampleAnnotation2: EventAnnotationConfig = { icon: 'circle', + type: 'manual', id: 'an2', key: { timestamp: '2022-04-18T11:01:59.135Z', @@ -237,7 +239,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 +252,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 +467,7 @@ describe('xy_visualization', () => { { layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ], @@ -495,6 +503,7 @@ describe('xy_visualization', () => { { layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], }, ], @@ -530,6 +539,7 @@ describe('xy_visualization', () => { { layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation, exampleAnnotation2], }, ], @@ -566,12 +576,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], }, ], @@ -614,12 +626,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 +658,13 @@ describe('xy_visualization', () => { { layerId: 'first', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], }, { layerId: 'second', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ]); @@ -662,12 +678,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 +710,13 @@ describe('xy_visualization', () => { { layerId: 'first', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [], }, { layerId: 'second', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ]); @@ -710,12 +730,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 +762,13 @@ describe('xy_visualization', () => { { layerId: 'first', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [], }, { layerId: 'second', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ]); @@ -1105,6 +1129,7 @@ describe('xy_visualization', () => { { layerId: 'ann', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation, { ...exampleAnnotation, id: 'an2' }], }, ], @@ -1123,6 +1148,7 @@ describe('xy_visualization', () => { { layerId: 'ann', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ]); @@ -1842,6 +1868,7 @@ describe('xy_visualization', () => { { layerId: 'annotations', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index 330fbf1f55da61..f4c375d46e5d27 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -16,6 +16,7 @@ import { State } from '../../types'; import { Position } from '@elastic/charts'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import moment from 'moment'; +import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); @@ -26,8 +27,9 @@ jest.mock('lodash', () => { }; }); -const customLineStaticAnnotation = { +const customLineStaticAnnotation: EventAnnotationConfig = { id: 'ann1', + type: 'manual', key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, label: 'Event', icon: 'triangle' as const, @@ -49,6 +51,7 @@ describe('AnnotationsPanel', () => { { layerType: layerTypes.ANNOTATIONS, layerId: 'annotation', + indexPatternId: 'indexPattern1', annotations: [customLineStaticAnnotation], }, ], @@ -109,6 +112,7 @@ describe('AnnotationsPanel', () => { color: 'red', icon: 'triangle', id: 'ann1', + type: 'manual', isHidden: undefined, key: { endTimestamp: '2022-03-21T10:49:00.000Z', @@ -122,6 +126,7 @@ describe('AnnotationsPanel', () => { ], layerId: 'annotation', layerType: 'annotations', + indexPatternId: 'indexPattern1', }; const component = mount( { const annotationLayer: XYAnnotationLayerConfig = { layerId: 'second', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [ { id: '1', + type: 'manual', key: { type: 'point_in_time', timestamp: '2020-20-22', From bb3a711e2b02ba62281f7b9fe72d5c0a4c75253b Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 12 Aug 2022 19:28:58 +0200 Subject: [PATCH 40/98] :wrench: Fix expression issue --- src/plugins/event_annotation/common/index.ts | 2 ++ .../event_annotation/common/query_event_annotation/index.ts | 6 +++--- src/plugins/event_annotation/public/plugin.ts | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 8c6d0db59dbd2b..eca684c67626ea 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -13,6 +13,7 @@ export type { ManualRangeEventAnnotationOutput, } from './manual_event_annotation/types'; export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; +export { queryPointEventAnnotation } from './query_event_annotation'; export { eventAnnotationGroup } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group'; export { fetchEventAnnotations } from './fetch_event_annotations'; @@ -23,4 +24,5 @@ export type { PointInTimeEventAnnotationConfig, PointInTimeQueryEventAnnotationConfig, AvailableAnnotationIcon, + EventAnnotationOutput, } from './types'; diff --git a/src/plugins/event_annotation/common/query_event_annotation/index.ts b/src/plugins/event_annotation/common/query_event_annotation/index.ts index 835d70f4d94771..f0d768d2bff00a 100644 --- a/src/plugins/event_annotation/common/query_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_event_annotation/index.ts @@ -92,13 +92,13 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition< query: { types: ['kibana_query'], help: i18n.translate('eventAnnotation.queryAnnotation.args.query', { - defaultMessage: ``, + defaultMessage: `Query to apply for the annotation`, }), }, additionalFields: { types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.query', { - defaultMessage: ``, + help: i18n.translate('eventAnnotation.queryAnnotation.args.additionalFields', { + defaultMessage: `Provide additional field names to visualize in the tooltip`, }), multi: true, }, diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index 138b925d44baa4..60cf3c752d49e1 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -13,6 +13,7 @@ import { manualRangeEventAnnotation, eventAnnotationGroup, fetchEventAnnotations, + queryPointEventAnnotation, } from '../common'; import { EventAnnotationService } from './event_annotation_service'; @@ -35,6 +36,7 @@ export class EventAnnotationPlugin public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup { dependencies.expressions.registerFunction(manualPointEventAnnotation); dependencies.expressions.registerFunction(manualRangeEventAnnotation); + dependencies.expressions.registerFunction(queryPointEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); dependencies.expressions.registerFunction(fetchEventAnnotations); return this.eventAnnotationService; From 3523006aeb4f1584e7626c276d228923e02d588f Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 14:57:44 +0200 Subject: [PATCH 41/98] :white_check_mark: Add service unit tests --- .../event_annotation_service/service.test.ts | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 src/plugins/event_annotation/public/event_annotation_service/service.test.ts diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts new file mode 100644 index 00000000000000..d1229466f2b2ff --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -0,0 +1,333 @@ +/* + * 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', + key: { + type: 'point_in_time', + field: '@timestamp', + }, + label: 'Hello Range', + isHidden: true, + query: { query: '', language: 'kql' }, + }, + ]) + ).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: { + time: ['2022'], + label: ['Hello'], + color: ['#f04e98'], + lineWidth: [1], + lineStyle: ['solid'], + icon: ['triangle'], + textVisibility: [false], + isHidden: [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: { + time: ['2021'], + endTime: ['2022'], + label: ['Hello'], + color: ['#F04E981A'], + outside: [false], + isHidden: [false], + }, + }, + ], + }, + ]); + }); + it('should process query based annotations', () => { + expect( + eventAnnotationService.toExpression([ + { + id: 'myEvent', + type: 'query', + key: { + type: 'point_in_time', + field: '@timestamp', + }, + label: 'Hello', + query: { query: '', language: 'kql' }, + }, + ]) + ).toEqual([ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'query_point_event_annotation', + arguments: { + field: ['@timestamp'], + label: ['Hello'], + color: ['#f04e98'], + lineWidth: [1], + lineStyle: ['solid'], + icon: ['triangle'], + textVisibility: [false], + textSource: [], + textField: [], + isHidden: [false], + query: [ + { + type: 'expression', + chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], + }, + ], + additionalFields: [], + }, + }, + ], + }, + ]); + }); + 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', + key: { + type: 'point_in_time', + field: '@timestamp', + }, + label: 'Hello', + query: { query: '', language: 'kql' }, + }, + ]) + ).toEqual([ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_point_event_annotation', + arguments: { + time: ['2022'], + label: ['Hello'], + color: ['#f04e98'], + lineWidth: [1], + lineStyle: ['solid'], + icon: ['triangle'], + textVisibility: [false], + isHidden: [false], + }, + }, + ], + }, + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_range_event_annotation', + arguments: { + time: ['2021'], + endTime: ['2022'], + label: ['Hello Range'], + color: ['#F04E981A'], + outside: [false], + isHidden: [false], + }, + }, + ], + }, + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'query_point_event_annotation', + arguments: { + field: ['@timestamp'], + label: ['Hello'], + color: ['#f04e98'], + lineWidth: [1], + lineStyle: ['solid'], + icon: ['triangle'], + textVisibility: [false], + textSource: [], + textField: [], + isHidden: [false], + query: [ + { + type: 'expression', + chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], + }, + ], + additionalFields: [], + }, + }, + ], + }, + ]); + }); + it.each` + textSource | textField | expected + ${''} | ${''} | ${'name'} + ${'name'} | ${''} | ${'name'} + ${'name'} | ${'myField'} | ${'name'} + ${'field'} | ${''} | ${'field'} + ${'field'} | ${'myField'} | ${'field'} + `( + "should handle correctly textVisibility when textSource is set to '$textSource' and textField to '$textField'", + ({ textSource, textField, expected }) => { + expect( + eventAnnotationService.toExpression([ + { + id: 'myEvent', + type: 'query', + key: { + type: 'point_in_time', + field: '@timestamp', + }, + label: 'Hello', + query: { query: '', language: 'kql' }, + textVisibility: true, + textSource, + textField, + }, + ]) + ).toEqual([ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'query_point_event_annotation', + arguments: { + field: ['@timestamp'], + label: ['Hello'], + color: ['#f04e98'], + lineWidth: [1], + lineStyle: ['solid'], + icon: ['triangle'], + textVisibility: [true], + textSource: [expected], + textField: textSource === 'field' && textField ? [textField] : [], + isHidden: [false], + query: [ + { + type: 'expression', + chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], + }, + ], + additionalFields: [], + }, + }, + ], + }, + ]); + } + ); + }); +}); From 3d9c4f073cf7e89d50eaccf2e84993d61bb826ac Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 15:50:03 +0200 Subject: [PATCH 42/98] :ok_hand: Integrated feedback --- src/plugins/event_annotation/common/types.ts | 2 +- .../event_annotation_service/service.test.ts | 15 ++++++--------- .../public/event_annotation_service/service.tsx | 5 ++--- .../annotations_config_panel/helpers.ts | 2 +- .../tooltip_annotation_panel.tsx | 4 ++-- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 79bc8cc14fa950..e2d9a4576eda7f 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -65,7 +65,7 @@ export type PointInTimeQueryEventAnnotationConfig = { type: 'point_in_time'; field: string; }; - additionalFields?: string[]; + extraFields?: string[]; query: Query; textSource?: 'name' | 'field'; textField?: string; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index d1229466f2b2ff..bb2165c3c7ab6f 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -155,7 +155,6 @@ describe('Event Annotation Service', () => { lineStyle: ['solid'], icon: ['triangle'], textVisibility: [false], - textSource: [], textField: [], isHidden: [false], query: [ @@ -255,7 +254,6 @@ describe('Event Annotation Service', () => { lineStyle: ['solid'], icon: ['triangle'], textVisibility: [false], - textSource: [], textField: [], isHidden: [false], query: [ @@ -273,11 +271,11 @@ describe('Event Annotation Service', () => { }); it.each` textSource | textField | expected - ${''} | ${''} | ${'name'} - ${'name'} | ${''} | ${'name'} - ${'name'} | ${'myField'} | ${'name'} - ${'field'} | ${''} | ${'field'} - ${'field'} | ${'myField'} | ${'field'} + ${''} | ${''} | ${''} + ${'name'} | ${''} | ${''} + ${'name'} | ${'myField'} | ${''} + ${'field'} | ${''} | ${''} + ${'field'} | ${'myField'} | ${'myField'} `( "should handle correctly textVisibility when textSource is set to '$textSource' and textField to '$textField'", ({ textSource, textField, expected }) => { @@ -312,8 +310,7 @@ describe('Event Annotation Service', () => { lineStyle: ['solid'], icon: ['triangle'], textVisibility: [true], - textSource: [expected], - textField: textSource === 'field' && textField ? [textField] : [], + textField: expected ? [expected] : [], isHidden: [false], query: [ { diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index b37596b753d4d4..95e58eb6055ab6 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -91,7 +91,7 @@ export function getEventAnnotationService(): EventAnnotationServiceType { textField, textSource, query, - additionalFields, + extraFields, } = annotation; expressions.push({ type: 'expression' as const, @@ -107,11 +107,10 @@ export function getEventAnnotationService(): EventAnnotationServiceType { lineStyle: [lineStyle || 'solid'], icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], - textSource: textVisibility ? [textSource || 'name'] : [], textField: textVisibility && textSource === 'field' && textField ? [textField] : [], isHidden: [Boolean(isHidden)], query: query ? [queryToAst(query)] : [], - additionalFields: additionalFields || [], + extraFields: extraFields || [], }, }, ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts index 4914a638011c4d..2ea1b03b9b0d86 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts @@ -96,7 +96,7 @@ export const sanitizeProperties = (annotation: EventAnnotationConfig) => { 'textSource', 'textField', 'query', - 'additionalFields', + 'extraFields', ]); return lineAnnotation; } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index c2a190585918ee..f8f34e6ae4d263 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -61,13 +61,13 @@ export function TooltipSection({ (values: WrappedValue[]) => { setConfig({ ...currentConfig, - additionalFields: values.filter(removeNewEmptyField).map(({ value }) => value), + extraFields: values.filter(removeNewEmptyField).map(({ value }) => value), }); }, [setConfig, currentConfig] ); const wrappedValues = useMemo(() => { - return currentConfig.additionalFields?.map((value) => ({ id: generateId(), value })) || []; + return currentConfig.extraFields?.map((value) => ({ id: generateId(), value })) || []; }, [currentConfig]); const { inputValue: localValues, handleInputChange } = useDebouncedValue({ From 7e763adaca1e08208ad47fd4b554217b68ba4f87 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 16:56:35 +0200 Subject: [PATCH 43/98] :sparkles: Add migration code for manual annotations --- .../make_lens_embeddable_factory.ts | 13 ++++++ .../server/migrations/common_migrations.ts | 35 ++++++++++++++++ .../saved_object_migrations.test.ts | 41 +++++++++++++++++++ .../migrations/saved_object_migrations.ts | 12 ++++++ .../plugins/lens/server/migrations/types.ts | 21 ++++++++++ 5 files changed, 122 insertions(+) diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index dc3933d852979c..c066ccd8a26855 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -27,6 +27,7 @@ import { commonUpdateVisLayerType, getLensCustomVisualizationMigrations, getLensFilterMigrations, + commonExplicitAnnotationType, } from '../migrations/common_migrations'; import { CustomVisualizationMigrations, @@ -38,6 +39,7 @@ import { VisState810, VisStatePre715, VisStatePre830, + XYVisState850, } from '../migrations/types'; import { extract, inject } from '../../common/embeddable_factory'; @@ -124,6 +126,17 @@ export const makeLensEmbeddableFactory = attributes: migratedLensState, } as unknown as SerializableRecord; }, + '8.5.0': (state) => { + const lensState = state as unknown as { + attributes: LensDocShape810; + }; + const migratedLensState = commonExplicitAnnotationType(lensState.attributes); + + return { + ...lensState, + attributes: migratedLensState, + } as unknown as SerializableRecord; + }, }), getLensCustomVisualizationMigrations(customVisualizationMigrations) ), diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 10fc9b25e6f349..73cd82bba768bf 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -30,6 +30,8 @@ import { LensDocShape810, LensDocShape830, VisStatePre830, + XYVisStatePre850, + XYVisState850, } from './types'; import { DOCUMENT_FIELD_NAME, layerTypes, MetricState } from '../../common'; import { LensDocShape } from './saved_object_migrations'; @@ -398,3 +400,36 @@ export const commonFixValueLabelsInXY = ( }, }; }; + +export const commonExplicitAnnotationType = ( + attributes: LensDocShape830 +): LensDocShape830 => { + // Skip the migration heavy part if not XY or it does not contain annotations + if ( + attributes.visualizationType !== 'lnsXY' || + attributes.state.visualization.layers.every((l) => l.layerType !== 'annotations') + ) { + return attributes as LensDocShape830; + } + const newAttributes = cloneDeep(attributes); + const { visualization } = newAttributes.state; + const { layers } = visualization; + return { + ...newAttributes, + state: { + ...newAttributes.state, + visualization: { + ...visualization, + layers: layers.map((l) => { + if (l.layerType !== 'annotations') { + return l; + } + return { + ...l, + annotations: l.annotations.map((a) => ({ ...a, type: 'manual' })), + }; + }), + }, + }, + }; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 53765ed69cdac3..3cb65d8862a8a0 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -22,6 +22,9 @@ import { VisState810, VisState820, VisState830, + LensDocShape830, + XYVisStatePre850, + XYVisState850, } from './types'; import { layerTypes, MetricState } from '../../common'; import { Filter } from '@kbn/es-query'; @@ -2232,4 +2235,42 @@ describe('Lens migrations', () => { expect(visState.valueLabels).toBe('hide'); }); }); + + describe('8.5.0 Add Annotation event type', () => { + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + layers: [ + { layerType: 'data' }, + { + layerType: 'annotations', + annotations: [{ id: 'annotation-id' }], + }, + ], + }, + }, + }, + } as unknown as SavedObjectUnsanitizedDoc>; + + it('migrates existing annotation events as manual type', () => { + const result = migrations['8.5.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + const visState = result.attributes.state.visualization as XYVisState850; + const [dataLayer, annotationLayer] = visState.layers; + expect(dataLayer).toEqual({ layerType: 'data' }); + expect(annotationLayer).toEqual({ + layerType: 'annotations', + annotations: [{ id: 'annotation-id', type: 'manual' }], + }); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index e6daa2cb99439c..468b0f8fc2fed5 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -33,6 +33,8 @@ import { XYVisualizationState830, VisState810, VisState820, + XYVisState850, + XYVisStatePre850, } from './types'; import { commonRenameOperationsForFormula, @@ -50,6 +52,7 @@ import { commonFixValueLabelsInXY, commonLockOldMetricVisSettings, commonPreserveOldLegendSizeDefault, + commonExplicitAnnotationType, } from './common_migrations'; interface LensDocShapePre710 { @@ -510,6 +513,14 @@ const preserveOldLegendSizeDefault: SavedObjectMigrationFn ({ ...doc, attributes: commonPreserveOldLegendSizeDefault(doc.attributes) }); +const addEventAnnotationType: SavedObjectMigrationFn< + LensDocShape830, + LensDocShape830 +> = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: commonExplicitAnnotationType(newDoc.attributes) }; +}; + const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -530,6 +541,7 @@ const lensMigrations: SavedObjectMigrationMap = { enhanceTableRowHeight ), '8.3.0': flow(lockOldMetricVisSettings, preserveOldLegendSizeDefault, fixValueLabelsInXY), + '8.5.0': flow(addEventAnnotationType), }; export const getAllMigrations = ( diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 6b38bb4b4f6311..a72b390e307d4b 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -269,3 +269,24 @@ export interface XYVisualizationState830 extends VisState820 { export type VisStatePre830 = XYVisualizationStatePre830; export type VisState830 = XYVisualizationState830; + +export interface XYVisStatePre850 { + layers: Array< + | { + layerType: Exclude; + } + | { layerType: Extract; annotations: Array<{ id: string }> } + >; +} + +export interface XYVisState850 { + layers: Array< + | { + layerType: Exclude; + } + | { + layerType: Extract; + annotations: Array<{ id: string; type: 'manual' | 'query' }>; + } + >; +} From bb86c3a72195890226acdc0adc8e4c9921cbf5f6 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 17:23:02 +0200 Subject: [PATCH 44/98] :label: Fix type issue --- .../event_annotation/common/event_annotation_group/index.ts | 2 +- .../event_annotation/common/manual_event_annotation/types.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts index 4258d4835c103f..c56454b26c8d69 100644 --- a/src/plugins/event_annotation/common/event_annotation_group/index.ts +++ b/src/plugins/event_annotation/common/event_annotation_group/index.ts @@ -9,7 +9,7 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; import { IndexPatternExpressionType } from '@kbn/data-views-plugin/common'; -import type { EventAnnotationOutput } from '../manual_event_annotation/types'; +import type { EventAnnotationOutput } from '../types'; export interface EventAnnotationGroupOutput { type: 'event_annotation_group'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index f720da61a4013f..5ede7d5233aaf7 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -26,11 +26,6 @@ export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs & type: 'manual_range_event_annotation'; }; -export type EventAnnotationArgs = ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs; -export type EventAnnotationOutput = - | ManualPointEventAnnotationOutput - | ManualRangeEventAnnotationOutput; - export const annotationColumns: DatatableColumn[] = [ { id: 'time', name: 'time', meta: { type: 'string' } }, { id: 'endTime', name: 'endTime', meta: { type: 'string' } }, From 3da5b1946b00ab957adbd5aef8b5cee09c793db9 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 19:15:39 +0200 Subject: [PATCH 45/98] :white_check_mark: Add some other unit test --- .../visualizations/xy/to_expression.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts index 11215a8cf492c2..37e1e3c5be1392 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts @@ -538,4 +538,36 @@ describe('#toExpression', () => { expect(getYConfigColorForLayer(expression, 0)).toEqual([]); 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); + }); }); From c8206b836b0df5e9c27e7fae55ac6bc95c88f468 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 24 Aug 2022 19:21:49 +0200 Subject: [PATCH 46/98] :label: Fix more type issues --- .../public/components/annotations.tsx | 3 ++ .../common/fetch_event_annotations/index.ts | 42 ++++++++++++------- .../common/fetch_event_annotations/utils.ts | 12 +++++- .../common/manual_event_annotation/types.ts | 4 ++ .../common/query_event_annotation/index.ts | 4 +- .../common/query_event_annotation/types.ts | 2 +- .../visualizations/xy/visualization.test.ts | 6 +++ .../annotations_config_panel/index.test.tsx | 5 +++ .../xy/xy_config_panel/layer_header.tsx | 2 +- .../shared/marker_decoration_settings.tsx | 9 ++-- 10 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 119bc0948cd5c4..8a123dce8cdec1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -29,6 +29,8 @@ import { defaultAnnotationColor, defaultAnnotationRangeColor, } from '@kbn/event-annotation-plugin/public'; +import { isQueryAnnotation } from '@kbn/event-annotation-plugin/common/fetch_event_annotations/utils'; +import { ManualEventAnnotationOutput } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types'; import type { AnnotationLayerArgs, CommonXYAnnotationLayerConfig, @@ -65,6 +67,7 @@ const groupVisibleConfigsByInterval = ( .flatMap(({ annotations }) => annotations.filter((a) => !a.isHidden && a.type === 'manual_point_event_annotation') ) + .filter((a): a is ManualEventAnnotationOutput => !isQueryAnnotation(a)) .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()) .reduce>((acc, current) => { const roundedTimestamp = getRoundedTimestamp( diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/index.ts b/src/plugins/event_annotation/common/fetch_event_annotations/index.ts index 92408b52846e0f..364deca43349b2 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/index.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/index.ts @@ -13,8 +13,14 @@ import type { ExpressionFunctionDefinition, Datatable } from '@kbn/expressions-p import moment from 'moment'; import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts'; import { EventAnnotationGroupOutput } from '../event_annotation_group'; -import { annotationColumns, EventAnnotationOutput } from '../manual_event_annotation/types'; -import { filterOutOfTimeRange, isManualPointAnnotation, sortByTime } from './utils'; +import { annotationColumns, ManualEventAnnotationOutput } from '../manual_event_annotation/types'; +import { + filterOutOfTimeRange, + isManualPointAnnotation, + isQueryAnnotation, + sortByTime, +} from './utils'; +import { EventAnnotationOutput } from '../types'; export interface FetchEventAnnotationsDatatable { annotations: EventAnnotationOutput[]; @@ -76,20 +82,24 @@ export function fetchEventAnnotations(): FetchEventAnnotationsExpressionFunction const datatable: Datatable = { type: 'datatable', columns: annotationColumns, - rows: annotations.sort(sortByTime).map((annotation) => { - const initialDate = moment(annotation.time).valueOf(); - const snappedDate = roundDateToESInterval( - initialDate, - parseEsInterval(args.interval) as ESCalendarInterval | ESFixedInterval, - 'start', - 'UTC' - ); - return { - ...annotation, - type: isManualPointAnnotation(annotation) ? 'point' : 'range', - timebucket: moment(snappedDate).toISOString(), - }; - }), + rows: annotations + // TODO: remove once the Query fetching part is complete + .filter((a): a is ManualEventAnnotationOutput => !isQueryAnnotation(a)) + .sort(sortByTime) + .map((annotation) => { + const initialDate = moment(annotation.time).valueOf(); + const snappedDate = roundDateToESInterval( + initialDate, + parseEsInterval(args.interval) as ESCalendarInterval | ESFixedInterval, + 'start', + 'UTC' + ); + return { + ...annotation, + type: isManualPointAnnotation(annotation) ? 'point' : 'range', + timebucket: moment(snappedDate).toISOString(), + }; + }), }; return new Observable((subscriber) => { diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts index 8590a095864e1a..315fea60df8606 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts @@ -9,10 +9,12 @@ import { TimeRange } from '@kbn/data-plugin/common'; import { - EventAnnotationOutput, + ManualEventAnnotationOutput, ManualPointEventAnnotationOutput, ManualRangeEventAnnotationOutput, } from '../manual_event_annotation/types'; +import { QueryPointEventAnnotationOutput } from '../query_event_annotation/types'; +import { EventAnnotationOutput } from '../types'; export const isRangeAnnotation = ( annotation: EventAnnotationOutput @@ -26,6 +28,12 @@ export const isManualPointAnnotation = ( return 'time' in annotation && !('endTime' in annotation); }; +export const isQueryAnnotation = ( + annotation: EventAnnotationOutput +): annotation is QueryPointEventAnnotationOutput => { + return 'query' in annotation; +}; + export const filterOutOfTimeRange = (annotation: EventAnnotationOutput, timerange?: TimeRange) => { if (!timerange) { return false; @@ -38,6 +46,6 @@ export const filterOutOfTimeRange = (annotation: EventAnnotationOutput, timerang } }; -export const sortByTime = (a: EventAnnotationOutput, b: EventAnnotationOutput) => { +export const sortByTime = (a: ManualEventAnnotationOutput, b: ManualEventAnnotationOutput) => { return a.time.localeCompare(b.time); }; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index 5ede7d5233aaf7..178eeb0f30ab5f 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -26,6 +26,10 @@ export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs & type: 'manual_range_event_annotation'; }; +export type ManualEventAnnotationOutput = + | ManualPointEventAnnotationOutput + | ManualRangeEventAnnotationOutput; + export const annotationColumns: DatatableColumn[] = [ { id: 'time', name: 'time', meta: { type: 'string' } }, { id: 'endTime', name: 'endTime', meta: { type: 'string' } }, diff --git a/src/plugins/event_annotation/common/query_event_annotation/index.ts b/src/plugins/event_annotation/common/query_event_annotation/index.ts index f0d768d2bff00a..41bfd0de75cdb8 100644 --- a/src/plugins/event_annotation/common/query_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_event_annotation/index.ts @@ -95,9 +95,9 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition< defaultMessage: `Query to apply for the annotation`, }), }, - additionalFields: { + extraFields: { types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.additionalFields', { + help: i18n.translate('eventAnnotation.queryAnnotation.args.extraFields', { defaultMessage: `Provide additional field names to visualize in the tooltip`, }), multi: true, diff --git a/src/plugins/event_annotation/common/query_event_annotation/types.ts b/src/plugins/event_annotation/common/query_event_annotation/types.ts index 39b0fdd28dbe30..65ceb04606d2b0 100644 --- a/src/plugins/event_annotation/common/query_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/query_event_annotation/types.ts @@ -15,7 +15,7 @@ export type QueryPointEventAnnotationArgs = { textSource?: string; textField?: string; query: ExpressionValueBoxed<'kibana_query', Query>; - additionalFields?: string[]; + extraFields?: string[]; } & PointStyleProps; export type QueryPointEventAnnotationOutput = QueryPointEventAnnotationArgs & { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index c0c36dfb5b40b3..8cc000b0cc5358 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -479,10 +479,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', @@ -526,6 +528,7 @@ describe('xy_visualization', () => { ).toEqual({ layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'newColId' }], }); }); @@ -563,6 +566,7 @@ describe('xy_visualization', () => { ).toEqual({ layerId: 'annotation', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2, exampleAnnotation], }); }); @@ -608,11 +612,13 @@ describe('xy_visualization', () => { { layerId: 'first', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], }, { layerId: 'second', layerType: layerTypes.ANNOTATIONS, + indexPatternId: 'indexPattern1', annotations: [{ ...exampleAnnotation, id: 'an2' }], }, ]); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index f4c375d46e5d27..470489edf1d8e5 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -181,6 +181,7 @@ describe('AnnotationsPanel', () => { panelRef={React.createRef()} /> ); + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); expect(setState).toBeCalledWith({ @@ -193,6 +194,7 @@ describe('AnnotationsPanel', () => { id: 'ann1', isHidden: undefined, label: 'Event range', + type: 'manual', key: { endTimestamp: '2022-03-21T10:49:00.000Z', timestamp: '2022-03-18T08:25:00.000Z', @@ -200,6 +202,7 @@ describe('AnnotationsPanel', () => { }, }, ], + indexPatternId: 'indexPattern1', layerId: 'annotation', layerType: 'annotations', }, @@ -220,8 +223,10 @@ describe('AnnotationsPanel', () => { type: 'point_in_time', }, label: 'Event', + type: 'manual', }, ], + indexPatternId: 'indexPattern1', layerId: 'annotation', layerType: 'annotations', }, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 19ed252721b551..f23ee51b327ba1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -15,7 +15,7 @@ import type { VisualizationLayerWidgetProps, VisualizationType, } from '../../../types'; -import type { State, visualizationTypes, SeriesType, XYAnnotationLayerConfig } from '../types'; +import { State, visualizationTypes, SeriesType, XYAnnotationLayerConfig } from '../types'; import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { ChangeIndexPattern, StaticHeader } from '../../../shared_components'; import { updateLayer } from '.'; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx index b11bb65ad97b7e..3c08843f7c99bb 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx @@ -82,7 +82,7 @@ export interface MarkerDecorationConfig { function getSelectedOption( { textSource, textVisibility }: MarkerDecorationConfig = {}, - isQueryBased: boolean + isQueryBased?: boolean ) { if (!textVisibility) { return 'none'; @@ -103,8 +103,9 @@ export function TextDecorationSetting({ currentConfig?: MarkerDecorationConfig; setConfig: (config: MarkerDecorationConfig) => void; customIconSet?: IconSet; - isQueryBased: boolean; - children: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; + isQueryBased?: boolean; + /** A children render function for custom sub fields on textDecoration change */ + children?: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; }) { const options = [ { @@ -164,7 +165,7 @@ export function TextDecorationSetting({ }} isFullWidth /> - {children(selectedVisibleOption)} + {children?.(selectedVisibleOption)}
); From e3d4cedaad82df1957377f917acff11b7c0f91ad Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 25 Aug 2022 12:37:15 +0200 Subject: [PATCH 47/98] :bug: Fix importing issue --- .../expression_xy/public/components/annotations.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 8a123dce8cdec1..7ec68d01390e4d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -29,7 +29,6 @@ import { defaultAnnotationColor, defaultAnnotationRangeColor, } from '@kbn/event-annotation-plugin/public'; -import { isQueryAnnotation } from '@kbn/event-annotation-plugin/common/fetch_event_annotations/utils'; import { ManualEventAnnotationOutput } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types'; import type { AnnotationLayerArgs, @@ -67,7 +66,7 @@ const groupVisibleConfigsByInterval = ( .flatMap(({ annotations }) => annotations.filter((a) => !a.isHidden && a.type === 'manual_point_event_annotation') ) - .filter((a): a is ManualEventAnnotationOutput => !isQueryAnnotation(a)) + .filter((a): a is ManualEventAnnotationOutput => a.type !== 'query_point_event_annotation') .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()) .reduce>((acc, current) => { const roundedTimestamp = getRoundedTimestamp( From e66340b44f7d942c86a4d40b3be6ada1dea5a1e2 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 25 Aug 2022 12:37:44 +0200 Subject: [PATCH 48/98] :recycle: Make range default color dependant on opint one --- .../public/event_annotation_service/helpers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index 858673c1a14932..ee19bd2744d80e 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -6,14 +6,18 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; +import { transparentize } from '@elastic/eui'; import { euiLightVars } from '@kbn/ui-theme'; +import Color from 'color'; import { EventAnnotationConfig, RangeEventAnnotationConfig, PointInTimeQueryEventAnnotationConfig, } from '../../common'; export const defaultAnnotationColor = euiLightVars.euiColorAccent; -export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 +export const defaultAnnotationRangeColor = new Color( + transparentize(defaultAnnotationColor, 0.1) +).hexa(); export const defaultAnnotationLabel = i18n.translate( 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', From 196bf1dc97308e205320e4ab8a9f4351562a3689 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 25 Aug 2022 12:38:12 +0200 Subject: [PATCH 49/98] :bug: Fix duplicate fields selection in tooltip section --- .../tooltip_annotation_panel.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index f8f34e6ae4d263..c62fb322cc84cd 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -29,6 +29,7 @@ import { } from '../../../../shared_components'; const generateId = htmlIdGenerator(); +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); export interface FieldInputsProps { currentConfig: PointInTimeQueryEventAnnotationConfig; @@ -66,8 +67,12 @@ export function TooltipSection({ }, [setConfig, currentConfig] ); - const wrappedValues = useMemo(() => { - return currentConfig.extraFields?.map((value) => ({ id: generateId(), value })) || []; + const { wrappedValues, rawValuesLookup } = useMemo(() => { + const rawValues = currentConfig.extraFields ?? []; + return { + wrappedValues: rawValues.map((value) => ({ id: generateId(), value })), + rawValuesLookup: new Set(rawValues), + }; }, [currentConfig]); const { inputValue: localValues, handleInputChange } = useDebouncedValue({ @@ -89,7 +94,6 @@ export function TooltipSection({ [localValues, indexPattern, handleInputChange] ); - // diminish attention to adding fields alternative if (localValues.length === 0) { return ( <> @@ -117,7 +121,10 @@ export function TooltipSection({ } const disableActions = localValues.length === 2 && localValues.some(({ isNew }) => isNew); const options = indexPattern.fields - .filter(({ displayName, type }) => displayName && type !== 'document') + .filter( + ({ displayName, type }) => + displayName && !rawValuesLookup.has(displayName) && supportedTypes.has(type) + ) .map( (field) => ({ @@ -131,7 +138,8 @@ export function TooltipSection({ compatible: true, 'data-test-subj': `lnsXY-annotation-tooltip-fieldOption-${field.name}`, } as FieldOption) - ); + ) + .sort((a, b) => a.label.localeCompare(b.label)); return ( <> From 7ffbc5ceaf9bc74a8245db6db148c05640b34fe3 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 25 Aug 2022 15:59:38 +0200 Subject: [PATCH 50/98] :white_check_mark: Add more unit tests --- .../lens/public/indexpattern_service/mocks.ts | 40 +++++++--- x-pack/plugins/lens/public/mocks/index.ts | 45 ++++++----- .../xy/annotations/helpers.test.ts | 65 ++++++++-------- .../annotations_config_panel/index.test.tsx | 76 ++++++++++++++++++- .../query_annotation_panel.tsx | 2 + 5 files changed, 163 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts index 6eb7156a91cffd..34eebf27ba13b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_service/mocks.ts @@ -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 => { + 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) => { return { diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index 0d90e719ebadee..a13d66bb532fb2 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { createMockDataViewsState } from '../indexpattern_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; -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 = {}): 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; -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 = {}): 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), }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.test.ts b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.test.ts index 8560e3bbaec311..d953814621aad6 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.test.ts @@ -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'); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index 470489edf1d8e5..e604f8ab60a9f5 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -17,6 +17,8 @@ import { Position } from '@elastic/charts'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import moment from 'moment'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { createMockDataViewsState } from '../../../../indexpattern_service/mocks'; +import { createMockedIndexPattern } from '../../../../indexpattern_datasource/mocks'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); @@ -59,9 +61,9 @@ describe('AnnotationsPanel', () => { } beforeEach(() => { - frame = createMockFramePublicAPI(); - frame.datasourceLayers = {}; + frame = createMockFramePublicAPI({ datasourceLayers: {} }); }); + describe('Dimension Editor', () => { test('shows correct options for line annotations', () => { const state = testState(); @@ -233,5 +235,75 @@ describe('AnnotationsPanel', () => { ], }); }); + + test('shows correct options for query based', () => { + const state = testState(); + const indexPattern = createMockedIndexPattern(); + state.layers[0] = { + annotations: [ + { + color: 'red', + icon: 'triangle', + id: 'ann1', + type: 'query', + isHidden: undefined, + key: { + field: 'timestamp', + type: 'point_in_time', + }, + label: 'Query based event', + lineStyle: 'dashed', + lineWidth: 3, + query: { query: '', language: 'kql' }, + }, + ], + layerId: 'annotation', + layerType: 'annotations', + indexPatternId: indexPattern.id, + }; + const frameMock = createMockFramePublicAPI({ + datasourceLayers: {}, + dataViews: createMockDataViewsState({ + indexPatterns: { [indexPattern.id]: indexPattern }, + }), + }); + + const component = mount( + + ); + + expect( + component.find('[data-test-subj="annotation-query-based-field-picker"]').exists() + ).toBeTruthy(); + expect( + component.find('[data-test-subj="annotation-query-based-query-input"]').exists() + ).toBeTruthy(); + + // The provided indexPattern has 2 date fields + expect( + component + .find('[data-test-subj="annotation-query-based-field-picker"]') + .at(0) + .prop('options') + ).toHaveLength(2); + // When in query mode a new "field" option is added to the previous 2 ones + expect( + component.find('[data-test-subj="lns-lineMarker-text-visibility"]').at(0).prop('options') + ).toHaveLength(3); + expect( + component.find('[data-test-subj="lnsXY-annotation-tooltip-add_field"]').exists() + ).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 11025e599c65da..d71b0a92a8e508 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -86,6 +86,7 @@ export const ConfigPanelQueryAnnotation = ({ } }} fieldIsInvalid={false} + data-test-subj="annotation-query-based-field-picker" /> {}} + data-test-subj="annotation-query-based-query-input" placeholder={ inputQuery.language === 'kuery' ? i18n.translate('xpack.lens.annotations.query.queryPlaceholderKql', { From 6f1332c1f33cae6d8f7985e9844a246dc71964b7 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 25 Aug 2022 15:59:58 +0200 Subject: [PATCH 51/98] :white_check_mark: Fix broken test --- x-pack/plugins/lens/public/state_management/lens_slice.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index d38c0aed166767..fba5c9c9abd54d 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -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 = { From 85719d3d5af1030e33265b39f21f453d050b9ffd Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 26 Aug 2022 10:31:35 +0200 Subject: [PATCH 52/98] :label: Mute ts error for now --- x-pack/plugins/lens/public/data_views_service/loader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index 89d1765bc61afe..4f87a9030318a1 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -84,6 +84,7 @@ export function convertDataViewIntoLensIndexPattern( Object.fromEntries( Object.entries(fieldFormatMap).map(([id, format]) => [ id, + // @ts-ignore 'toJSON' in format ? format.toJSON() : format, ]) ), From 23da90815bad63ce7416c83a33491ffffbf6af83 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 26 Aug 2022 10:33:22 +0200 Subject: [PATCH 53/98] :white_check_mark: Fix tests --- .../public/event_annotation_service/service.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index bb2165c3c7ab6f..fe72f6d21ab21d 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -163,7 +163,7 @@ describe('Event Annotation Service', () => { chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], - additionalFields: [], + extraFields: [], }, }, ], @@ -262,7 +262,7 @@ describe('Event Annotation Service', () => { chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], - additionalFields: [], + extraFields: [], }, }, ], @@ -318,7 +318,7 @@ describe('Event Annotation Service', () => { chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], - additionalFields: [], + extraFields: [], }, }, ], From 239e0563c1730b219a77bcfaeb15904528e6acae Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 26 Aug 2022 10:47:39 +0200 Subject: [PATCH 54/98] :fire: Reduce plugin weight --- .../public/event_annotation_service/helpers.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index ee19bd2744d80e..ddc130cd057f67 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -6,18 +6,15 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { transparentize } from '@elastic/eui'; import { euiLightVars } from '@kbn/ui-theme'; -import Color from 'color'; import { EventAnnotationConfig, RangeEventAnnotationConfig, PointInTimeQueryEventAnnotationConfig, } from '../../common'; export const defaultAnnotationColor = euiLightVars.euiColorAccent; -export const defaultAnnotationRangeColor = new Color( - transparentize(defaultAnnotationColor, 0.1) -).hexa(); +// 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( 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', From ecd34162f65b1126571e1db3d1dcb4710af5d6fb Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 26 Aug 2022 11:41:10 +0200 Subject: [PATCH 55/98] :bug: prevent layout shift on panel open --- .../annotations_config_panel/query_annotation_panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index d71b0a92a8e508..f712d34f4406a9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -102,6 +102,7 @@ export const ConfigPanelQueryAnnotation = ({ onChange={function (input: Query): void { onChange({ query: input }); }} + disableAutoFocus indexPatternTitle={frame.dataViews.indexPatterns[layer.indexPatternId].title} isInvalid={false} onSubmit={() => {}} From ba084c236ebf3e9c7ed7f2f2dde542daaa12bf32 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 26 Aug 2022 20:05:04 +0200 Subject: [PATCH 56/98] :bug: Fix extract + inject visualization references --- x-pack/plugins/lens/public/app_plugin/app.tsx | 1 + .../lens/public/app_plugin/lens_top_nav.tsx | 7 +++ .../plugins/lens/public/app_plugin/types.ts | 1 + .../buttons/drop_targets_utils.tsx | 4 +- .../editor_frame/data_panel_wrapper.tsx | 58 ++++++++++++------ .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/state_helpers.ts | 33 +++++++++- .../init_middleware/load_initial.ts | 61 +++++++------------ .../lens/public/state_management/selectors.ts | 15 ++++- x-pack/plugins/lens/public/types.ts | 5 +- x-pack/plugins/lens/public/utils.ts | 26 +++----- .../public/visualizations/xy/state_helpers.ts | 17 ++++-- .../lens/public/visualizations/xy/types.ts | 2 + .../visualizations/xy/visualization.tsx | 9 ++- 14 files changed, 149 insertions(+), 91 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7ff105adf43f75..06953b91efa58d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -392,6 +392,7 @@ export function App({ setHeaderActionMenu={setHeaderActionMenu} indicateNoData={indicateNoData} datasourceMap={datasourceMap} + visualizationMap={visualizationMap} title={persistedDoc?.title} lensInspector={lensInspector} goBackToOriginatingApp={goBackToOriginatingApp} diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 88643b52aab776..332ac977a5020e 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -210,6 +210,7 @@ export const LensTopNavMenu = ({ onAppLeave, redirectToOrigin, datasourceMap, + visualizationMap, title, goBackToOriginatingApp, contextOriginatingApp, @@ -310,6 +311,10 @@ export const LensTopNavMenu = ({ {} ), datasourceStates, + visualizationState: visualization.state, + activeVisualization: visualization.activeId + ? visualizationMap[visualization.activeId] + : undefined, }); const hasIndexPatternsChanged = indexPatterns.length + rejectedIndexPatterns.length !== indexPatternIds.length || @@ -334,6 +339,8 @@ export const LensTopNavMenu = ({ activeDatasourceId, rejectedIndexPatterns, datasourceMap, + visualizationMap, + visualization, indexPatterns, dataViewsService, ]); diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index be49ca768acdc3..1380b70a9a3428 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -101,6 +101,7 @@ export interface LensTopNavMenuProps { setIsSaveModalVisible: React.Dispatch>; runSave: RunSave; datasourceMap: DatasourceMap; + visualizationMap: VisualizationMap; title?: string; lensInspector: LensInspector; goBackToOriginatingApp?: () => void; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index 8ce2a4c0cc9750..5f3fd2d4a73b5b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -196,9 +196,9 @@ export interface OnVisDropProps { group?: VisualizationDimensionGroupConfig; } -export function onDropForVisualization( +export function onDropForVisualization( props: OnVisDropProps, - activeVisualization: Visualization + activeVisualization: Visualization ) { const { prevState, target, frame, dropType, source, group } = props; const { layerId, columnId, groupId } = target; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 553a61ba2c1c55..90dbdedd856480 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -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,6 +33,7 @@ import { selectExecutionContext, selectActiveDatasourceId, selectDatasourceStates, + selectVisualizationState, } from '../../state_management'; import { initializeSources } from './state_helpers'; import type { IndexPatternServiceAPI } from '../../indexpattern_service/service'; @@ -35,6 +42,7 @@ 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, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index f2cef399b49e88..cc0db7073306f7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -103,6 +103,7 @@ export function EditorFrame(props: EditorFrameProps) { core={props.core} plugins={props.plugins} datasourceMap={datasourceMap} + visualizationMap={visualizationMap} showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index cfb3e5d96dd9c6..7c446ade5a009a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -36,7 +36,7 @@ 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'; @@ -137,6 +137,8 @@ export async function initializeSources( { dataViews, datasourceMap, + visualizationMap, + visualizationState, datasourceStates, storage, defaultIndexPatternId, @@ -145,6 +147,8 @@ export async function initializeSources( }: { dataViews: DataViewsContract; datasourceMap: DatasourceMap; + visualizationMap: VisualizationMap; + visualizationState: VisualizationState; datasourceStates: DatasourceStates; defaultIndexPatternId: string; storage: IStorageWrapper; @@ -168,7 +172,7 @@ export async function initializeSources( return { indexPatterns, indexPatternRefs, - states: initializeDatasources({ + datasourceStates: initializeDatasources({ datasourceMap, datasourceStates, initialContext, @@ -176,9 +180,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, diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index dcedee1616f80c..ce33e94e24ae0e 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -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, ...loaderSharedArgs, @@ -125,14 +133,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]: { @@ -157,40 +165,6 @@ export function loadInitial( }); redirectCallback(); }); - // return initializeDatasources(datasourceMap, lens.datasourceStates, undefined, initialContext, { - // isFullEditor: true, - // }) - // .then((result) => { - // store.dispatch( - // initEmpty({ - // newState: { - // ...emptyState, - // searchSessionId: data.search.session.getSessionId() || data.search.session.start(), - // datasourceStates: Object.entries(result).reduce( - // (state, [datasourceId, datasourceState]) => ({ - // ...state, - // [datasourceId]: { - // ...datasourceState, - // isLoading: false, - // }, - // }), - // {} - // ), - // isLoading: false, - // }, - // initialContext, - // }) - // ); - // if (autoApplyDisabled) { - // store.dispatch(disableAutoApply()); - // } - // }) - // .catch((e: { message: string }) => { - // notifications.toasts.addDanger({ - // title: e.message, - // }); - // redirectCallback(); - // }); } return getPersisted({ initialInput, lensServices, history }) @@ -220,9 +194,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, initialContext, @@ -232,7 +213,7 @@ export function loadInitial( }, { isFullEditor: true } ) - .then(({ states, indexPatterns, indexPatternRefs }) => { + .then(({ datasourceStates, visualizationState, indexPatterns, indexPatternRefs }) => { const currentSessionId = data.search.session.getSessionId(); store.dispatch( setState({ @@ -251,10 +232,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]: { diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index f1f53197978fa9..64550e65d4edd8 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -25,6 +25,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; @@ -85,7 +86,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] @@ -113,6 +116,14 @@ export const selectSavedObjectFormat = createSelector( references.push(...savedObjectReferences); }); + let persistibleVisualizationState = visualization.state; + if (activeVisualization.getPersistableState) { + const { state: persistableState, savedObjectReferences } = + activeVisualization.getPersistableState(visualization.state); + persistibleVisualizationState = persistableState; + references.push(...savedObjectReferences); + } + const { state: persistableFilters, references: filterReferences } = extractFilterReferences(filters); @@ -126,7 +137,7 @@ export const selectSavedObjectFormat = createSelector( type: 'lens', references, state: { - visualization: visualization.state, + visualization: persistibleVisualizationState, query, filters: persistableFilters, datasourceStates: persistibleDatasourceStates, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 74763ffeea4bf8..92a84fb699ec25 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -869,7 +869,7 @@ export interface Visualization { /** * 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; @@ -907,7 +907,8 @@ export interface Visualization { 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 */ diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index cb3d7d491f038a..82774024d00687 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -114,29 +114,16 @@ export async function refreshIndexPatternsList({ }); } -// export function refreshIndexPatternsList({ -// activeDatasources, -// indexPatternId, -// setDatasourceState, -// }: { -// activeDatasources: Record; -// indexPatternId: string; -// setDatasourceState: StateSetter; -// }): void { -// Object.entries(activeDatasources).forEach(([id, datasource]) => { -// datasource?.refreshIndexPatternsList?.({ -// indexPatternId, -// setState: setDatasourceState, -// }); -// }); -// } - export function getIndexPatternsIds({ activeDatasources, datasourceStates, + visualizationState, + activeVisualization, }: { activeDatasources: Record; datasourceStates: DatasourceStates; + visualizationState: unknown; + activeVisualization?: Visualization; }): string[] { let currentIndexPatternId: string | undefined; const references: SavedObjectReference[] = []; @@ -146,6 +133,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); diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts index 06dcd4ede59482..6c1a287890a5f7 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts @@ -110,10 +110,10 @@ function getLayerReferenceName(layerId: string) { return `xy-visualization-layer-${layerId}`; } -export function extractReferences({ layers }: XYState) { +export function extractReferences(state: XYState) { const savedObjectReferences: SavedObjectReference[] = []; const persistableLayers: Array> = []; - layers.forEach((layer) => { + state.layers.forEach((layer) => { if (isAnnotationsLayer(layer)) { const { indexPatternId, ...persistableLayer } = layer; savedObjectReferences.push({ @@ -126,14 +126,21 @@ export function extractReferences({ layers }: XYState) { persistableLayers.push(layer); } }); - return { savedObjectReferences, state: { layers: persistableLayers } }; + return { savedObjectReferences, state: { ...state, layers: persistableLayers } }; } -export function injectReferences(state: XYPersistedState, references: SavedObjectReference[]) { +export function injectReferences( + state: XYPersistedState, + references?: SavedObjectReference[] +): XYState { + if (!references || !references.length) { + return state as XYState; + } return { + ...state, layers: state.layers.map((layer) => { if (!isAnnotationsLayer(layer)) { - return layer; + return layer as XYLayerConfig; } return { ...layer, diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index bd53346f06b648..51602fc222adb3 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -165,6 +165,8 @@ export type XYPersistedState = Omit & { layers: Array>; }; +export type PersistedState = XYPersistedState; + const groupLabelForBar = i18n.translate('xpack.lens.xyVisualization.barGroupLabel', { defaultMessage: 'Bar', }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index d1abb794d2e531..abcf29aa531696 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -33,9 +33,10 @@ import { YConfig, YAxisMode, SeriesType, + PersistedState, } from './types'; import { layerTypes } from '../../../common'; -import { extractReferences, isHorizontalChart } from './state_helpers'; +import { extractReferences, injectReferences, isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { getAccessorColorConfigs, getColorAssignments } from './color_assignment'; import { getColumnToLabelMap } from './state_helpers'; @@ -98,7 +99,7 @@ export const getXyVisualization = ({ fieldFormats: FieldFormatsStart; useLegacyTimeAxis: boolean; kibanaTheme: ThemeServiceStart; -}): Visualization => ({ +}): Visualization => ({ id: XY_ID, visualizationTypes, getVisualizationTypeId(state) { @@ -157,6 +158,10 @@ export const getXyVisualization = ({ return extractReferences(state); }, + fromPersistableState(state, references) { + return injectReferences(state, references); + }, + getDescription, switchVisualizationType(seriesType: string, state: State) { From 9e0ec88c88eaf30083d62f8793e14b89b78e6c35 Mon Sep 17 00:00:00 2001 From: dej611 Date: Sun, 28 Aug 2022 10:03:37 +0200 Subject: [PATCH 57/98] :label: fix type issues --- .../editor_frame/data_panel_wrapper.test.tsx | 3 ++- x-pack/plugins/lens/public/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx index 63e4f88d6d2346..648b989f65e8f7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx @@ -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'; @@ -32,6 +32,7 @@ describe('Data Panel Wrapper', () => { const mountResult = await mountWithProvider( {}} core={{} as DatasourceDataPanelProps['core']} dropOntoWorkspace={(field: DragDropIdentifier) => {}} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 92a84fb699ec25..f2bc65858b6d8c 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -108,8 +108,8 @@ export interface EditorFrameSetup { registerDatasource: ( datasource: Datasource | (() => Promise>) ) => void; - registerVisualization: ( - visualization: Visualization | (() => Promise>) + registerVisualization: ( + visualization: Visualization | (() => Promise>) ) => void; } From a7161f9bb37146b1d08bdc844133b920b5088cfa Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 31 Aug 2022 11:07:18 +0200 Subject: [PATCH 58/98] :sparkles: Add dataview reference migration for annotations --- .../server/migrations/common_migrations.ts | 27 +++++++++++++++++++ .../saved_object_migrations.test.ts | 14 +++++++++- .../migrations/saved_object_migrations.ts | 11 +++++++- .../plugins/lens/server/migrations/types.ts | 9 ++++++- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index d59e82120a5e73..483e86bcf49989 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -13,6 +13,7 @@ import { MigrateFunction, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; +import { SavedObjectReference } from '@kbn/core-saved-objects-common'; import { LensDocShapePre712, OperationTypePre712, @@ -435,6 +436,32 @@ export const commonExplicitAnnotationType = ( }; }; +export const commonAnnotationAddDataViewIdReferences = ( + attributes: LensDocShape840, + references: SavedObjectReference[] | undefined +): SavedObjectReference[] | undefined => { + if ( + !references || + references.every(({ type }) => type !== 'index-pattern') || + attributes.visualizationType !== 'lnsXY' || + attributes.state.visualization.layers.every((l) => l.layerType !== 'annotations') + ) { + return references; + } + const dataViewRef = references.find(({ type }) => type === 'index-pattern'); + if (!dataViewRef) { + return references; + } + const annotationReferences: SavedObjectReference[] = attributes.state.visualization.layers + .filter(({ layerType }) => layerType === 'annotations') + .map(({ layerId }) => ({ + type: 'index-pattern', + id: dataViewRef.id, + name: `xy-visualization-layer-${layerId}`, + })); + return [...references, ...annotationReferences]; +}; + export const commonMigrateMetricIds = ( attributes: LensDocShape840 ): LensDocShape840 => { diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 3dfb1030bbafd8..25b56f31270ff7 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -2236,7 +2236,7 @@ describe('Lens migrations', () => { }); }); - describe('8.5.0 Add Annotation event type', () => { + describe('8.5.0 Add Annotation event type and dataView references', () => { const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { type: 'lens', @@ -2272,6 +2272,18 @@ describe('Lens migrations', () => { annotations: [{ id: 'annotation-id', type: 'manual' }], }); }); + + it('adds dataView references for annotation layers', () => { + const result = migrations['8.5.0']( + { + ...example, + references: [{ id: 'dataViewId', type: 'index-pattern', name: 'datasource1' }], + }, + context + ) as ReturnType>; + // A new reference has been added for the annotation layer + expect(result.references).toHaveLength(2); + }); }); describe('8.5.0 migrates metric IDs', () => { diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index d4d21b480ddd5f..4f64069027e001 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -55,6 +55,7 @@ import { commonPreserveOldLegendSizeDefault, commonExplicitAnnotationType, commonMigrateMetricIds, + commonAnnotationAddDataViewIdReferences, } from './common_migrations'; interface LensDocShapePre710 { @@ -523,6 +524,14 @@ const addEventAnnotationType: SavedObjectMigrationFn< return { ...newDoc, attributes: commonExplicitAnnotationType(newDoc.attributes) }; }; +const addEventAnnotationDataViewReferences: SavedObjectMigrationFn< + LensDocShape840, + LensDocShape840 +> = (doc) => ({ + ...doc, + references: commonAnnotationAddDataViewIdReferences(doc.attributes, doc.references), +}); + const migrateMetricIds: SavedObjectMigrationFn = (doc) => ({ ...doc, attributes: commonMigrateMetricIds(doc.attributes), @@ -548,7 +557,7 @@ const lensMigrations: SavedObjectMigrationMap = { enhanceTableRowHeight ), '8.3.0': flow(lockOldMetricVisSettings, preserveOldLegendSizeDefault, fixValueLabelsInXY), - '8.5.0': flow(migrateMetricIds, addEventAnnotationType), + '8.5.0': flow(migrateMetricIds, addEventAnnotationType, addEventAnnotationDataViewReferences), }; export const getAllMigrations = ( diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index d09d29b25df11b..f39eba9f9c9c98 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -273,18 +273,25 @@ export type VisState830 = XYVisualizationState830; export interface XYVisStatePre840 { layers: Array< | { + layerId: string; layerType: Exclude; } - | { layerType: Extract; annotations: Array<{ id: string }> } + | { + layerId: string; + layerType: Extract; + annotations: Array<{ id: string }>; + } >; } export interface XYVisState840 { layers: Array< | { + layerId: string; layerType: Exclude; } | { + layerId: string; layerType: Extract; annotations: Array<{ id: string; type: 'manual' | 'query' }>; } From 49d517323419e18fd76eae5c261e98d1ca41ff48 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 31 Aug 2022 11:15:08 +0200 Subject: [PATCH 59/98] :wrench: Add migration to embedadble --- .../server/embeddable/make_lens_embeddable_factory.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index 11a69b9aa0891c..22ece242acc3bd 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -11,6 +11,7 @@ import { mergeMigrationFunctionMaps, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; +import { SavedObjectReference } from '@kbn/core-saved-objects-common'; import { DOC_TYPE } from '../../common'; import { commonEnhanceTableRowHeight, @@ -29,6 +30,7 @@ import { getLensFilterMigrations, commonExplicitAnnotationType, commonMigrateMetricIds, + commonAnnotationAddDataViewIdReferences, } from '../migrations/common_migrations'; import { CustomVisualizationMigrations, @@ -132,15 +134,21 @@ export const makeLensEmbeddableFactory = '8.5.0': (state) => { const lensState = state as unknown as { attributes: LensDocShape840; + references: SavedObjectReference[] | undefined; }; let migratedLensState = commonMigrateMetricIds( lensState.attributes ) as LensDocShape840; migratedLensState = commonExplicitAnnotationType(migratedLensState); + const migratedReferences = commonAnnotationAddDataViewIdReferences( + migratedLensState as LensDocShape840, + lensState.references + ); return { ...lensState, attributes: migratedLensState, + references: migratedReferences, } as unknown as SerializableRecord; }, }), From a5e4a1e658d78cec21a88be7aece009f031a0980 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 31 Aug 2022 12:09:17 +0200 Subject: [PATCH 60/98] :label: Fix type export --- src/plugins/event_annotation/common/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 97c854991f7257..c763132f76c369 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -16,7 +16,6 @@ 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'; @@ -25,6 +24,7 @@ export type { EventAnnotationGroupArgs } from './event_annotation_group'; export type { FetchEventAnnotationsArgs } from './fetch_event_annotations/types'; export type { EventAnnotationConfig, + EventAnnotationArgs, RangeEventAnnotationConfig, PointInTimeEventAnnotationConfig, QueryPointEventAnnotationConfig, From af7951089b51eb83b3056fde37c32c83e1e40211 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 31 Aug 2022 13:03:04 +0200 Subject: [PATCH 61/98] :bug: Fix more conflicts with main --- .../public/components/annotations.tsx | 2 - .../common/query_event_annotation/index.ts | 112 ------------------ .../common/query_event_annotation/types.ts | 23 ---- .../query_point_event_annotation/index.ts | 1 - .../event_annotation_service/helpers.ts | 2 +- .../annotations_panel.tsx | 5 + .../manual_annotation_panel.tsx | 4 +- .../query_annotation_panel.tsx | 14 +-- .../range_annotation_panel.tsx | 4 +- .../shared/marker_decoration_settings.tsx | 23 ++-- 10 files changed, 30 insertions(+), 160 deletions(-) delete mode 100644 src/plugins/event_annotation/common/query_event_annotation/index.ts delete mode 100644 src/plugins/event_annotation/common/query_event_annotation/types.ts diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 4b1b0e77150daa..dcbe5381c9927f 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -30,7 +30,6 @@ import { defaultAnnotationColor, defaultAnnotationRangeColor, } from '@kbn/event-annotation-plugin/public'; -import { ManualEventAnnotationOutput } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types'; import type { AnnotationLayerArgs, CommonXYAnnotationLayerConfig, @@ -70,7 +69,6 @@ const groupVisibleConfigsByInterval = ( !a.isHidden && a.type === 'manual_point_event_annotation' ) ) - .filter((a): a is ManualEventAnnotationOutput => a.type !== 'query_point_event_annotation') .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()) .reduce>((acc, current) => { const roundedTimestamp = getRoundedTimestamp( diff --git a/src/plugins/event_annotation/common/query_event_annotation/index.ts b/src/plugins/event_annotation/common/query_event_annotation/index.ts deleted file mode 100644 index 41bfd0de75cdb8..00000000000000 --- a/src/plugins/event_annotation/common/query_event_annotation/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { AvailableAnnotationIcons } from '../constants'; -import type { QueryPointEventAnnotationArgs, QueryPointEventAnnotationOutput } from './types'; - -export const queryPointEventAnnotation: ExpressionFunctionDefinition< - 'query_point_event_annotation', - null, - QueryPointEventAnnotationArgs, - QueryPointEventAnnotationOutput -> = { - name: 'query_point_event_annotation', - aliases: [], - type: 'query_point_event_annotation', - help: i18n.translate('eventAnnotation.queryAnnotation.description', { - defaultMessage: `Configure query annotation`, - }), - inputTypes: ['null'], - args: { - field: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.field', { - defaultMessage: `The time field chosen for annotation`, - }), - }, - label: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.label', { - defaultMessage: `The name of the annotation`, - }), - }, - color: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.color', { - defaultMessage: 'The color of the line', - }), - }, - lineStyle: { - types: ['string'], - options: ['solid', 'dotted', 'dashed'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.lineStyle', { - defaultMessage: 'The style of the annotation line', - }), - }, - lineWidth: { - types: ['number'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.lineWidth', { - defaultMessage: 'The width of the annotation line', - }), - }, - icon: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.icon', { - defaultMessage: 'An optional icon used for annotation lines', - }), - options: [...Object.values(AvailableAnnotationIcons)], - strict: true, - }, - textVisibility: { - types: ['boolean'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.textVisibility', { - defaultMessage: 'Visibility of the label on the annotation line', - }), - }, - textSource: { - types: ['string'], - options: ['name', 'field'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.textSource', { - defaultMessage: `Source of annotation label`, - }), - }, - textField: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.textField', { - defaultMessage: `Field name used for the annotation label`, - }), - }, - isHidden: { - types: ['boolean'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.isHidden', { - defaultMessage: `Switch to hide annotation`, - }), - }, - query: { - types: ['kibana_query'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.query', { - defaultMessage: `Query to apply for the annotation`, - }), - }, - extraFields: { - types: ['string'], - help: i18n.translate('eventAnnotation.queryAnnotation.args.extraFields', { - defaultMessage: `Provide additional field names to visualize in the tooltip`, - }), - multi: true, - }, - }, - fn: function fn(input: unknown, args: QueryPointEventAnnotationArgs) { - return { - type: 'query_point_event_annotation', - ...args, - }; - }, -}; diff --git a/src/plugins/event_annotation/common/query_event_annotation/types.ts b/src/plugins/event_annotation/common/query_event_annotation/types.ts deleted file mode 100644 index 65ceb04606d2b0..00000000000000 --- a/src/plugins/event_annotation/common/query_event_annotation/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { Query } from '@kbn/es-query'; -import { ExpressionValueBoxed } from '@kbn/expressions-plugin/common'; -import { PointStyleProps } from '../types'; - -export type QueryPointEventAnnotationArgs = { - field: string; - textSource?: string; - textField?: string; - query: ExpressionValueBoxed<'kibana_query', Query>; - extraFields?: string[]; -} & PointStyleProps; - -export type QueryPointEventAnnotationOutput = QueryPointEventAnnotationArgs & { - type: 'query_point_event_annotation'; -}; diff --git a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts index 15c59235f7f531..b7bc03df03043f 100644 --- a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts @@ -31,7 +31,6 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition< help: i18n.translate('eventAnnotation.queryAnnotation.args.filter', { defaultMessage: `Annotation filter`, }), - required: true, }, extraFields: { multi: true, diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index 2c7ca0ab820a4a..afe64a5a47eb2e 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -39,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'); }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx index f84b4df0f106f5..2d21d64ca78b33 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -134,6 +134,11 @@ export const AnnotationsPanel = ( onChange={(id) => { setAnnotations({ type: id === `lens_xyChart_annotation_query` ? 'query' : 'manual', + // when switching to query, reset the key value + key: + !isQueryBased && id === `lens_xyChart_annotation_query` + ? { type: 'point_in_time' } + : currentAnnotation?.key, }); }} isFullWidth diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx index f6db9415696b3f..33b04b17b1a2fd 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx @@ -6,7 +6,7 @@ */ import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import { isRangeAnnotation } from '@kbn/event-annotation-plugin/public'; +import { isRangeAnnotationConfig } from '@kbn/event-annotation-plugin/public'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; @@ -31,7 +31,7 @@ export const ConfigPanelManualAnnotation = ({ frame: FramePublicAPI; state: XYState; }) => { - const isRange = isRangeAnnotation(annotation); + const isRange = isRangeAnnotationConfig(annotation); return ( <> {isRange ? ( diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index f712d34f4406a9..53ccc8ab57761d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -7,7 +7,7 @@ import { EuiFormRow } from '@elastic/eui'; import type { Query } from '@kbn/data-plugin/common'; -import type { PointInTimeQueryEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import type { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { QueryInput } from '../../../../indexpattern_datasource/query_input'; @@ -32,13 +32,13 @@ export const ConfigPanelQueryAnnotation = ({ onChange, layer, }: { - annotation?: PointInTimeQueryEventAnnotationConfig; - onChange: (annotations: Partial | undefined) => void; + annotation?: QueryPointEventAnnotationConfig; + onChange: (annotations: Partial | undefined) => void; frame: FramePublicAPI; state: XYState; layer: XYAnnotationLayerConfig; }) => { - const inputQuery = annotation?.query ?? defaultQuery; + const inputQuery = annotation?.filter ?? defaultQuery; const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; // list only supported field by operation, remove the rest const options = currentIndexPattern.fields @@ -57,7 +57,7 @@ export const ConfigPanelQueryAnnotation = ({ } as FieldOption; }); - const selectedField = annotation?.key.field; + const selectedField = annotation?.timeField; return ( <> { - const isRange = isRangeAnnotation(annotation); + const isRange = isRangeAnnotationConfig(annotation); return ( { icon?: T; iconPosition?: IconPosition; textVisibility?: boolean; - textSource?: 'name' | 'field'; + textField?: string; } function getSelectedOption( - { textSource, textVisibility }: MarkerDecorationConfig = {}, + { textField, textVisibility }: MarkerDecorationConfig = {}, isQueryBased?: boolean ) { if (!textVisibility) { return 'none'; } - if (!isQueryBased && textSource === 'field') { - return 'name'; + if (isQueryBased && textField) { + return 'field'; } - return textSource ?? 'name'; + return 'name'; } export function TextDecorationSetting({ @@ -107,6 +107,11 @@ export function TextDecorationSetting({ /** A children render function for custom sub fields on textDecoration change */ children?: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; }) { + // To model the temporary state for label based on field when user didn't pick up the field yet, + // use a local state + const [selectedVisibleOption, setVisibleOption] = useState<'none' | 'name' | 'field'>( + getSelectedOption(currentConfig, isQueryBased) + ); const options = [ { id: `${idPrefix}none`, @@ -133,8 +138,6 @@ export function TextDecorationSetting({ }); } - // Override the only conflictual scenario - const selectedVisibleOption = getSelectedOption(currentConfig, isQueryBased); return ( ({ idSelected={ !currentConfig?.textVisibility ? `${idPrefix}none` - : `${idPrefix}${selectedVisibleOption ?? 'name'}` + : `${idPrefix}${selectedVisibleOption}` } onChange={(id) => { setConfig({ textVisibility: id !== `${idPrefix}none`, - textSource: id.replace(idPrefix, '') as 'name' | 'field', }); + setVisibleOption(id.replace(idPrefix, '') as 'none' | 'name' | 'field'); }} isFullWidth /> From 383b1afeddf647cef56e2448c0156607e73a3935 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 11:54:05 +0200 Subject: [PATCH 62/98] :white_check_mark: Fix tests --- .../event_annotation_service/service.test.ts | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index fe72f6d21ab21d..24a9745ebc1ce9 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -46,13 +46,14 @@ describe('Event Annotation Service', () => { { id: 'myEvent', type: 'query', + timeField: '@timestamp', key: { type: 'point_in_time', - field: '@timestamp', }, label: 'Hello Range', isHidden: true, - query: { query: '', language: 'kql' }, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + textField: '', }, ]) ).toEqual([]); @@ -78,6 +79,7 @@ describe('Event Annotation Service', () => { type: 'function', function: 'manual_point_event_annotation', arguments: { + id: ['myEvent'], time: ['2022'], label: ['Hello'], color: ['#f04e98'], @@ -114,6 +116,7 @@ describe('Event Annotation Service', () => { type: 'function', function: 'manual_range_event_annotation', arguments: { + id: ['myEvent'], time: ['2021'], endTime: ['2022'], label: ['Hello'], @@ -132,12 +135,13 @@ describe('Event Annotation Service', () => { { id: 'myEvent', type: 'query', + timeField: '@timestamp', key: { type: 'point_in_time', - field: '@timestamp', }, label: 'Hello', - query: { query: '', language: 'kql' }, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + textField: '', }, ]) ).toEqual([ @@ -148,7 +152,8 @@ describe('Event Annotation Service', () => { type: 'function', function: 'query_point_event_annotation', arguments: { - field: ['@timestamp'], + id: ['myEvent'], + timeField: ['@timestamp'], label: ['Hello'], color: ['#f04e98'], lineWidth: [1], @@ -157,10 +162,18 @@ describe('Event Annotation Service', () => { textVisibility: [false], textField: [], isHidden: [false], - query: [ + filter: [ { + chain: [ + { + arguments: { + q: [''], + }, + function: 'kql', + type: 'function', + }, + ], type: 'expression', - chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], extraFields: [], @@ -195,12 +208,13 @@ describe('Event Annotation Service', () => { { id: 'myEvent', type: 'query', + timeField: '@timestamp', key: { type: 'point_in_time', - field: '@timestamp', }, label: 'Hello', - query: { query: '', language: 'kql' }, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + textField: '', }, ]) ).toEqual([ @@ -211,6 +225,7 @@ describe('Event Annotation Service', () => { type: 'function', function: 'manual_point_event_annotation', arguments: { + id: ['myEvent'], time: ['2022'], label: ['Hello'], color: ['#f04e98'], @@ -230,6 +245,7 @@ describe('Event Annotation Service', () => { type: 'function', function: 'manual_range_event_annotation', arguments: { + id: ['myRangeEvent'], time: ['2021'], endTime: ['2022'], label: ['Hello Range'], @@ -247,7 +263,8 @@ describe('Event Annotation Service', () => { type: 'function', function: 'query_point_event_annotation', arguments: { - field: ['@timestamp'], + id: ['myEvent'], + timeField: ['@timestamp'], label: ['Hello'], color: ['#f04e98'], lineWidth: [1], @@ -256,10 +273,18 @@ describe('Event Annotation Service', () => { textVisibility: [false], textField: [], isHidden: [false], - query: [ + filter: [ { + chain: [ + { + arguments: { + q: [''], + }, + function: 'kql', + type: 'function', + }, + ], type: 'expression', - chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], extraFields: [], @@ -270,28 +295,26 @@ describe('Event Annotation Service', () => { ]); }); it.each` - textSource | textField | expected - ${''} | ${''} | ${''} - ${'name'} | ${''} | ${''} - ${'name'} | ${'myField'} | ${''} - ${'field'} | ${''} | ${''} - ${'field'} | ${'myField'} | ${'myField'} + textVisibility | textField | expected + ${'true'} | ${''} | ${''} + ${'false'} | ${''} | ${''} + ${'true'} | ${'myField'} | ${'myField'} + ${'false'} | ${''} | ${''} `( - "should handle correctly textVisibility when textSource is set to '$textSource' and textField to '$textField'", - ({ textSource, textField, expected }) => { + "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', - field: '@timestamp', }, label: 'Hello', - query: { query: '', language: 'kql' }, - textVisibility: true, - textSource, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + textVisibility, textField, }, ]) @@ -303,19 +326,28 @@ describe('Event Annotation Service', () => { type: 'function', function: 'query_point_event_annotation', arguments: { - field: ['@timestamp'], + id: ['myEvent'], + timeField: ['@timestamp'], label: ['Hello'], color: ['#f04e98'], lineWidth: [1], lineStyle: ['solid'], icon: ['triangle'], - textVisibility: [true], + textVisibility: [textVisibility], textField: expected ? [expected] : [], isHidden: [false], - query: [ + filter: [ { + chain: [ + { + arguments: { + q: [''], + }, + function: 'kql', + type: 'function', + }, + ], type: 'expression', - chain: [{ arguments: { q: ['""'] }, function: 'lucene', type: 'function' }], }, ], extraFields: [], From 4750aa2a0d02ed7262c2d0282388edb55553614e Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 12:21:03 +0200 Subject: [PATCH 63/98] :label: Make textField optional --- src/plugins/event_annotation/common/types.ts | 2 +- .../public/event_annotation_service/service.test.ts | 3 --- .../xy_config_panel/annotations_config_panel/index.test.tsx | 4 ++-- .../annotations_config_panel/tooltip_annotation_panel.tsx | 6 +++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 940b3a0f278398..5bb39f60d69354 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -70,7 +70,7 @@ export type QueryPointEventAnnotationConfig = { type: QueryAnnotationType; filter: KibanaQueryOutput; timeField: string; - textField: string; + textField?: string; extraFields?: string[]; key: { type: 'point_in_time'; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index 24a9745ebc1ce9..64bbf050a6dd1e 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -53,7 +53,6 @@ describe('Event Annotation Service', () => { label: 'Hello Range', isHidden: true, filter: { type: 'kibana_query', query: '', language: 'kuery' }, - textField: '', }, ]) ).toEqual([]); @@ -141,7 +140,6 @@ describe('Event Annotation Service', () => { }, label: 'Hello', filter: { type: 'kibana_query', query: '', language: 'kuery' }, - textField: '', }, ]) ).toEqual([ @@ -214,7 +212,6 @@ describe('Event Annotation Service', () => { }, label: 'Hello', filter: { type: 'kibana_query', query: '', language: 'kuery' }, - textField: '', }, ]) ).toEqual([ diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index e604f8ab60a9f5..9d0a73c22c2ee2 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -247,14 +247,14 @@ describe('AnnotationsPanel', () => { id: 'ann1', type: 'query', isHidden: undefined, + timeField: 'timestamp', key: { - field: 'timestamp', type: 'point_in_time', }, label: 'Query based event', lineStyle: 'dashed', lineWidth: 3, - query: { query: '', language: 'kql' }, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, }, ], layerId: 'annotation', diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index c62fb322cc84cd..764afc70d365f1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -14,9 +14,9 @@ import { EuiFlexItem, EuiIcon, } from '@elastic/eui'; -import type { PointInTimeQueryEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; +import { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import type { ExistingFieldsMap, IndexPattern } from '../../../../types'; import { fieldExists, @@ -32,8 +32,8 @@ const generateId = htmlIdGenerator(); const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); export interface FieldInputsProps { - currentConfig: PointInTimeQueryEventAnnotationConfig; - setConfig: (config: PointInTimeQueryEventAnnotationConfig) => void; + currentConfig: QueryPointEventAnnotationConfig; + setConfig: (config: QueryPointEventAnnotationConfig) => void; indexPattern: IndexPattern; existingFields: ExistingFieldsMap; invalidFields?: string[]; From b75c79c83843214f207839e73ccaa52cc059dac2 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 14:55:51 +0200 Subject: [PATCH 64/98] :recycle: Refactor query input to be a shared component --- .../dimension_panel/filtering.tsx | 4 +-- .../definitions/filters/filter_popover.tsx | 4 +-- .../definitions/filters/filters.tsx | 24 -------------- .../lens/public/shared_components/index.ts | 1 + .../shared_components/query_input/helpers.ts | 33 +++++++++++++++++++ .../shared_components/query_input/index.ts | 9 +++++ .../query_input}/query_input.tsx | 2 +- .../query_annotation_panel.tsx | 11 +++++-- 8 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/lens/public/shared_components/query_input/helpers.ts create mode 100644 x-pack/plugins/lens/public/shared_components/query_input/index.ts rename x-pack/plugins/lens/public/{indexpattern_datasource => shared_components/query_input}/query_input.tsx (97%) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx index 5117e935c59d62..1c68844079fa63 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx @@ -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', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index 95021dd22900ff..a231fd0a5da6cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -10,10 +10,10 @@ import './filter_popover.scss'; import React from 'react'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; +import { QueryInput, isQueryValid } 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, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index f3dc6490865c33..9cc0cf26751330 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -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'; @@ -58,29 +57,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: { diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index a4e6bf046b36d1..924f678c1f96b5 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -12,6 +12,7 @@ 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, diff --git a/x-pack/plugins/lens/public/shared_components/query_input/helpers.ts b/x-pack/plugins/lens/public/shared_components/query_input/helpers.ts new file mode 100644 index 00000000000000..1b598a10f6f508 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/query_input/helpers.ts @@ -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; diff --git a/x-pack/plugins/lens/public/shared_components/query_input/index.ts b/x-pack/plugins/lens/public/shared_components/query_input/index.ts new file mode 100644 index 00000000000000..b2934de9168228 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/query_input/index.ts @@ -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'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/shared_components/query_input/query_input.tsx similarity index 97% rename from x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx rename to x-pack/plugins/lens/public/shared_components/query_input/query_input.tsx index 583f6b28996c9d..ecbc102eb751cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/shared_components/query_input/query_input.tsx @@ -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, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 53ccc8ab57761d..633194ef413721 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -10,12 +10,13 @@ import type { Query } from '@kbn/data-plugin/common'; import type { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { QueryInput } from '../../../../indexpattern_datasource/query_input'; import { fieldExists, FieldOption, FieldOptionValue, FieldPicker, + QueryInput, + validateQuery, } from '../../../../shared_components'; import type { FramePublicAPI } from '../../../../types'; import type { XYState, XYAnnotationLayerConfig } from '../../types'; @@ -56,6 +57,10 @@ export const ConfigPanelQueryAnnotation = ({ 'data-test-subj': `lns-fieldOption-${field.name}`, } as FieldOption; }); + const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( + annotation?.filter, + currentIndexPattern + ); const selectedField = annotation?.timeField; return ( @@ -96,6 +101,8 @@ export const ConfigPanelQueryAnnotation = ({ label={i18n.translate('xpack.lens.xyChart.annotation.queryInput', { defaultMessage: 'Annotation query', })} + isInvalid={!isQueryInputValid} + error={queryInputError} > {}} data-test-subj="annotation-query-based-query-input" placeholder={ From d5c8fdf2c03d092e5599a7c9fc93bbf1715fc983 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 15:01:05 +0200 Subject: [PATCH 65/98] :bug: Fix missing import --- .../operations/definitions/filters/filters.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 9cc0cf26751330..8aff5ac3f850ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -17,6 +17,7 @@ import { buildExpressionFunction } from '@kbn/expressions-plugin/public'; import { DragDropBuckets, DraggableBucketContainer, + isQueryValid, NewBucketButton, } from '../../../../shared_components'; import { IndexPattern } from '../../../../types'; From ee2ab7d2ed7d0f2dc0d5dd0dade4211c7ffd5ac9 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 15:22:33 +0200 Subject: [PATCH 66/98] :bug: fix more import issues --- .../operations/definitions/filters/filter_popover.test.tsx | 2 +- x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx index 0fc67aca367475..475761c94ec54b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx @@ -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, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index 0f485d71a7f355..bf6e4a5f1d3549 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -33,12 +33,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, From 1ea7feee49526902057a57140fb1574622d23f5f Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 17:25:36 +0200 Subject: [PATCH 67/98] :fire: remove duplicate code --- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- .../plugins/lens/public/app_plugin/types.ts | 2 +- .../loader.test.ts | 0 .../lens/public/data_views_service/loader.ts | 70 +++- .../mocks.ts | 0 .../lens/public/data_views_service/service.ts | 52 ++- .../editor_frame/config_panel/layer_panel.tsx | 2 +- .../editor_frame/config_panel/types.ts | 2 +- .../editor_frame/data_panel_wrapper.tsx | 2 +- .../editor_frame/editor_frame.tsx | 2 +- .../editor_frame/state_helpers.ts | 2 +- .../lens/public/embeddable/embeddable.tsx | 2 +- .../datapanel.test.tsx | 2 +- .../indexpattern_datasource/datapanel.tsx | 2 +- .../indexpattern_datasource/loader.test.ts | 2 +- .../formula/formula_public_api.test.ts | 4 +- .../definitions/formula/formula_public_api.ts | 2 +- .../public/indexpattern_service/loader.ts | 346 ------------------ .../public/indexpattern_service/service.ts | 155 -------- .../public/mocks/data_views_service_mock.ts | 2 +- x-pack/plugins/lens/public/mocks/index.ts | 2 +- x-pack/plugins/lens/public/types.ts | 2 +- x-pack/plugins/lens/public/utils.ts | 2 +- .../annotations_config_panel/index.test.tsx | 2 +- 24 files changed, 117 insertions(+), 544 deletions(-) rename x-pack/plugins/lens/public/{indexpattern_service => data_views_service}/loader.test.ts (100%) rename x-pack/plugins/lens/public/{indexpattern_service => data_views_service}/mocks.ts (100%) delete mode 100644 x-pack/plugins/lens/public/indexpattern_service/loader.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_service/service.ts diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index fa88d9cdd4d256..b495d3cd3f1f2a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -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 & { diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 0a5a2c82ad379b..335400b4cf793d 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -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 { diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.test.ts b/x-pack/plugins/lens/public/data_views_service/loader.test.ts similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_service/loader.test.ts rename to x-pack/plugins/lens/public/data_views_service/loader.test.ts diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index 4f87a9030318a1..836daea7b6d30f 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -6,7 +6,7 @@ */ import { isNestedField } from '@kbn/data-views-plugin/common'; -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; import { keyBy } from 'lodash'; import { HttpSetup } from '@kbn/core/public'; import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types'; @@ -15,6 +15,7 @@ import { BASE_API_URL, DateRange, ExistingFields } from '../../common'; import { DataViewsState } from '../state_management'; type ErrorHandler = (err: Error) => void; +type MinimalDataViewsContract = Pick; /** * All these functions will be used by the Embeddable instance too, @@ -28,7 +29,7 @@ export function getFieldByNameFactory(newFields: IndexPatternField[]) { export function convertDataViewIntoLensIndexPattern( dataView: DataView, - restrictionRemapper: (name: string) => string + restrictionRemapper: (name: string) => string = onRestrictionMapping ): IndexPattern { const newFields = dataView.fields .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) @@ -44,6 +45,18 @@ export function convertDataViewIntoLensIndexPattern( esTypes: field.esTypes, scripted: field.scripted, runtime: Boolean(field.runtimeField), + timeSeriesMetricType: field.timeSeriesMetric, + timeSeriesRollup: field.isRolledUpField, + partiallyApplicableFunctions: field.isRolledUpField + ? { + percentile: true, + percentile_rank: true, + median: true, + last_value: true, + unique_count: true, + standard_deviation: true, + } + : undefined, }; // Simplifies tests by hiding optional properties instead of undefined @@ -91,17 +104,27 @@ export function convertDataViewIntoLensIndexPattern( fields: newFields, getFieldByName: getFieldByNameFactory(newFields), hasRestrictions: !!typeMeta?.aggs, + spec: dataView.isPersisted() ? undefined : dataView.toSpec(false), }; } export async function loadIndexPatternRefs( - indexPatternsService: DataViewsContract + dataViews: MinimalDataViewsContract, + adHocDataViews?: Record ): Promise { - const indexPatterns = await indexPatternsService.getIdsWithTitle(); + const indexPatterns = await dataViews.getIdsWithTitle(); - return indexPatterns.sort((a, b) => { - return a.title.localeCompare(b.title); - }); + return indexPatterns + .concat( + Object.values(adHocDataViews || {}).map((dataViewSpec) => ({ + id: dataViewSpec.id!, + name: dataViewSpec.name, + title: dataViewSpec.title!, + })) + ) + .sort((a, b) => { + return a.title.localeCompare(b.title); + }); } /** @@ -121,17 +144,20 @@ export async function loadIndexPatterns({ patterns, notUsedPatterns, cache, + adHocDataViews, onIndexPatternRefresh, }: { - dataViews: DataViewsContract; + dataViews: MinimalDataViewsContract; patterns: string[]; notUsedPatterns?: string[]; cache: Record; + adHocDataViews?: Record; onIndexPatternRefresh?: () => void; }) { - const missingIds = patterns.filter((id) => !cache[id]); + const missingIds = patterns.filter((id) => !cache[id] && !adHocDataViews?.[id]); + const hasAdHocDataViews = Object.values(adHocDataViews || {}).length > 0; - if (missingIds.length === 0) { + if (missingIds.length === 0 && !hasAdHocDataViews) { return cache; } @@ -146,16 +172,22 @@ export async function loadIndexPatterns({ .map((response) => response.value); // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds - if (!indexPatterns.length && notUsedPatterns) { + if (!indexPatterns.length && !hasAdHocDataViews && notUsedPatterns) { for (const notUsedPattern of notUsedPatterns) { const resp = await dataViews.get(notUsedPattern).catch((e) => { // do nothing }); if (resp) { indexPatterns = [resp]; + break; } } } + indexPatterns.push( + ...(await Promise.all( + Object.values(adHocDataViews || {}).map((spec) => dataViews.create(spec)) + )) + ); const indexPatternsObject = indexPatterns.reduce( (acc, indexPattern) => ({ @@ -168,7 +200,7 @@ export async function loadIndexPatterns({ return indexPatternsObject; } -export async function loadIndexPattern({ +export async function ensureIndexPattern({ id, onError, dataViews, @@ -176,7 +208,7 @@ export async function loadIndexPattern({ }: { id: string; onError: ErrorHandler; - dataViews: DataViewsContract; + dataViews: MinimalDataViewsContract; cache?: IndexPatternMap; }) { const indexPatterns = await loadIndexPatterns({ @@ -227,6 +259,10 @@ async function refreshExistingFields({ body.timeFieldName = pattern.timeFieldName; } + if (pattern.spec) { + body.spec = pattern.spec; + } + return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { body: JSON.stringify(body), }) as Promise; @@ -289,13 +325,13 @@ export async function syncExistingFields({ updateIndexPatterns( { - isFirstExistenceFetch: status !== 200, existingFields: newExistingFields, ...(result - ? {} + ? { isFirstExistenceFetch: status !== 200 } : { - existenceFetchFailed: status !== 418, - existenceFetchTimeout: status === 418, + isFirstExistenceFetch, + existenceFetchFailed: status !== 408, + existenceFetchTimeout: status === 408, }), }, { applyImmediately: true } diff --git a/x-pack/plugins/lens/public/indexpattern_service/mocks.ts b/x-pack/plugins/lens/public/data_views_service/mocks.ts similarity index 100% rename from x-pack/plugins/lens/public/indexpattern_service/mocks.ts rename to x-pack/plugins/lens/public/data_views_service/mocks.ts diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/data_views_service/service.ts index 047013525266f1..91e070d52d48ae 100644 --- a/x-pack/plugins/lens/public/data_views_service/service.ts +++ b/x-pack/plugins/lens/public/data_views_service/service.ts @@ -5,27 +5,39 @@ * 2.0. */ -import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { ActionExecutionContext, UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + UPDATE_FILTER_REFERENCES_ACTION, + UPDATE_FILTER_REFERENCES_TRIGGER, +} from '@kbn/unified-search-plugin/public'; import type { DateRange } from '../../common'; import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; import { - loadIndexPattern, + ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns, syncExistingFields, } from './loader'; import type { DataViewsState } from '../state_management'; +import { generateId } from '../id_generator'; -interface IndexPatternServiceProps { +export interface IndexPatternServiceProps { core: Pick; dataViews: DataViewsContract; uiSettings: IUiSettingsClient; + uiActions: UiActionsStart; updateIndexPatterns: ( newState: Partial, options?: { applyImmediately: boolean } ) => void; + replaceIndexPattern: ( + newIndexPattern: IndexPattern, + oldId: string, + options?: { applyImmediately: boolean } + ) => void; } /** @@ -69,15 +81,17 @@ export interface IndexPatternServiceAPI { indexPatternList: IndexPattern[]; isFirstExistenceFetch: boolean; }) => Promise; + + replaceDataViewId: (newDataView: DataView) => Promise; /** * Retrieves the default indexPattern from the uiSettings */ getDefaultIndex: () => string; /** - * Update the Lens state cache of indexPatterns + * Update the Lens dataViews state */ - updateIndexPatternsCache: ( + updateDataViewsState: ( newState: Partial, options?: { applyImmediately: boolean } ) => void; @@ -88,6 +102,8 @@ export function createIndexPatternService({ dataViews, uiSettings, updateIndexPatterns, + replaceIndexPattern, + uiActions, }: IndexPatternServiceProps): IndexPatternServiceAPI { const onChangeError = (err: Error) => core.notifications.toasts.addError(err, { @@ -96,14 +112,36 @@ export function createIndexPatternService({ }), }); return { - updateIndexPatternsCache: updateIndexPatterns, + updateDataViewsState: updateIndexPatterns, loadIndexPatterns: (args) => { return loadIndexPatterns({ dataViews, ...args, }); }, - ensureIndexPattern: (args) => loadIndexPattern({ onError: onChangeError, dataViews, ...args }), + replaceDataViewId: async (dataView: DataView) => { + const newDataView = await dataViews.create({ ...dataView.toSpec(), id: generateId() }); + dataViews.clearInstanceCache(dataView.id); + const loadedPatterns = await loadIndexPatterns({ + dataViews, + patterns: [newDataView.id!], + cache: {}, + }); + replaceIndexPattern(loadedPatterns[newDataView.id!], dataView.id!, { + applyImmediately: true, + }); + const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER); + const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION); + + action?.execute({ + trigger, + fromDataView: dataView.id, + toDataView: newDataView.id, + usedDataViews: [], + } as ActionExecutionContext); + }, + ensureIndexPattern: (args) => + ensureIndexPattern({ onError: onChangeError, dataViews, ...args }), refreshExistingFields: (args) => syncExistingFields({ updateIndexPatterns, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 544835d2e3c21d..229542278c2c6f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -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, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 3e1458d42c438e..c4a2d77c30baba 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -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, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 90dbdedd856480..8e651709d44b7b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -36,7 +36,7 @@ import { 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'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index cc0db7073306f7..ced5bbe335ef9c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -29,7 +29,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; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 5af7deded33e7c..f15ca128e3a7eb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -39,7 +39,7 @@ import { } from '../error_helper'; 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[], diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 26a2df7916405b..e61015ccb0eef3 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -79,7 +79,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; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 7a3986ec3f5ba8..8eca86cd936045 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -31,7 +31,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'; const fieldsOne = [ { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 1d46cef70bc9d6..dd9054d82dfa37 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -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, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 80f6eaecd3a8a7..3d641ca004a3b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -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) => { return { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts index 3286696308d51d..c854595757a0ce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.test.ts @@ -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), })); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts index f5080de84d67d7..4085ec931d3a6d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula_public_api.ts @@ -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'; diff --git a/x-pack/plugins/lens/public/indexpattern_service/loader.ts b/x-pack/plugins/lens/public/indexpattern_service/loader.ts deleted file mode 100644 index 836daea7b6d30f..00000000000000 --- a/x-pack/plugins/lens/public/indexpattern_service/loader.ts +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isNestedField } from '@kbn/data-views-plugin/common'; -import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; -import { keyBy } from 'lodash'; -import { HttpSetup } from '@kbn/core/public'; -import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types'; -import { documentField } from '../indexpattern_datasource/document_field'; -import { BASE_API_URL, DateRange, ExistingFields } from '../../common'; -import { DataViewsState } from '../state_management'; - -type ErrorHandler = (err: Error) => void; -type MinimalDataViewsContract = Pick; - -/** - * All these functions will be used by the Embeddable instance too, - * therefore keep all these functions pretty raw here and do not use the IndexPatternService - */ - -export function getFieldByNameFactory(newFields: IndexPatternField[]) { - const fieldsLookup = keyBy(newFields, 'name'); - return (name: string) => fieldsLookup[name]; -} - -export function convertDataViewIntoLensIndexPattern( - dataView: DataView, - restrictionRemapper: (name: string) => string = onRestrictionMapping -): IndexPattern { - const newFields = dataView.fields - .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) - .map((field): IndexPatternField => { - // Convert the getters on the index pattern service into plain JSON - const base = { - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - meta: dataView.metaFields.includes(field.name), - esTypes: field.esTypes, - scripted: field.scripted, - runtime: Boolean(field.runtimeField), - timeSeriesMetricType: field.timeSeriesMetric, - timeSeriesRollup: field.isRolledUpField, - partiallyApplicableFunctions: field.isRolledUpField - ? { - percentile: true, - percentile_rank: true, - median: true, - last_value: true, - unique_count: true, - standard_deviation: true, - } - : undefined, - }; - - // Simplifies tests by hiding optional properties instead of undefined - return base.scripted - ? { - ...base, - lang: field.lang, - script: field.script, - } - : base; - }) - .concat(documentField); - - const { typeMeta, title, name, timeFieldName, fieldFormatMap } = dataView; - if (typeMeta?.aggs) { - const aggs = Object.keys(typeMeta.aggs); - newFields.forEach((field, index) => { - const restrictionsObj: IndexPatternField['aggregationRestrictions'] = {}; - aggs.forEach((agg) => { - const restriction = typeMeta.aggs && typeMeta.aggs[agg] && typeMeta.aggs[agg][field.name]; - if (restriction) { - restrictionsObj[restrictionRemapper(agg)] = restriction; - } - }); - if (Object.keys(restrictionsObj).length) { - newFields[index] = { ...field, aggregationRestrictions: restrictionsObj }; - } - }); - } - - return { - id: dataView.id!, // id exists for sure because we got index patterns by id - title, - name: name ? name : title, - timeFieldName, - fieldFormatMap: - fieldFormatMap && - Object.fromEntries( - Object.entries(fieldFormatMap).map(([id, format]) => [ - id, - // @ts-ignore - 'toJSON' in format ? format.toJSON() : format, - ]) - ), - fields: newFields, - getFieldByName: getFieldByNameFactory(newFields), - hasRestrictions: !!typeMeta?.aggs, - spec: dataView.isPersisted() ? undefined : dataView.toSpec(false), - }; -} - -export async function loadIndexPatternRefs( - dataViews: MinimalDataViewsContract, - adHocDataViews?: Record -): Promise { - const indexPatterns = await dataViews.getIdsWithTitle(); - - return indexPatterns - .concat( - Object.values(adHocDataViews || {}).map((dataViewSpec) => ({ - id: dataViewSpec.id!, - name: dataViewSpec.name, - title: dataViewSpec.title!, - })) - ) - .sort((a, b) => { - return a.title.localeCompare(b.title); - }); -} - -/** - * Map ES agg names with Lens ones - */ -const renameOperationsMapping: Record = { - avg: 'average', - cardinality: 'unique_count', -}; - -function onRestrictionMapping(agg: string): string { - return agg in renameOperationsMapping ? renameOperationsMapping[agg] : agg; -} - -export async function loadIndexPatterns({ - dataViews, - patterns, - notUsedPatterns, - cache, - adHocDataViews, - onIndexPatternRefresh, -}: { - dataViews: MinimalDataViewsContract; - patterns: string[]; - notUsedPatterns?: string[]; - cache: Record; - adHocDataViews?: Record; - onIndexPatternRefresh?: () => void; -}) { - const missingIds = patterns.filter((id) => !cache[id] && !adHocDataViews?.[id]); - const hasAdHocDataViews = Object.values(adHocDataViews || {}).length > 0; - - if (missingIds.length === 0 && !hasAdHocDataViews) { - return cache; - } - - onIndexPatternRefresh?.(); - - const allIndexPatterns = await Promise.allSettled(missingIds.map((id) => dataViews.get(id))); - // ignore rejected indexpatterns here, they're already handled at the app level - let indexPatterns = allIndexPatterns - .filter( - (response): response is PromiseFulfilledResult => response.status === 'fulfilled' - ) - .map((response) => response.value); - - // if all of the used index patterns failed to load, try loading one of not used ones till one succeeds - if (!indexPatterns.length && !hasAdHocDataViews && notUsedPatterns) { - for (const notUsedPattern of notUsedPatterns) { - const resp = await dataViews.get(notUsedPattern).catch((e) => { - // do nothing - }); - if (resp) { - indexPatterns = [resp]; - break; - } - } - } - indexPatterns.push( - ...(await Promise.all( - Object.values(adHocDataViews || {}).map((spec) => dataViews.create(spec)) - )) - ); - - const indexPatternsObject = indexPatterns.reduce( - (acc, indexPattern) => ({ - [indexPattern.id!]: convertDataViewIntoLensIndexPattern(indexPattern, onRestrictionMapping), - ...acc, - }), - { ...cache } - ); - - return indexPatternsObject; -} - -export async function ensureIndexPattern({ - id, - onError, - dataViews, - cache = {}, -}: { - id: string; - onError: ErrorHandler; - dataViews: MinimalDataViewsContract; - cache?: IndexPatternMap; -}) { - const indexPatterns = await loadIndexPatterns({ - dataViews, - cache, - patterns: [id], - }); - - if (indexPatterns[id] == null) { - onError(Error('Missing indexpatterns')); - return; - } - - const newIndexPatterns = { - ...cache, - [id]: indexPatterns[id], - }; - return newIndexPatterns; -} - -async function refreshExistingFields({ - dateRange, - fetchJson, - indexPatternList, - dslQuery, -}: { - dateRange: DateRange; - indexPatternList: IndexPattern[]; - fetchJson: HttpSetup['post']; - dslQuery: object; -}) { - try { - const emptinessInfo = await Promise.all( - indexPatternList.map((pattern) => { - if (pattern.hasRestrictions) { - return { - indexPatternTitle: pattern.title, - existingFieldNames: pattern.fields.map((field) => field.name), - }; - } - const body: Record = { - dslQuery, - fromDate: dateRange.fromDate, - toDate: dateRange.toDate, - }; - - if (pattern.timeFieldName) { - body.timeFieldName = pattern.timeFieldName; - } - - if (pattern.spec) { - body.spec = pattern.spec; - } - - return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { - body: JSON.stringify(body), - }) as Promise; - }) - ); - return { result: emptinessInfo, status: 200 }; - } catch (e) { - return { result: undefined, status: e.res?.status as number }; - } -} - -type FieldsPropsFromDataViewsState = Pick< - DataViewsState, - 'existingFields' | 'isFirstExistenceFetch' | 'existenceFetchTimeout' | 'existenceFetchFailed' ->; -export async function syncExistingFields({ - updateIndexPatterns, - isFirstExistenceFetch, - currentIndexPatternTitle, - onNoData, - existingFields, - ...requestOptions -}: { - dateRange: DateRange; - indexPatternList: IndexPattern[]; - existingFields: Record>; - fetchJson: HttpSetup['post']; - updateIndexPatterns: ( - newFieldState: FieldsPropsFromDataViewsState, - options: { applyImmediately: boolean } - ) => void; - isFirstExistenceFetch: boolean; - currentIndexPatternTitle: string; - dslQuery: object; - onNoData?: () => void; -}) { - const { indexPatternList } = requestOptions; - const newExistingFields = { ...existingFields }; - - const { result, status } = await refreshExistingFields(requestOptions); - - if (result) { - if (isFirstExistenceFetch) { - const fieldsCurrentIndexPattern = result.find( - (info) => info.indexPatternTitle === currentIndexPatternTitle - ); - if (fieldsCurrentIndexPattern && fieldsCurrentIndexPattern.existingFieldNames.length === 0) { - onNoData?.(); - } - } - - for (const { indexPatternTitle, existingFieldNames } of result) { - newExistingFields[indexPatternTitle] = booleanMap(existingFieldNames); - } - } else { - for (const { title, fields } of indexPatternList) { - newExistingFields[title] = booleanMap(fields.map((field) => field.name)); - } - } - - updateIndexPatterns( - { - existingFields: newExistingFields, - ...(result - ? { isFirstExistenceFetch: status !== 200 } - : { - isFirstExistenceFetch, - existenceFetchFailed: status !== 408, - existenceFetchTimeout: status === 408, - }), - }, - { applyImmediately: true } - ); -} - -function booleanMap(keys: string[]) { - return keys.reduce((acc, key) => { - acc[key] = true; - return acc; - }, {} as Record); -} diff --git a/x-pack/plugins/lens/public/indexpattern_service/service.ts b/x-pack/plugins/lens/public/indexpattern_service/service.ts deleted file mode 100644 index 91e070d52d48ae..00000000000000 --- a/x-pack/plugins/lens/public/indexpattern_service/service.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; -import type { CoreStart, IUiSettingsClient } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { ActionExecutionContext, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { - UPDATE_FILTER_REFERENCES_ACTION, - UPDATE_FILTER_REFERENCES_TRIGGER, -} from '@kbn/unified-search-plugin/public'; -import type { DateRange } from '../../common'; -import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; -import { - ensureIndexPattern, - loadIndexPatternRefs, - loadIndexPatterns, - syncExistingFields, -} from './loader'; -import type { DataViewsState } from '../state_management'; -import { generateId } from '../id_generator'; - -export interface IndexPatternServiceProps { - core: Pick; - dataViews: DataViewsContract; - uiSettings: IUiSettingsClient; - uiActions: UiActionsStart; - updateIndexPatterns: ( - newState: Partial, - options?: { applyImmediately: boolean } - ) => void; - replaceIndexPattern: ( - newIndexPattern: IndexPattern, - oldId: string, - options?: { applyImmediately: boolean } - ) => void; -} - -/** - * This service is only available for the full editor version - * and it encapsulate all the indexpattern methods and state - * in a single object. - * NOTE: this is not intended to be used with the Embeddable branch - */ -export interface IndexPatternServiceAPI { - /** - * Loads a list of indexPatterns from a list of id (patterns) - * leveraging existing cache. Eventually fallbacks to unused indexPatterns ( notUsedPatterns ) - * @returns IndexPatternMap - */ - loadIndexPatterns: (args: { - patterns: string[]; - notUsedPatterns?: string[]; - cache: IndexPatternMap; - onIndexPatternRefresh?: () => void; - }) => Promise; - /** - * Load indexPatternRefs with title and ids - */ - loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise; - /** - * Ensure an indexPattern is loaded in the cache, usually used in conjuction with a indexPattern change action. - */ - ensureIndexPattern: (args: { - id: string; - cache: IndexPatternMap; - }) => Promise; - /** - * Loads the existingFields map given the current context - */ - refreshExistingFields: (args: { - dateRange: DateRange; - currentIndexPatternTitle: string; - dslQuery: object; - onNoData?: () => void; - existingFields: Record>; - indexPatternList: IndexPattern[]; - isFirstExistenceFetch: boolean; - }) => Promise; - - replaceDataViewId: (newDataView: DataView) => Promise; - /** - * Retrieves the default indexPattern from the uiSettings - */ - getDefaultIndex: () => string; - - /** - * Update the Lens dataViews state - */ - updateDataViewsState: ( - newState: Partial, - options?: { applyImmediately: boolean } - ) => void; -} - -export function createIndexPatternService({ - core, - dataViews, - uiSettings, - updateIndexPatterns, - replaceIndexPattern, - uiActions, -}: IndexPatternServiceProps): IndexPatternServiceAPI { - const onChangeError = (err: Error) => - core.notifications.toasts.addError(err, { - title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { - defaultMessage: 'Error loading data view', - }), - }); - return { - updateDataViewsState: updateIndexPatterns, - loadIndexPatterns: (args) => { - return loadIndexPatterns({ - dataViews, - ...args, - }); - }, - replaceDataViewId: async (dataView: DataView) => { - const newDataView = await dataViews.create({ ...dataView.toSpec(), id: generateId() }); - dataViews.clearInstanceCache(dataView.id); - const loadedPatterns = await loadIndexPatterns({ - dataViews, - patterns: [newDataView.id!], - cache: {}, - }); - replaceIndexPattern(loadedPatterns[newDataView.id!], dataView.id!, { - applyImmediately: true, - }); - const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER); - const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION); - - action?.execute({ - trigger, - fromDataView: dataView.id, - toDataView: newDataView.id, - usedDataViews: [], - } as ActionExecutionContext); - }, - ensureIndexPattern: (args) => - ensureIndexPattern({ onError: onChangeError, dataViews, ...args }), - refreshExistingFields: (args) => - syncExistingFields({ - updateIndexPatterns, - fetchJson: core.http.post, - ...args, - }), - loadIndexPatternRefs: async ({ isFullEditor }) => - isFullEditor ? loadIndexPatternRefs(dataViews) : [], - getDefaultIndex: () => uiSettings.get('defaultIndex'), - }; -} diff --git a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts index e2a07c717ee077..f7bcdab9e6eac7 100644 --- a/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts +++ b/x-pack/plugins/lens/public/mocks/data_views_service_mock.ts @@ -13,7 +13,7 @@ import { createIndexPatternService, IndexPatternServiceProps, IndexPatternServiceAPI, -} from '../indexpattern_service/service'; +} from '../data_views_service/service'; export function createIndexPatternServiceMock({ core = coreMock.createStart(), diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index a13d66bb532fb2..4cfdfbad661af0 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createMockDataViewsState } from '../indexpattern_service/mocks'; +import { createMockDataViewsState } from '../data_views_service/mocks'; import { FramePublicAPI, FrameDatasourceAPI } from '../types'; export { mockDataPlugin } from './data_plugin_mock'; export { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index cf00a93fc55374..50b3bf9cca8481 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -46,7 +46,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 { diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 74070aef38c812..385a533dc7c754 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -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', { diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx index 9d0a73c22c2ee2..5514e3062213e0 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx @@ -17,7 +17,7 @@ import { Position } from '@elastic/charts'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import moment from 'moment'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; -import { createMockDataViewsState } from '../../../../indexpattern_service/mocks'; +import { createMockDataViewsState } from '../../../../data_views_service/mocks'; import { createMockedIndexPattern } from '../../../../indexpattern_datasource/mocks'; jest.mock('lodash', () => { From 56d6664c96e5fd968ef3f0c2144009dabb2e5293 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 19:07:01 +0200 Subject: [PATCH 68/98] :bug: Fix dataView switch bug --- .../editor_frame/data_panel_wrapper.test.tsx | 1 + .../editor_frame/data_panel_wrapper.tsx | 13 +++++++++++++ .../public/indexpattern_datasource/datapanel.tsx | 9 +++++---- .../dimension_panel/dimension_panel.test.tsx | 4 ++-- .../public/indexpattern_datasource/indexpattern.tsx | 3 +++ .../definitions/filters/filter_popover.tsx | 4 +++- x-pack/plugins/lens/public/mocks/datasource_mock.ts | 1 + x-pack/plugins/lens/public/types.ts | 13 +++++++------ .../lens/public/visualizations/xy/visualization.tsx | 5 +++++ .../annotations_config_panel/annotations_panel.tsx | 6 +++++- .../query_annotation_panel.tsx | 10 +++++++--- .../tooltip_annotation_panel.tsx | 6 ++++-- .../xy/xy_config_panel/layer_header.tsx | 7 ++++--- 13 files changed, 60 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx index 648b989f65e8f7..9370892a2d7fe6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.test.tsx @@ -26,6 +26,7 @@ describe('Data Panel Wrapper', () => { const datasourceMap = { activeDatasource: { renderDataPanel, + getUsedDataViews: jest.fn(), } as unknown as Datasource, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index 8e651709d44b7b..0e2b7f88ca6f24 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -165,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); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index dd9054d82dfa37..dc10a5e67625d6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -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; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 56acffaed1ffb4..3f7c4f1e9b006c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -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'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 7c4ca5f9db2558..d5a7a79ca07ee9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -701,6 +701,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; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index a231fd0a5da6cf..17314bbc991c01 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -10,7 +10,9 @@ import './filter_popover.scss'; import React from 'react'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; -import { QueryInput, isQueryValid } from '../../../../shared_components'; +// 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 } from '.'; import { LabelInput } from '../shared_components'; diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index 8111270cc5d124..eec1d062131e74 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -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(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 50b3bf9cca8481..0923274b750aa5 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -444,6 +444,10 @@ export interface Datasource { * 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 { @@ -505,6 +509,7 @@ export interface DatasourceDataPanelProps { uiActions: UiActionsStart; indexPatternService: IndexPatternServiceAPI; frame: FramePublicAPI; + usedIndexPatterns?: string[]; } interface SharedDimensionProps { @@ -891,13 +896,9 @@ export interface Visualization { initialize: (addNewLayer: () => string, state?: T, mainPalette?: PaletteOutput) => T; /** - * Retrieve the used indexpatterns in the visualization + * Retrieve the used DataViews in the visualization */ - getUsedIndexPatterns?: ( - state?: T, - indexPatternRefs?: IndexPatternRef[], - savedObjectReferences?: SavedObjectReference[] - ) => { usedPatterns: string[] }; + getUsedDataViews?: (state?: T) => string[]; getMainPalette?: (state: T) => undefined | PaletteOutput; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index abcf29aa531696..818393d30d00e1 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -741,6 +741,11 @@ export const getXyVisualization = ({ getUniqueLabels(state) { return getUniqueLabels(state.layers); }, + getUsedDataViews(state) { + return ( + state?.layers.filter(isAnnotationsLayer).map(({ indexPatternId }) => indexPatternId) ?? [] + ); + }, renderDimensionTrigger({ columnId, label, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 2d21d64ca78b33..96f31e2e8754f0 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -220,6 +220,10 @@ export const AnnotationsPanel = ( ); const selectedField = (currentAnnotation as QueryPointEventAnnotationConfig) .textField; + + const fieldIsValid = selectedField + ? Boolean(currentIndexPattern.getFieldByName(selectedField)) + : true; return ( <> @@ -240,7 +244,7 @@ export const AnnotationsPanel = ( setAnnotations({ textField: choice.field }); } }} - fieldIsInvalid={false} + fieldIsInvalid={!fieldIsValid} /> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 633194ef413721..b9decc4fac6ba4 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -41,6 +41,7 @@ export const ConfigPanelQueryAnnotation = ({ }) => { const inputQuery = annotation?.filter ?? defaultQuery; const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; + const currentExistingFields = frame.dataViews.existingFields[currentIndexPattern.title]; // list only supported field by operation, remove the rest const options = currentIndexPattern.fields .filter((field) => field.type === 'date' && field.displayName) @@ -52,7 +53,7 @@ export const ConfigPanelQueryAnnotation = ({ field: field.name, dataType: field.type, }, - exists: fieldExists(frame.dataViews.existingFields[currentIndexPattern.title], field.name), + exists: fieldExists(currentExistingFields, field.name), compatible: true, 'data-test-subj': `lns-fieldOption-${field.name}`, } as FieldOption; @@ -63,6 +64,9 @@ export const ConfigPanelQueryAnnotation = ({ ); const selectedField = annotation?.timeField; + const fieldIsValid = selectedField + ? Boolean(currentIndexPattern.getFieldByName(selectedField)) + : true; return ( <> @@ -111,7 +115,7 @@ export const ConfigPanelQueryAnnotation = ({ }} disableAutoFocus indexPatternTitle={frame.dataViews.indexPatterns[layer.indexPatternId].title} - isInvalid={isQueryInputValid} + isInvalid={!isQueryInputValid} onSubmit={() => {}} data-test-subj="annotation-query-based-query-input" placeholder={ diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index 764afc70d365f1..309feaf2e68705 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -119,6 +119,7 @@ export function TooltipSection({ ); } + const currentExistingField = existingFields[indexPattern.title]; const disableActions = localValues.length === 2 && localValues.some(({ isNew }) => isNew); const options = indexPattern.fields .filter( @@ -134,7 +135,7 @@ export function TooltipSection({ field: field.name, dataType: field.type, }, - exists: fieldExists(existingFields[indexPattern.title], field.name), + exists: fieldExists(currentExistingField, field.name), compatible: true, 'data-test-subj': `lnsXY-annotation-tooltip-fieldOption-${field.name}`, } as FieldOption) @@ -152,6 +153,7 @@ export function TooltipSection({ items={localValues} > {localValues.map(({ id, value, isNew }, index) => { + const fieldIsValid = value ? Boolean(indexPattern.getFieldByName(value)) : true; return ( diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index f23ee51b327ba1..819dfe13c2ba21 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -76,20 +76,21 @@ function AnnotationLayerHeaderContent({ }); const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[layerIndex] as XYAnnotationLayerConfig; + const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; return ( ); From e970b7901f7af40fd8d879bf391dfc924471ec83 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 1 Sep 2022 19:37:55 +0200 Subject: [PATCH 69/98] :label: Fix type issue --- .../public/indexpattern_datasource/datapanel.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 8eca86cd936045..c7a5a14fb697bc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -327,7 +327,9 @@ describe('IndexPattern Data Panel', () => { }); describe('loading existence data', () => { - function testProps(updateIndexPatterns: IndexPatternServiceProps['updateIndexPatterns']) { + function testProps( + updateIndexPatterns: IndexPatternServiceProps['updateIndexPatterns'] + ): Props { core.http.post.mockImplementation(async (path) => { const parts = (path as unknown as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; @@ -390,7 +392,7 @@ describe('IndexPattern Data Panel', () => { stateChanges?: Partial, propChanges?: Partial ) { - const inst = mountWithIntl(); + const inst = mountWithIntl(); await act(async () => { inst.update(); @@ -400,10 +402,10 @@ describe('IndexPattern Data Panel', () => { await act(async () => { inst.setProps({ ...props, - ...((propChanges as object) || {}), + ...(propChanges || {}), state: { ...props.state, - ...((stateChanges as object) || {}), + ...(stateChanges || {}), }, }); inst.update(); From b35753c3d7faceda7ed2d40238e79b56720837ab Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 15:23:44 +0200 Subject: [PATCH 70/98] annotations with fetching_event_annotations --- .../expression_xy/common/__mocks__/index.ts | 8 + .../event_annotations_result.ts | 57 +++++ .../extended_annotation_layer.ts | 6 +- .../expression_functions/layered_xy_vis.ts | 9 +- .../expression_functions/xy_vis.test.ts | 23 +- .../common/expression_functions/xy_vis.ts | 7 +- .../common/expression_functions/xy_vis_fn.ts | 2 - .../common/types/expression_functions.ts | 10 +- .../public/components/xy_chart.test.tsx | 21 +- .../public/components/xy_chart.tsx | 77 +------ .../public/helpers/visualization.ts | 7 - .../expression_xy/public/plugin.ts | 2 + .../expression_xy/server/plugin.ts | 2 + .../data/common/search/aggs/buckets/index.ts | 1 + .../request_event_annotations.ts | 103 ++++++--- src/plugins/event_annotation/common/index.ts | 1 + .../query_point_event_annotation/index.ts | 1 - .../query_point_event_annotation/types.ts | 2 +- src/plugins/event_annotation/common/types.ts | 7 +- .../event_annotation_service/service.tsx | 207 +++++++++++------- .../public/event_annotation_service/types.ts | 6 +- .../fetch_event_annotations.test.ts | 6 +- .../indexpattern_datasource/indexpattern.tsx | 3 + .../indexpattern_suggestions.test.tsx | 17 ++ x-pack/plugins/lens/public/types.ts | 1 + .../__snapshots__/to_expression.test.ts.snap | 1 + .../public/visualizations/xy/to_expression.ts | 32 ++- 27 files changed, 386 insertions(+), 233 deletions(-) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/event_annotations_result.ts diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index ff3d89163c0701..ee25839fc7370e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -132,6 +132,14 @@ export const createArgsWithLayers = ( }, ], layers: Array.isArray(layers) ? layers : [layers], + annotations: { + layers: [], + datatable: { + type: 'datatable', + columns: [], + rows: [], + }, + }, }); export function sampleArgs() { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/event_annotations_result.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/event_annotations_result.ts new file mode 100644 index 00000000000000..bb2e91097746fc --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/event_annotations_result.ts @@ -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 || {}, + }; + }, + }; +} diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts index 7c361000f7823f..5098be2aa2705f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts @@ -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: { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts index 392c9a0d5830ac..0ca9a3ac4e5cce 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts @@ -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(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 0b81682eb4381d..7e6afa0dd23a7b 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -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', diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 9db238a117b756..0b00525e3c9147 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -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(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 2808b861c6df85..defda933784de1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -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); diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 0970cec985d308..2a9c29c9125012 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -215,7 +215,6 @@ export interface XYArgs extends DataLayerArgs { emphasizeFitting?: boolean; valueLabels: ValueLabelMode; referenceLines: ReferenceLineConfigResult[]; - annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; fillOpacity?: number; hideEndzones?: boolean; @@ -239,6 +238,10 @@ export interface LayeredXYArgs { emphasizeFitting?: boolean; valueLabels: ValueLabelMode; layers?: XYExtendedLayerConfigResult[]; + annotations?: { + layers: AnnotationLayerConfigResult[]; + datatable: Datatable; + }; fittingFunction?: FittingFunction; fillOpacity?: number; hideEndzones?: boolean; @@ -279,6 +282,10 @@ export interface XYProps { orderBucketsBySum?: boolean; showTooltip: boolean; singleTable?: boolean; + annotations?: { + layers: AnnotationLayerConfigResult[]; + datatable: Datatable; + }; } export interface AnnotationLayerArgs { @@ -326,7 +333,6 @@ export type XYExtendedLayerConfig = export type XYExtendedLayerConfigResult = | ExtendedDataLayerConfigResult | ReferenceLineLayerConfigResult - | ExtendedAnnotationLayerConfigResult | ReferenceLineConfigResult; export interface ExtendedReferenceLineDecorationConfig extends ReferenceLineArgs { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 493ba08ee9542c..f22e7b4a6e656e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -54,11 +54,7 @@ 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'; @@ -3070,10 +3066,9 @@ describe('XYChart component', () => { }; const createLayerWithAnnotations = ( annotations: EventAnnotationOutput[] = [defaultLineStaticAnnotation] - ): CommonXYAnnotationLayerConfig => ({ + ): AnnotationLayerConfigResult => ({ type: 'annotationLayer', layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', annotations, }); function sampleArgsWithAnnotations(annotationLayers = [createLayerWithAnnotations()]) { @@ -3081,7 +3076,15 @@ describe('XYChart component', () => { return { args: { ...args, - layers: [dateHistogramLayer, ...annotationLayers], + layers: [dateHistogramLayer], + annotations: { + layers: annotationLayers, + datatable: { + type: 'datatable' as const, + columns: [], + rows: [], + }, + }, }, }; } @@ -3102,7 +3105,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(); expect(component.find('LineAnnotation')).toMatchSnapshot(); expect(component.find('RectAnnotation')).toMatchSnapshot(); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 0d6c21506a79ff..761761f96d97ec 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -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 { ManualPointEventAnnotationRow } 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(null); const chartTheme = chartsThemeService.useChartsTheme(); @@ -448,21 +398,11 @@ 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 groupedLineAnnotations = getAnnotationsGroupedByInterval( lineAnnotations as ManualPointEventAnnotationRow[], xAxisFormatter @@ -483,7 +423,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) : {}; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts index 87d27d30badb23..c75c4ee54ffe6c 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -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) - ); diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 6de052b3921498..2de8dcd3d1e583 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -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); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index f0e0fc141302ab..b6175774ac515e 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -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 @@ -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); diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 4019e2d5b2aa0d..9d7819157e1890 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -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'; diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 1f734b0182d6e2..2716448110f120 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -10,19 +10,22 @@ import { defer, firstValueFrom } from 'rxjs'; import { partition } from 'lodash'; import { AggsStart, - DataViewsContract, + DataView, DataViewSpec, ExpressionValueSearchContext, parseEsInterval, AggConfigs, - IndexPatternExpressionType, + TimeBuckets, + UI_SETTINGS, } from '@kbn/data-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; import { ExecutionContext } from '@kbn/expressions-plugin/common'; import moment from 'moment'; import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts'; import { Adapters } from '@kbn/inspector-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import dateMath from '@kbn/datemath'; import { handleRequest } from './handle_request'; import { ANNOTATIONS_PER_BUCKET, @@ -45,9 +48,9 @@ interface ManualGroup { interface QueryGroup { type: 'query'; annotations: QueryPointEventAnnotationOutput[]; - allFields?: string[]; - dataView: IndexPatternExpressionType; timeField: string; + dataView: DataView; + allFields?: string[]; } export function getTimeZone(uiSettings: IUiSettingsClient) { @@ -59,6 +62,20 @@ export function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } +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 requestEventAnnotations = ( input: ExpressionValueSearchContext | null, args: FetchEventAnnotationsArgs, @@ -72,14 +89,46 @@ export const requestEventAnnotations = ( ) => { return defer(async () => { const { aggs, dataViews, searchSource, getNow, uiSettings } = await getStartDependencies(); + if (!input?.timeRange) { + return; + } + const dates = toAbsoluteDates(input?.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(args.interval); + buckets.setBounds({ + min: moment(dates.from), + max: moment(dates.to), + }); + + const interval = buckets.getInterval().expression; + + const uniqueDataViewsToLoad = args.groups + .map((g) => g.dataView.value) + .reduce((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 @@ -92,7 +141,7 @@ export const requestEventAnnotations = ( aggConfigs, timeFields, }: { - dataView: any; + dataView: DataView; aggConfigs: AggConfigs; timeFields: string[]; }) => @@ -113,12 +162,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 +213,9 @@ const convertManualToDatatableRows = ( const prepareEsaggsForQueryGroups = async ( queryGroups: QueryGroup[], interval: string, - dataViews: DataViewsContract, aggs: AggsStart ) => { - const uniqueDataViewsToLoad = queryGroups - .map((g) => g.dataView.value) - .reduce((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 +290,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>( (acc, fieldName, i) => ({ @@ -278,7 +311,8 @@ const prepareEsaggsForQueryGroups = async ( function regroupForRequestOptimization( { groups }: FetchEventAnnotationsArgs, - input: ExpressionValueSearchContext | null + input: ExpressionValueSearchContext | null, + loadedDataViews: DataView[] ) { const outputGroups = groups .map((g) => { @@ -297,7 +331,10 @@ 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; + // get default timefield if not defined + const key = `${g.dataView.value.id}-${timeField}`; const subGroup = acc[key] as QueryGroup; if (subGroup) { let allFields = [...(subGroup.allFields || []), ...(current.extraFields || [])]; @@ -321,8 +358,8 @@ function regroupForRequestOptimization( ...acc, [key]: { type: 'query', - dataView: g.dataView, - timeField: current.timeField, + dataView, + timeField: timeField!, allFields, annotations: [current], }, diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index a43d1b567f087b..a075fe0c2161f9 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -26,6 +26,7 @@ export type { EventAnnotationGroupArgs } from './event_annotation_group'; export type { FetchEventAnnotationsArgs } from './fetch_event_annotations/types'; export type { EventAnnotationConfig, + EventAnnotationGroupConfig, EventAnnotationArgs, RangeEventAnnotationConfig, PointInTimeEventAnnotationConfig, diff --git a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts index 5ce8b8eb3fb142..cb9ba882a9f891 100644 --- a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts @@ -47,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`, diff --git a/src/plugins/event_annotation/common/query_point_event_annotation/types.ts b/src/plugins/event_annotation/common/query_point_event_annotation/types.ts index 0aa172017b36be..592c50a0621ce9 100644 --- a/src/plugins/event_annotation/common/query_point_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/query_point_event_annotation/types.ts @@ -12,7 +12,7 @@ import { PointStyleProps } from '../types'; export type QueryPointEventAnnotationArgs = { id: string; filter: KibanaQueryOutput; - timeField: string; + timeField?: string; extraFields?: string[]; textField?: string; } & PointStyleProps; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 5bb39f60d69354..76749bee5bc797 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -69,7 +69,7 @@ export type QueryPointEventAnnotationConfig = { id: string; type: QueryAnnotationType; filter: KibanaQueryOutput; - timeField: string; + timeField?: string; textField?: string; extraFields?: string[]; key: { @@ -82,6 +82,11 @@ export type EventAnnotationConfig = | RangeEventAnnotationConfig | QueryPointEventAnnotationConfig; +export interface EventAnnotationGroupConfig { + annotations: EventAnnotationConfig[]; + indexPatternId: string; +} + export type EventAnnotationArgs = | ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 3f03f1205fc85b..d25aea241b9ad7 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -8,6 +8,8 @@ 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, @@ -22,104 +24,157 @@ export function hasIcon(icon: string | undefined): icon is string { } export function getEventAnnotationService(): EventAnnotationServiceType { - return { - toExpression: (annotations) => { - const visibleAnnotations = annotations.filter(({ isHidden }) => !isHidden); - const [queryBasedAnnotations, manualBasedAnnotations] = partition( - visibleAnnotations, - isQueryAnnotationConfig - ); + const annotationsToExpression = (annotations: EventAnnotationConfig[]) => { + const visibleAnnotations = annotations.filter(({ isHidden }) => !isHidden); + const [queryBasedAnnotations, manualBasedAnnotations] = partition( + visibleAnnotations, + isQueryAnnotationConfig + ); - const expressions = []; + const expressions = []; - for (const annotation of manualBasedAnnotations) { - if (isRangeAnnotationConfig(annotation)) { - const { label, isHidden, color, key, outside, id } = annotation; - const { timestamp: time, endTimestamp: endTime } = key; - expressions.push({ - type: 'expression' as const, - chain: [ - { - type: 'function' as const, - function: 'manual_range_event_annotation', - arguments: { - id: [id], - time: [time], - endTime: [endTime], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationRangeColor], - outside: [Boolean(outside)], - isHidden: [Boolean(isHidden)], - }, - }, - ], - }); - } else { - const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility, id } = - annotation; - expressions.push({ - type: 'expression' as const, - chain: [ - { - type: 'function' as const, - function: 'manual_point_event_annotation', - arguments: { - id: [id], - time: [key.timestamp], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationColor], - lineWidth: [lineWidth || 1], - lineStyle: [lineStyle || 'solid'], - icon: hasIcon(icon) ? [icon] : ['triangle'], - textVisibility: [textVisibility || false], - isHidden: [Boolean(isHidden)], - }, + for (const annotation of manualBasedAnnotations) { + if (isRangeAnnotationConfig(annotation)) { + const { label, isHidden, color, key, outside, id } = annotation; + const { timestamp: time, endTimestamp: endTime } = key; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'manual_range_event_annotation', + arguments: { + id: [id], + time: [time], + endTime: [endTime], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationRangeColor], + outside: [Boolean(outside)], + isHidden: [Boolean(isHidden)], }, - ], - }); - } - } - - for (const annotation of queryBasedAnnotations) { - const { - id, - label, - isHidden, - color, - lineStyle, - lineWidth, - icon, - timeField, - textVisibility, - textField, - filter, - extraFields, - } = annotation; + }, + ], + }); + } else { + const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility, id } = + annotation; expressions.push({ type: 'expression' as const, chain: [ { type: 'function' as const, - function: 'query_point_event_annotation', + function: 'manual_point_event_annotation', arguments: { id: [id], - timeField: [timeField], + time: [key.timestamp], label: [label || defaultAnnotationLabel], color: [color || defaultAnnotationColor], lineWidth: [lineWidth || 1], lineStyle: [lineStyle || 'solid'], icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], - textField: textVisibility && textField ? [textField] : [], isHidden: [Boolean(isHidden)], - filter: filter ? [queryToAst(filter)] : [], - extraFields: extraFields || [], }, }, ], }); } - return expressions; + } + + for (const annotation of queryBasedAnnotations) { + const { + id, + label, + isHidden, + 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] : [], + isHidden: [Boolean(isHidden)], + filter: filter ? [queryToAst(filter)] : [], + extraFields: extraFields || [], + }, + }, + ], + }); + } + return expressions; + }; + return { + toExpression: annotationsToExpression, + toFetchExpression: ({ interval, groups }) => { + if (groups.length === 0) { + return []; + } + // types correction + const groupsExpressions = groups.map( + ({ annotations, indexPatternId }): ExpressionAstExpression => { + const indexPatternExpression: ExpressionAstExpression = { + type: 'expression', + chain: [ + { + type: 'function', + function: 'indexPatternLoad', + arguments: { + id: [indexPatternId], + }, + }, + ], + }; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'event_annotation_group', + arguments: { + dataView: [indexPatternExpression], + annotations: [...annotationsToExpression(annotations)], + }, + }, + ], + }; + } + ); + + const fetchExpression: ExpressionAstExpression = { + type: 'expression', + chain: [ + { type: 'function', function: 'kibana', arguments: {} }, + { + type: 'function', + function: 'fetch_event_annotations', + arguments: { + interval: [interval], + groups: [...groupsExpressions], + }, + }, + ], + }; + + return [fetchExpression]; }, }; } diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index 7cecd2b418af9e..cf3d759b7a324f 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -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[]; + toFetchExpression: (props: { + interval: string; + groups: EventAnnotationGroupConfig[]; + }) => ExpressionAstExpression[]; } diff --git a/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts b/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts index aafb59a97d42a9..b0fd170969325c 100644 --- a/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts +++ b/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts @@ -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', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index d5a7a79ca07ee9..72c0952bb008f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -103,6 +103,9 @@ export function columnToOperation( ? 'version' : undefined, hasTimeShift: Boolean(timeShift), + interval: isColumnOfType('date_histogram', column) + ? column.params.interval + : undefined, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index e90ed2c781d5a2..3950f9a02dadf2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -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, }, }, ], diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0923274b750aa5..b46845cabe68f4 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -631,6 +631,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 diff --git a/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap index 2c58f75779456c..80c6b826f024d3 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/xy/__snapshots__/to_expression.test.ts.snap @@ -5,6 +5,7 @@ Object { "chain": Array [ Object { "arguments": Object { + "annotations": Array [], "emphasizeFitting": Array [ true, ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index f31100aaa030d2..3b2c27a77f2b3d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -342,10 +342,36 @@ export const buildExpression = ( datasourceExpressionsByLayers[layer.layerId] ) ), - ...validAnnotationsLayers.map((layer) => - annotationLayerToExpression(layer, eventAnnotationService) - ), ], + annotations: validAnnotationsLayers.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, + })), + }), + }, + }, + ], + }, + ] + : [], }, }, ], From 50a93a253b98b9a58ae015cbbae774d0bdfdd91b Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 16:01:14 +0200 Subject: [PATCH 71/98] portal for kql input fix --- .../editor_frame/config_panel/dimension_container.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx index f3cfa9a97667cb..cd74119bbe3156 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx @@ -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; } From 871bd4e5525991672781c21b1dabd165c2e8efc4 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 16:51:20 +0200 Subject: [PATCH 72/98] timeField goes for default if not filled --- .../annotations_config_panel/query_annotation_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index b9decc4fac6ba4..75c1e3651eb2fd 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -63,7 +63,7 @@ export const ConfigPanelQueryAnnotation = ({ currentIndexPattern ); - const selectedField = annotation?.timeField; + const selectedField = annotation?.timeField || currentIndexPattern.timeFieldName; const fieldIsValid = selectedField ? Boolean(currentIndexPattern.getFieldByName(selectedField)) : true; From 621eda71cf97d24c5b7146061de496402d271bee Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 16:51:48 +0200 Subject: [PATCH 73/98] limit changes --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 71abf16bb8fcd2..a8a93882cdb198 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -130,7 +130,7 @@ pageLoadAssetSize: eventAnnotation: 19334 screenshotting: 22870 synthetics: 40958 - expressionXY: 36000 + expressionXY: 38000 kibanaUsageCollection: 16463 kubernetesSecurity: 77234 threatIntelligence: 29195 From 9815161171a218967669ca2b647775c0d99dc403 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 5 Sep 2022 17:03:57 +0200 Subject: [PATCH 74/98] handle ad-hoc data view references correctly --- x-pack/plugins/lens/public/state_management/selectors.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 9f0e178972d01a..4a2f4cdad2de90 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -137,7 +137,13 @@ export const selectSavedObjectFormat = createSelector( const { state: persistableState, savedObjectReferences } = activeVisualization.getPersistableState(visualization.state); persistibleVisualizationState = persistableState; - references.push(...savedObjectReferences); + savedObjectReferences.forEach((r) => { + if (r.type === 'index-pattern' && adHocDataViews[r.id]) { + internalReferences.push(r); + } else { + references.push(r); + } + }); } const persistableAdHocDataViews = Object.fromEntries( From bcd127a4458ee1cf5d61ec948b4a1406cee3afb1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 17:15:56 +0200 Subject: [PATCH 75/98] fix types --- .../fetch_event_annotations/request_event_annotations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 2716448110f120..5aae7c4f7993d3 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -90,11 +90,11 @@ export const requestEventAnnotations = ( return defer(async () => { const { aggs, dataViews, searchSource, getNow, uiSettings } = await getStartDependencies(); if (!input?.timeRange) { - return; + return null; } const dates = toAbsoluteDates(input?.timeRange); if (!dates) { - return; + return null; } const buckets = new TimeBuckets({ 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), From f87f33fac51d5a0c1d5130d3271a66904ad488ee Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 5 Sep 2022 17:16:35 +0200 Subject: [PATCH 76/98] adjust tests to datatable format (remove isHidden tests as it's filtered before) --- .../__snapshots__/xy_chart.test.tsx.snap | 12 +++---- .../public/components/xy_chart.test.tsx | 34 +++++-------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index 43dbace37905c3..da9491651613e5 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -101,9 +101,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, }, ] } @@ -124,7 +124,7 @@ exports[`XYChart component annotations should render grouped line annotations pr "position": "bottom", "textVisibility": undefined, "time": "2022-03-18T08:25:00.000Z", - "timebucket": 1647591900000, + "timebucket": 1647591917100, "type": "point", } } @@ -161,9 +161,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, }, ] } @@ -184,7 +184,7 @@ exports[`XYChart component annotations should render grouped line annotations wi "position": "bottom", "textVisibility": undefined, "time": "2022-03-18T08:25:00.000Z", - "timebucket": 1647591900000, + "timebucket": 1647591917100, "type": "point", } } diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index f22e7b4a6e656e..6dfac1b207a990 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -56,7 +56,6 @@ import { import { XYChart, XYChartRenderProps } from './xy_chart'; 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'; @@ -3064,6 +3063,13 @@ 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] ): AnnotationLayerConfigResult => ({ @@ -3082,7 +3088,7 @@ describe('XYChart component', () => { datatable: { type: 'datatable' as const, columns: [], - rows: [], + rows: annotationLayers.flatMap((l) => l.annotations.map(configToRowHelper)), }, }, }, @@ -3138,7 +3144,7 @@ describe('XYChart component', () => { // checking tooltip const renderLinks = mount(
{groupedAnnotation.prop('customTooltipDetails')!()}
); 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 1 2022-03-18T08:25:00.000Z Event 2 2022-03-18T08:25:00.020Z Event 3 2022-03-18T08:25:00.001Z' ); }); @@ -3164,28 +3170,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(); - 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', () => { From 7459129e239e89d5f9a7895f0ce24e22044416fd Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 6 Sep 2022 10:25:38 +0200 Subject: [PATCH 77/98] small refactors --- .../expression_xy/common/index.ts | 2 +- .../common/types/expression_renderers.ts | 2 +- .../public/components/annotations.tsx | 59 +++++++++++++------ .../public/components/xy_chart.tsx | 6 +- .../public/helpers/annotations.tsx | 8 +-- .../public/visualizations/xy/to_expression.ts | 13 +++- .../tooltip_annotation_panel.tsx | 35 +++++------ 7 files changed, 76 insertions(+), 49 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index da4c969f47a0c0..b39002c8549cb8 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -34,7 +34,7 @@ export type { DataLayerConfig, FittingFunction, AxisExtentConfig, - CollectiveConfig, + MergedAnnotation, LegendConfigResult, AxesSettingsConfig, XAxisConfigResult, diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index 8ee27ad7eab583..150e18c2b71b6d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -24,7 +24,7 @@ export interface XYRender { value: XYChartProps; } -export interface CollectiveConfig extends Omit { +export interface MergedAnnotation extends Omit { timebucket: number; position: 'bottom'; icon?: AvailableAnnotationIcon | string; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index ee8360d80ab658..c58498c1691133 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -21,22 +21,23 @@ import { import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem, 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 { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common'; import { ManualPointEventAnnotationRow } from '@kbn/event-annotation-plugin/common/manual_event_annotation/types'; -import type { CollectiveConfig } from '../../common'; +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; @@ -50,7 +51,7 @@ export interface AnnotationsProps { const createCustomTooltipDetails = ( config: ManualPointEventAnnotationArgs[], - formatter?: FieldFormat + timeFormatter?: FieldFormat ): AnnotationTooltipFormatter | undefined => () => { return ( @@ -65,7 +66,9 @@ const createCustomTooltipDetails = )} {label} - {formatter?.convert(time) || String(time)} + + {timeFormatter?.convert(time) || String(time)} +
))}
@@ -110,8 +113,12 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; export const getAnnotationsGroupedByInterval = ( annotations: ManualPointEventAnnotationRow[], - formatter?: FieldFormat + configs: EventAnnotationOutput[] | undefined, + columns: DatatableColumn[] | undefined, + formatFactory: FormatFactory, + timeFormatter?: FieldFormat ) => { + console.log(columns); const visibleGroupedConfigs = annotations.reduce>( (acc, current) => { const timebucket = moment(current.timebucket).valueOf(); @@ -122,24 +129,38 @@ export const getAnnotationsGroupedByInterval = ( }, {} ); - let collectiveConfig: CollectiveConfig; - return Object.entries(visibleGroupedConfigs).map(([timebucket, configArr]) => { - collectiveConfig = { - ...configArr[0], - icon: configArr[0].icon || 'triangle', + let mergedAnnotation: MergedAnnotation; + return Object.entries(visibleGroupedConfigs).map(([timebucket, rowsPerBucket]) => { + // get config from the annotation + // if textField is defined, get the value from the row + 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; + mergedAnnotation = { + ...firstRow, + label, + icon: firstRow.icon || 'triangle', timebucket: Number(timebucket), position: 'bottom', }; - if (configArr.length > 1) { - const commonStyles = getCommonStyles(configArr); - collectiveConfig = { - ...collectiveConfig, + if (rowsPerBucket.length > 1) { + const commonStyles = getCommonStyles(rowsPerBucket); + mergedAnnotation = { + ...mergedAnnotation, ...commonStyles, - icon: String(configArr.length), - customTooltipDetails: createCustomTooltipDetails(configArr, formatter), + label: '', + icon: String(rowsPerBucket.length), + customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, timeFormatter), }; } - return collectiveConfig; + return mergedAnnotation; }); }; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 761761f96d97ec..e5d9fa90dadbd0 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -397,14 +397,18 @@ export function XYChart({ }; const referenceLineLayers = getReferenceLayers(layers); - const [rangeAnnotations, lineAnnotations] = partition( annotations?.datatable.rows, isRangeAnnotation ); + const annotationsConfigs = annotations?.layers.flatMap((l) => l.annotations); + const groupedLineAnnotations = getAnnotationsGroupedByInterval( lineAnnotations as ManualPointEventAnnotationRow[], + annotationsConfigs, + annotations?.datatable.columns, + formatFactory, xAxisFormatter ); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index c4aebbfb969023..132c88e4f863dc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -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; +type PartialMergedAnnotation = Pick; 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, + visualConfigs: Array, axesMap: AxesMap, shouldRotate: boolean ) => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index 3b2c27a77f2b3d..796f2879024981 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -8,7 +8,11 @@ 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 { @@ -363,7 +367,12 @@ export const buildExpression = ( 'auto', groups: validAnnotationsLayers.map((layer) => ({ indexPatternId: layer.indexPatternId, - annotations: layer.annotations, + annotations: layer.annotations.filter( + (a) => + isManualPointAnnotationConfig(a) || + isRangeAnnotationConfig(a) || + (a.filter && a.filter?.query !== '') + ), })), }), }, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx index 309feaf2e68705..e4945f42f8089e 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/tooltip_annotation_panel.tsx @@ -94,6 +94,18 @@ export function TooltipSection({ [localValues, indexPattern, handleInputChange] ); + const newBucketButton = ( + { + handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]); + }} + label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { + defaultMessage: 'Add field', + })} + /> + ); + if (localValues.length === 0) { return ( <> @@ -104,18 +116,7 @@ export function TooltipSection({ })} - { - handleInputChange([ - ...localValues, - { id: generateId(), value: undefined, isNew: true }, - ]); - }} - label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { - defaultMessage: 'Add field', - })} - /> + {newBucketButton} ); } @@ -225,15 +226,7 @@ export function TooltipSection({ ); })} - { - handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]); - }} - data-test-subj={`lnsXY-annotation-tooltip-addField`} - label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { - defaultMessage: 'Add field', - })} - /> + {newBucketButton} ); } From 2f63f82013d7cf9f0735588c9e1af297dce35d2c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 6 Sep 2022 12:36:29 +0200 Subject: [PATCH 78/98] fix loading on dashboard --- .../editor_frame_service/editor_frame/state_helpers.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index f6b9fc01ab789e..aab10d783ab520 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -300,7 +300,7 @@ export async function persistedStateToExpression( ): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> { const { state: { - visualization: visualizationState, + visualization: persistedVisualizationState, datasourceStates: persistedDatasourceStates, adHocDataViews, internalReferences, @@ -323,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, From bf634d9e85d1aac95d5c7e44f887dd2a8fa40263 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 6 Sep 2022 13:32:02 +0200 Subject: [PATCH 79/98] empty is invalid (?) tbd --- .../public/components/annotations.tsx | 1 - .../public/visualizations/xy/to_expression.ts | 70 ++++++++++--------- .../query_annotation_panel.tsx | 2 +- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index c58498c1691133..769f186e926c8f 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -118,7 +118,6 @@ export const getAnnotationsGroupedByInterval = ( formatFactory: FormatFactory, timeFormatter?: FieldFormat ) => { - console.log(columns); const visibleGroupedConfigs = annotations.reduce>( (acc, current) => { const timebucket = moment(current.timebucket).valueOf(); diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index 796f2879024981..ed571ef1a1473b 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -15,6 +15,7 @@ import { } 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, @@ -245,6 +246,11 @@ export const buildExpression = ( }); } + const isValidAnnotation = (a: EventAnnotationConfig) => + isManualPointAnnotationConfig(a) || + isRangeAnnotationConfig(a) || + (a.filter && a.filter?.query !== ''); + return { type: 'expression', chain: [ @@ -347,40 +353,38 @@ export const buildExpression = ( ) ), ], - annotations: validAnnotationsLayers.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( - (a) => - isManualPointAnnotationConfig(a) || - isRangeAnnotationConfig(a) || - (a.filter && a.filter?.query !== '') - ), - })), - }), + 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), + })), + }), + }, }, - }, - ], - }, - ] - : [], + ], + }, + ] + : [], }, }, ], diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 75c1e3651eb2fd..1616fa10ab40b2 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -115,7 +115,7 @@ export const ConfigPanelQueryAnnotation = ({ }} disableAutoFocus indexPatternTitle={frame.dataViews.indexPatterns[layer.indexPatternId].title} - isInvalid={!isQueryInputValid} + isInvalid={!isQueryInputValid || inputQuery.query === ''} onSubmit={() => {}} data-test-subj="annotation-query-based-query-input" placeholder={ From 9ab8e52e99a67af1ccce0803b0001d9f9b6cf173 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 6 Sep 2022 15:15:51 +0200 Subject: [PATCH 80/98] new tooltip --- .../common/types/expression_renderers.ts | 3 +- .../public/components/annotations.scss | 13 +- .../public/components/annotations.tsx | 148 +++++++++++++----- .../public/components/xy_chart.tsx | 4 +- src/plugins/event_annotation/common/index.ts | 2 +- .../common/manual_event_annotation/types.ts | 7 +- .../shared/marker_decoration_settings.tsx | 22 ++- 7 files changed, 148 insertions(+), 51 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index 150e18c2b71b6d..431c2721fa1c1a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -28,5 +28,6 @@ export interface MergedAnnotation extends Omit + rows: PointEventAnnotationRow[], + formatFactory: FormatFactory, + columns: DatatableColumn[] | undefined + ): AnnotationTooltipFormatter => () => { + if (rows.length === 1) { + return ( +
+ {rows.map(({ icon, label, time, color }) => ( +
+ + {hasIcon(icon) && ( + + + + )} + {label} + +
+ ))} +
+ ); + } + const groupedConfigs = groupBy(rows, 'id'); + const lastElement = rows[rows.length - 1]; return ( -
- {config.map(({ icon, label, time, color }) => ( -
- - {hasIcon(icon) && ( - - - - )} - {label} - - - {timeFormatter?.convert(time) || String(time)} - +
+ {Object.values(groupedConfigs).map((group) => { + const firstElement = group[0]; + const extraFields = Object.keys(firstElement) + .filter((key) => key.startsWith('field:')) + .map((key) => { + const columnFormatter = columns?.find((c) => c.id === key)?.meta?.params; + const formatter = columnFormatter && formatFactory(columnFormatter); + return { + key, + name: key.replace('field:', ''), + formatter, + }; + }); + + return ( +
+ + {hasIcon(firstElement.icon) && ( + + + + )} + {firstElement.label} + + + {group.map((el) => ( +
+ + {moment(el.time).format('YYYY-MM-DD, hh:mm:ss')} +
+ {extraFields.map((field) => ( +
+ {field.name} : + {field.formatter + ? field.formatter.convert(el[field.key]) + : el[field.key]} +
+ ))} +
+
+
+ ))} +
+
+ ); + })} + {lastElement.skippedCount && ( +
+ ... +{lastElement.skippedCount} more
- ))} + )}
); }; @@ -112,13 +178,13 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8; export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; export const getAnnotationsGroupedByInterval = ( - annotations: ManualPointEventAnnotationRow[], + annotations: PointEventAnnotationRow[], configs: EventAnnotationOutput[] | undefined, columns: DatatableColumn[] | undefined, formatFactory: FormatFactory, timeFormatter?: FieldFormat ) => { - const visibleGroupedConfigs = annotations.reduce>( + const visibleGroupedConfigs = annotations.reduce>( (acc, current) => { const timebucket = moment(current.timebucket).valueOf(); return { @@ -148,15 +214,18 @@ export const getAnnotationsGroupedByInterval = ( icon: firstRow.icon || 'triangle', timebucket: Number(timebucket), position: 'bottom', + customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, formatFactory, columns), + isGrouped: false, }; if (rowsPerBucket.length > 1) { const commonStyles = getCommonStyles(rowsPerBucket); mergedAnnotation = { ...mergedAnnotation, ...commonStyles, + isGrouped: true, label: '', icon: String(rowsPerBucket.length), - customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, timeFormatter), + customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, formatFactory, columns), }; } return mergedAnnotation; @@ -181,20 +250,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 ( ) : 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, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index e5d9fa90dadbd0..8033d6cd17d879 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -35,7 +35,7 @@ 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 } 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 { @@ -405,7 +405,7 @@ export function XYChart({ const annotationsConfigs = annotations?.layers.flatMap((l) => l.annotations); const groupedLineAnnotations = getAnnotationsGroupedByInterval( - lineAnnotations as ManualPointEventAnnotationRow[], + lineAnnotations as PointEventAnnotationRow[], annotationsConfigs, annotations?.datatable.columns, formatFactory, diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index a075fe0c2161f9..779d0e13e78134 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -12,7 +12,7 @@ export type { ManualRangeEventAnnotationArgs, ManualRangeEventAnnotationOutput, ManualRangeEventAnnotationRow, - ManualPointEventAnnotationRow, + PointEventAnnotationRow, } from './manual_event_annotation/types'; export type { QueryPointEventAnnotationArgs, diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index 235c0b03ff39d1..a74ba5723b61ed 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -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; export type ManualRangeEventAnnotationArgs = { id: string; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx index c0e133b814780e..b347a39c788cea 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx @@ -161,10 +161,24 @@ export function TextDecorationSetting({ : `${idPrefix}${selectedVisibleOption}` } onChange={(id) => { - setConfig({ - textVisibility: id !== `${idPrefix}none`, - }); - setVisibleOption(id.replace(idPrefix, '') as 'none' | 'name' | 'field'); + const chosenOption = id.replace(idPrefix, '') as 'none' | 'name' | 'field'; + if (chosenOption === 'none') { + setConfig({ + textVisibility: false, + textField: undefined, + }); + } else if (chosenOption === 'field') { + setConfig({ + textVisibility: true, + }); + } else if (chosenOption === 'name') { + setConfig({ + textVisibility: true, + textField: undefined, + }); + } + + setVisibleOption(chosenOption); }} isFullWidth /> From 278541b39c17a521039551ad3f8e91c4d569e310 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 6 Sep 2022 17:27:44 +0200 Subject: [PATCH 81/98] emptyDatatable --- .../request_event_annotations.ts | 46 +++----------- .../common/fetch_event_annotations/utils.ts | 63 ++++++++++++++++++- .../event_annotation_service/service.tsx | 2 +- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 5aae7c4f7993d3..468b5d089819a4 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -15,20 +15,18 @@ import { ExpressionValueSearchContext, parseEsInterval, AggConfigs, - TimeBuckets, - UI_SETTINGS, } from '@kbn/data-plugin/common'; -import type { TimeRange } from '@kbn/es-query'; + import { ExecutionContext } from '@kbn/expressions-plugin/common'; import moment from 'moment'; import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts'; import { Adapters } from '@kbn/inspector-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import dateMath from '@kbn/datemath'; import { handleRequest } from './handle_request'; import { ANNOTATIONS_PER_BUCKET, + getCalculatedInterval, isInRange, isManualAnnotation, isManualPointAnnotation, @@ -61,20 +59,7 @@ export function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } - -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(), - }; -} +const emptyDatatable = { rows: [], columns: [], type: 'datatable' }; export const requestEventAnnotations = ( input: ExpressionValueSearchContext | null, @@ -90,26 +75,13 @@ export const requestEventAnnotations = ( return defer(async () => { const { aggs, dataViews, searchSource, getNow, uiSettings } = await getStartDependencies(); if (!input?.timeRange) { - return null; - } - const dates = toAbsoluteDates(input?.timeRange); - if (!dates) { - return null; + return emptyDatatable; } - 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(args.interval); - buckets.setBounds({ - min: moment(dates.from), - max: moment(dates.to), - }); - - const interval = buckets.getInterval().expression; + const interval = getCalculatedInterval(uiSettings, args.interval, input?.timeRange); + if (!interval) { + return emptyDatatable; + } const uniqueDataViewsToLoad = args.groups .map((g) => g.dataView.value) @@ -133,7 +105,7 @@ export const requestEventAnnotations = ( if (!queryGroups.length) { return manualAnnotationDatatableRows.length ? wrapRowsInDatatable(manualAnnotationDatatableRows) - : null; + : emptyDatatable; } const createEsaggsSingleRequest = async ({ diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts index 9f53d2ca452add..caf6055c607933 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts @@ -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; }; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index d25aea241b9ad7..ffa611843e1cea 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -128,7 +128,7 @@ export function getEventAnnotationService(): EventAnnotationServiceType { if (groups.length === 0) { return []; } - // types correction + const groupsExpressions = groups.map( ({ annotations, indexPatternId }): ExpressionAstExpression => { const indexPatternExpression: ExpressionAstExpression = { From eed9eb213b556631e9cb642d15d53634c91c9e5b Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 10:08:31 +0200 Subject: [PATCH 82/98] :recycle: Flip field + query inputs --- .../query_annotation_panel.tsx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 1616fa10ab40b2..12947d185fdcc9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -69,35 +69,6 @@ export const ConfigPanelQueryAnnotation = ({ : true; return ( <> - - - + + + ); }; From bfb892d97c6e6be0690e25b0bc11e7cb22c33a92 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 11:00:00 +0200 Subject: [PATCH 83/98] :label: Fix type issue --- .../event_annotation/common/fetch_event_annotations/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/types.ts b/src/plugins/event_annotation/common/fetch_event_annotations/types.ts index d323814e988953..79921cd2e13994 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/types.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/types.ts @@ -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; +export type FetchEventAnnotationsOutput = Observable< + Datatable | { rows: never[]; columns: never[]; type: string } +>; export interface FetchEventAnnotationsArgs { groups: EventAnnotationGroupOutput[]; From 3d71bd5ea7c04769a1f4db002b8b315580bf4e2b Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 12:44:18 +0200 Subject: [PATCH 84/98] :sparkles: Add field validation for text and tooltip fields --- .../public/visualizations/xy/state_helpers.ts | 89 ++++++++++++++++++- .../visualizations/xy/visualization.tsx | 86 +++++++----------- 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts index 6c1a287890a5f7..29d60894de90de 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts @@ -7,7 +7,14 @@ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { SavedObjectReference } from '@kbn/core/public'; -import type { FramePublicAPI, DatasourcePublicAPI } from '../../types'; +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, @@ -17,6 +24,7 @@ import { YConfig, XYState, XYPersistedState, + State, } from './types'; import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers'; @@ -151,3 +159,82 @@ export function injectReferences( }), }; } + +export function validateColumn( + state: State, + frame: Pick, + 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.textFieldNotFound', { + 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, + }; +} diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 2208db8e71bba2..a39a2094b5a094 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -13,10 +13,7 @@ import { i18n } from '@kbn/i18n'; import type { PaletteRegistry } from '@kbn/coloring'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { CoreStart, ThemeServiceStart } from '@kbn/core/public'; -import { - EventAnnotationServiceType, - isQueryAnnotationConfig, -} from '@kbn/event-annotation-plugin/public'; +import { EventAnnotationServiceType } from '@kbn/event-annotation-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'; @@ -39,7 +36,12 @@ import { PersistedState, } from './types'; import { layerTypes } from '../../../common'; -import { extractReferences, injectReferences, 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'; @@ -83,7 +85,6 @@ import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; 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'; -import { validateQuery } from '../../shared_components'; const XY_ID = 'lnsXY'; export const getXyVisualization = ({ @@ -485,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 { @@ -636,38 +639,11 @@ export const getXyVisualization = ({ ), validateColumn(state, frame, layerId, columnId, group) { - if (group?.invalid) { - return { - invalid: true, - invalidMessage: group.invalidMessage, - }; - } - 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 { invalid, invalidMessages } = validateColumn(state, frame, layerId, columnId, group); + if (!invalid) { + return { invalid }; } - const { dataViews } = frame || {}; - const layerDataView = dataViews.indexPatterns[layer.indexPatternId]; - - if (annotation.timeField && !Boolean(layerDataView.getFieldByName(annotation.timeField))) { - return { - invalid: !Boolean(layerDataView.getFieldByName(annotation.timeField)), - invalidMessage: 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); - return { - invalid: !isValid, - invalidMessage: error, - }; + return { invalid, invalidMessage: invalidMessages![0] }; }, getErrorMessages(state, frame) { @@ -682,26 +658,28 @@ export const getXyVisualization = ({ if (dataViews) { annotationLayers.forEach((layer) => { layer.annotations.forEach((annotation) => { - const validatedColumn = this.validateColumn?.( + const validatedColumn = validateColumn?.( state, { dataViews }, layer.layerId, annotation.id ); - if (validatedColumn?.invalid && validatedColumn.invalidMessage) { - errors.push({ - shortMessage: validatedColumn.invalidMessage, - longMessage: ( - - ), - }); + if (validatedColumn?.invalid && validatedColumn.invalidMessages?.length) { + errors.push( + ...validatedColumn.invalidMessages.map((invalidMessage) => ({ + shortMessage: invalidMessage, + longMessage: ( + + ), + })) + ); } }); }); From 56d17b735229726ed64c908532bd1e36a6476aff Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 13:07:45 +0200 Subject: [PATCH 85/98] tooltip for single annotation --- .../public/components/annotations.scss | 7 +- .../public/components/annotations.tsx | 159 ++++++++++-------- .../public/components/xy_chart.tsx | 3 +- 3 files changed, 93 insertions(+), 76 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss index f741731e16816d..3fb13d7b22195e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss @@ -23,11 +23,8 @@ ); } -.xyAnnotationTooltip__skippedCount { - text-align: right; - margin-right: 0; -} - .xyAnnotationTooltip__group__singleItem+.xyAnnotationTooltip__group__singleItem { border-top: $euiBorderThin; + padding-top: $euiSizeXS; + margin-top: $euiSizeXS; } \ No newline at end of file diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index e60635147425ba..a0c6109a21d237 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -32,6 +32,7 @@ import { } from '@kbn/event-annotation-plugin/public'; 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'; @@ -48,6 +49,74 @@ export interface AnnotationsProps { outsideDimension: number; } +const TooltipAnnotationHeader = ({ + row: { label, color, icon }, +}: { + row: PointEventAnnotationRow; +}) => ( +
+ + {hasIcon(icon) && ( + + + + )} + {label} + +
+); + +const TooltipAnnotationDetails = ({ + row, + extraFields, + isGrouped, +}: { + row: PointEventAnnotationRow; + extraFields: Array<{ + key: string; + name: string; + formatter: FieldFormat | undefined; + }>; + isGrouped?: boolean; +}) => { + return ( +
+ + {isGrouped &&
{moment(row.time).format('YYYY-MM-DD, hh:mm:ss')}
} + +
+ {extraFields.map((field) => ( +
+ {field.name}:{' '} + {field.formatter ? field.formatter.convert(row[field.key]) : row[field.key]} +
+ ))} +
+
+
+ ); +}; + +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 = ( rows: PointEventAnnotationRow[], @@ -55,52 +124,17 @@ const createCustomTooltipDetails = columns: DatatableColumn[] | undefined ): AnnotationTooltipFormatter => () => { - if (rows.length === 1) { - return ( -
- {rows.map(({ icon, label, time, color }) => ( -
- - {hasIcon(icon) && ( - - - - )} - {label} - -
- ))} -
- ); - } const groupedConfigs = groupBy(rows, 'id'); const lastElement = rows[rows.length - 1]; return (
{Object.values(groupedConfigs).map((group) => { const firstElement = group[0]; - const extraFields = Object.keys(firstElement) - .filter((key) => key.startsWith('field:')) - .map((key) => { - const columnFormatter = columns?.find((c) => c.id === key)?.meta?.params; - const formatter = columnFormatter && formatFactory(columnFormatter); - return { - key, - name: key.replace('field:', ''), - formatter, - }; - }); + const extraFields = getExtraFields(firstElement, formatFactory, columns); return (
- - {hasIcon(firstElement.icon) && ( - - - - )} - {firstElement.label} - + - {group.map((el) => ( -
- - {moment(el.time).format('YYYY-MM-DD, hh:mm:ss')} -
- {extraFields.map((field) => ( -
- {field.name} : - {field.formatter - ? field.formatter.convert(el[field.key]) - : el[field.key]} -
- ))} -
-
-
+ {group.map((row) => ( + 1} + row={row} + extraFields={extraFields} + /> ))}
); })} {lastElement.skippedCount && ( -
- ... +{lastElement.skippedCount} more +
+
)}
@@ -181,8 +206,7 @@ export const getAnnotationsGroupedByInterval = ( annotations: PointEventAnnotationRow[], configs: EventAnnotationOutput[] | undefined, columns: DatatableColumn[] | undefined, - formatFactory: FormatFactory, - timeFormatter?: FieldFormat + formatFactory: FormatFactory ) => { const visibleGroupedConfigs = annotations.reduce>( (acc, current) => { @@ -194,10 +218,7 @@ export const getAnnotationsGroupedByInterval = ( }, {} ); - let mergedAnnotation: MergedAnnotation; return Object.entries(visibleGroupedConfigs).map(([timebucket, rowsPerBucket]) => { - // get config from the annotation - // if textField is defined, get the value from the row const firstRow = rowsPerBucket[0]; const config = configs?.find((c) => c.id === firstRow.id); @@ -208,7 +229,7 @@ export const getAnnotationsGroupedByInterval = ( textField && formatter && `field:${textField}` in firstRow ? formatter.convert(firstRow[`field:${textField}`]) : firstRow.label; - mergedAnnotation = { + const mergedAnnotation = { ...firstRow, label, icon: firstRow.icon || 'triangle', @@ -219,13 +240,11 @@ export const getAnnotationsGroupedByInterval = ( }; if (rowsPerBucket.length > 1) { const commonStyles = getCommonStyles(rowsPerBucket); - mergedAnnotation = { + return { ...mergedAnnotation, ...commonStyles, isGrouped: true, - label: '', icon: String(rowsPerBucket.length), - customTooltipDetails: createCustomTooltipDetails(rowsPerBucket, formatFactory, columns), }; } return mergedAnnotation; @@ -266,7 +285,7 @@ export const Annotations = ({ config: annotation, isHorizontal: !isHorizontal, hasReducedPadding, - label: annotation.label, + label: !isGrouped ? annotation.label : undefined, rotateClassName: isHorizontal ? 'xyAnnotationIcon_rotate90' : undefined, }} /> @@ -276,7 +295,9 @@ export const Annotations = ({ !simpleView ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index bd80b2b5f7a19f..9fb415308635e9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -408,8 +408,7 @@ export function XYChart({ lineAnnotations as PointEventAnnotationRow[], annotationsConfigs, annotations?.datatable.columns, - formatFactory, - xAxisFormatter + formatFactory ); const visualConfigs = [ From 705edbb6b8c4fff408ac7074b9fca176fc2eb48b Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 13:14:29 +0200 Subject: [PATCH 86/98] fix tests --- .../components/__snapshots__/xy_chart.test.tsx.snap | 12 ++++++++++-- .../expression_xy/public/components/annotations.tsx | 6 ++---- .../public/components/xy_chart.test.tsx | 2 +- .../fetch_event_annotations.test.ts | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index da9491651613e5..f178c870cffd83 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -2,6 +2,7 @@ exports[`XYChart component annotations should render basic line annotation 1`] = ` } markerPosition="top" + placement="bottom" style={ Object { "line": Object { @@ -118,6 +122,7 @@ 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, @@ -130,7 +135,6 @@ exports[`XYChart component annotations should render grouped line annotations pr } 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 { @@ -178,6 +183,7 @@ 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, @@ -190,7 +196,6 @@ exports[`XYChart component annotations should render grouped line annotations wi } 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`] = ` { return ( -
+
{isGrouped &&
{moment(row.time).format('YYYY-MM-DD, hh:mm:ss')}
} @@ -144,6 +141,7 @@ const createCustomTooltipDetails = > {group.map((row) => ( 1} row={row} extraFields={extraFields} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 07f07dbd72c88a..dec3fa42f4f12d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -3144,7 +3144,7 @@ describe('XYChart component', () => { // checking tooltip const renderLinks = mount(
{groupedAnnotation.prop('customTooltipDetails')!()}
); expect(renderLinks.text()).toEqual( - ' Event 1 2022-03-18T08:25:00.000Z Event 2 2022-03-18T08:25:00.020Z Event 3 2022-03-18T08:25:00.001Z' + ' Event 12022-03-18, 04:25:002022-03-18, 04:25:002022-03-18, 04:25:00' ); }); diff --git a/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts b/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts index b0fd170969325c..271ab5910afdf9 100644 --- a/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts +++ b/src/plugins/event_annotation/public/fetch_event_annotations/fetch_event_annotations.test.ts @@ -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', () => { From c38388551fa2241d298acc6d80975a0dd06a55d1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 13:40:40 +0200 Subject: [PATCH 87/98] fix for non--timefilter dataview --- .../fetch_event_annotations/request_event_annotations.ts | 8 ++++++-- .../annotations_config_panel/query_annotation_panel.tsx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 468b5d089819a4..91ffb6ff2a0c9a 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -304,8 +304,12 @@ function regroupForRequestOptimization( return acc; } else { const dataView = loadedDataViews.find((dv) => dv.id === g.dataView.value.id)!; - const timeField = current.timeField ?? dataView.timeFieldName; - // get default timefield if not defined + + 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) { diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx index 12947d185fdcc9..c9cd0bbec7d350 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx @@ -63,7 +63,8 @@ export const ConfigPanelQueryAnnotation = ({ currentIndexPattern ); - const selectedField = annotation?.timeField || currentIndexPattern.timeFieldName; + const selectedField = + annotation?.timeField || currentIndexPattern.timeFieldName || options[0]?.value.field; const fieldIsValid = selectedField ? Boolean(currentIndexPattern.getFieldByName(selectedField)) : true; From 1349f0598f8b5a4a59c33745efacd0bf0c58a072 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 14:12:23 +0200 Subject: [PATCH 88/98] fix annotations test - the cause was that we now don't display label for aggregated annotations ever --- x-pack/test/functional/apps/lens/group3/annotations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/group3/annotations.ts b/x-pack/test/functional/apps/lens/group3/annotations.ts index d6197cc2e39ab8..e139677737b426 100644 --- a/x-pack/test/functional/apps/lens/group3/annotations.ts +++ b/x-pack/test/functional/apps/lens/group3/annotations.ts @@ -76,7 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ) ).to.be(true); await PageObjects.lens.closeDimensionEditor(); - await testSubjects.existOrFail('xyVisAnnotationText'); await testSubjects.existOrFail('xyVisGroupedAnnotationIcon'); }); }); From 7fbbb1170eabf0b4b8c503d55df10143721ce339 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 14:21:50 +0200 Subject: [PATCH 89/98] use eui elements --- .../public/components/annotations.scss | 10 +----- .../public/components/annotations.tsx | 34 ++++++++++++++----- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss index 3fb13d7b22195e..6b0de17fa58d62 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss @@ -18,13 +18,5 @@ } .xyAnnotationTooltipDetail { - padding: $euiSizeXS ( - $euiSizeXS * 2 - ); + padding: $euiSizeXS ($euiSizeXS * 2); } - -.xyAnnotationTooltip__group__singleItem+.xyAnnotationTooltip__group__singleItem { - border-top: $euiBorderThin; - padding-top: $euiSizeXS; - margin-top: $euiSizeXS; -} \ No newline at end of file diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index bcd1b2435fd592..51e810cecfbae2 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -19,7 +19,14 @@ import { RectAnnotation, } from '@elastic/charts'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; import type { EventAnnotationOutput, ManualPointEventAnnotationArgs, @@ -80,7 +87,7 @@ const TooltipAnnotationDetails = ({ isGrouped?: boolean; }) => { return ( -
+
{isGrouped &&
{moment(row.time).format('YYYY-MM-DD, hh:mm:ss')}
} @@ -139,13 +146,22 @@ const createCustomTooltipDetails = borderRadius="none" hasBorder={true} > - {group.map((row) => ( - 1} - row={row} - extraFields={extraFields} - /> + {group.map((row, index) => ( + <> + {index > 0 && ( + <> + + + + + )} + 1} + row={row} + extraFields={extraFields} + /> + ))}
From 0fe367d84f7126914014692bcc4fa1341e724520 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 14:39:05 +0200 Subject: [PATCH 90/98] newline problem solved --- .../expression_xy/public/components/annotations.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss index 6b0de17fa58d62..e1e58252b5de78 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss @@ -19,4 +19,4 @@ .xyAnnotationTooltipDetail { padding: $euiSizeXS ($euiSizeXS * 2); -} +} \ No newline at end of file From 3647032afa43cb2916cd3c425d7fb6a8243c2afd Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 14:50:12 +0200 Subject: [PATCH 91/98] :white_check_mark: Add more error tests --- .../visualizations/xy/visualization.test.ts | 94 +++++++++++++++++++ .../visualizations/xy/visualization.tsx | 2 +- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 24e200359aff99..a68eabda65d7ee 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -36,6 +36,8 @@ 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', @@ -2433,6 +2435,98 @@ describe('xy_visualization', () => { }, ]); }); + + describe('Annotation layers', () => { + function createStateWithAnnotationProps(annotation: Partial) { + 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', () => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index a39a2094b5a094..4e9a2e55e19ad9 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -658,7 +658,7 @@ export const getXyVisualization = ({ if (dataViews) { annotationLayers.forEach((layer) => { layer.annotations.forEach((annotation) => { - const validatedColumn = validateColumn?.( + const validatedColumn = validateColumn( state, { dataViews }, layer.layerId, From 08deb3f3d124d36c5e3a74115a089f8a3badc54d Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 14:54:17 +0200 Subject: [PATCH 92/98] :ok_hand: Rename migration state version type --- .../make_lens_embeddable_factory.ts | 12 +++++------ .../server/migrations/common_migrations.ts | 20 +++++++++---------- .../saved_object_migrations.test.ts | 10 +++++----- .../migrations/saved_object_migrations.ts | 16 +++++++-------- .../plugins/lens/server/migrations/types.ts | 8 ++++---- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index cb19b4d342cb94..fae6a588ffa66a 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -38,14 +38,14 @@ import { LensDocShape713, LensDocShape715, LensDocShape810, - LensDocShape840, + LensDocShape850, LensDocShapePre712, VisState716, VisState810, - VisState840, + VisState850, VisStatePre715, VisStatePre830, - XYVisStatePre840, + XYVisStatePre850, } from '../migrations/types'; import { extract, inject } from '../../common/embeddable_factory'; @@ -138,16 +138,16 @@ export const makeLensEmbeddableFactory = }, '8.5.0': (state) => { const lensState = state as unknown as { - attributes: LensDocShape840; + attributes: LensDocShape850; references: SavedObjectReference[] | undefined; }; let migratedLensState = commonMigrateMetricIds( lensState.attributes - ) as LensDocShape840; + ) as LensDocShape850; migratedLensState = commonExplicitAnnotationType(migratedLensState); const migratedReferences = commonAnnotationAddDataViewIdReferences( - migratedLensState as LensDocShape840, + migratedLensState as LensDocShape850, lensState.references ); return { diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 1de3eacc4d65eb..fe0b766b69e5b9 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -31,9 +31,9 @@ import { LensDocShape810, LensDocShape830, VisStatePre830, - XYVisStatePre840, - VisState840, - LensDocShape840, + XYVisStatePre850, + VisState850, + LensDocShape850, } from './types'; import { DOCUMENT_FIELD_NAME, layerTypes, LegacyMetricState } from '../../common'; import { LensDocShape } from './saved_object_migrations'; @@ -425,14 +425,14 @@ export const commonFixValueLabelsInXY = ( }; export const commonExplicitAnnotationType = ( - attributes: LensDocShape840 -): LensDocShape840 => { + attributes: LensDocShape850 +): LensDocShape850 => { // Skip the migration heavy part if not XY or it does not contain annotations if ( attributes.visualizationType !== 'lnsXY' || attributes.state.visualization.layers.every((l) => l.layerType !== 'annotations') ) { - return attributes as LensDocShape840; + return attributes as LensDocShape850; } const newAttributes = cloneDeep(attributes); const { visualization } = newAttributes.state; @@ -458,7 +458,7 @@ export const commonExplicitAnnotationType = ( }; export const commonAnnotationAddDataViewIdReferences = ( - attributes: LensDocShape840, + attributes: LensDocShape850, references: SavedObjectReference[] | undefined ): SavedObjectReference[] | undefined => { if ( @@ -484,15 +484,15 @@ export const commonAnnotationAddDataViewIdReferences = ( }; export const commonMigrateMetricIds = ( - attributes: LensDocShape840 -): LensDocShape840 => { + attributes: LensDocShape850 +): LensDocShape850 => { const typeMappings = { lnsMetric: 'lnsLegacyMetric', lnsMetricNew: 'lnsMetric', } as Record; if (!attributes.visualizationType || !(attributes.visualizationType in typeMappings)) { - return attributes as LensDocShape840; + return attributes as LensDocShape850; } const newAttributes = cloneDeep(attributes); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 8458dd999b1baa..f543f33fd68fd8 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -22,9 +22,9 @@ import { VisState810, VisState820, VisState830, - LensDocShape840, - XYVisStatePre840, - VisState840, + LensDocShape850, + XYVisStatePre850, + VisState850, } from './types'; import { layerTypes, LegacyMetricState } from '../../common'; import { Filter } from '@kbn/es-query'; @@ -2316,13 +2316,13 @@ describe('Lens migrations', () => { }, }, }, - } as unknown as SavedObjectUnsanitizedDoc>; + } as unknown as SavedObjectUnsanitizedDoc>; it('migrates existing annotation events as manual type', () => { const result = migrations['8.5.0'](example, context) as ReturnType< SavedObjectMigrationFn >; - const visState = result.attributes.state.visualization as VisState840; + const visState = result.attributes.state.visualization as VisState850; const [dataLayer, annotationLayer] = visState.layers; expect(dataLayer).toEqual({ layerType: 'data' }); expect(annotationLayer).toEqual({ diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 2c9de8387fbe0b..095fedd072cf03 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -34,9 +34,9 @@ import { XYVisualizationState830, VisState810, VisState820, - XYVisStatePre840, - LensDocShape840, - VisState840, + XYVisStatePre850, + LensDocShape850, + VisState850, } from './types'; import { commonRenameOperationsForFormula, @@ -520,22 +520,22 @@ const preserveOldLegendSizeDefault: SavedObjectMigrationFn ({ ...doc, attributes: commonPreserveOldLegendSizeDefault(doc.attributes) }); const addEventAnnotationType: SavedObjectMigrationFn< - LensDocShape840, - LensDocShape840 + LensDocShape850, + LensDocShape850 > = (doc) => { const newDoc = cloneDeep(doc); return { ...newDoc, attributes: commonExplicitAnnotationType(newDoc.attributes) }; }; const addEventAnnotationDataViewReferences: SavedObjectMigrationFn< - LensDocShape840, - LensDocShape840 + LensDocShape850, + LensDocShape850 > = (doc) => ({ ...doc, references: commonAnnotationAddDataViewIdReferences(doc.attributes, doc.references), }); -const migrateMetricIds: SavedObjectMigrationFn = (doc) => ({ +const migrateMetricIds: SavedObjectMigrationFn = (doc) => ({ ...doc, attributes: commonMigrateMetricIds(doc.attributes), }); diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index f39eba9f9c9c98..9746e31d829325 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -270,7 +270,7 @@ export interface XYVisualizationState830 extends VisState820 { export type VisStatePre830 = XYVisualizationStatePre830; export type VisState830 = XYVisualizationState830; -export interface XYVisStatePre840 { +export interface XYVisStatePre850 { layers: Array< | { layerId: string; @@ -284,7 +284,7 @@ export interface XYVisStatePre840 { >; } -export interface XYVisState840 { +export interface XYVisState850 { layers: Array< | { layerId: string; @@ -297,5 +297,5 @@ export interface XYVisState840 { } >; } -export type VisState840 = XYVisState840; -export type LensDocShape840 = LensDocShape830; +export type VisState850 = XYVisState850; +export type LensDocShape850 = LensDocShape830; From 415f8936461c6fde14a843299dab5dca299349b6 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 15:16:47 +0200 Subject: [PATCH 93/98] fix types for expression chart --- .../expression_xy/public/components/annotations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index 51e810cecfbae2..fd5cdd874bc22a 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -243,7 +243,7 @@ export const getAnnotationsGroupedByInterval = ( textField && formatter && `field:${textField}` in firstRow ? formatter.convert(firstRow[`field:${textField}`]) : firstRow.label; - const mergedAnnotation = { + const mergedAnnotation: MergedAnnotation = { ...firstRow, label, icon: firstRow.icon || 'triangle', From cc03d2e8d60f26595e00f90666ee24ff3ff0704c Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 16:14:06 +0200 Subject: [PATCH 94/98] :bug: Fix i18n id --- x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts index 29d60894de90de..026a0ae3af7f3a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts @@ -217,7 +217,7 @@ export function validateColumn( } if (missingTooltipFields.length) { invalidMessages.push( - i18n.translate('xpack.lens.xyChart.annotationError.textFieldNotFound', { + i18n.translate('xpack.lens.xyChart.annotationError.tooltipFieldNotFound', { defaultMessage: 'Tooltip {missingFields, plural, one {field} other {fields}} {missingTooltipFields} not found in data view {dataView}', values: { From f3f6bdb08c0ff3e2b0f97b8bb5bb3b91aee73ec8 Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 16:34:11 +0200 Subject: [PATCH 95/98] :label: Fix type issue --- .../expression_xy/common/__mocks__/index.ts | 1 + .../common/types/expression_functions.ts | 18 ++++++++++-------- .../public/components/xy_chart.test.tsx | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index ee25839fc7370e..8b468c7196fab1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -133,6 +133,7 @@ export const createArgsWithLayers = ( ], layers: Array.isArray(layers) ? layers : [layers], annotations: { + type: 'event_annotations_result', layers: [], datatable: { type: 'datatable', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 2a9c29c9125012..43d05eda09b237 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -232,16 +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?: { - layers: AnnotationLayerConfigResult[]; - datatable: Datatable; - }; + annotations?: ExpressionAnnotationResult; fittingFunction?: FittingFunction; fillOpacity?: number; hideEndzones?: boolean; @@ -282,10 +287,7 @@ export interface XYProps { orderBucketsBySum?: boolean; showTooltip: boolean; singleTable?: boolean; - annotations?: { - layers: AnnotationLayerConfigResult[]; - datatable: Datatable; - }; + annotations?: ExpressionAnnotationResult; } export interface AnnotationLayerArgs { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index dec3fa42f4f12d..279e1e846ef85a 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -3084,6 +3084,7 @@ describe('XYChart component', () => { ...args, layers: [dateHistogramLayer], annotations: { + type: 'event_annotations_result' as const, layers: annotationLayers, datatable: { type: 'datatable' as const, From cdf64fe13b6b653dd5ccb8a79f1525802f631f7e Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 7 Sep 2022 17:48:35 +0200 Subject: [PATCH 96/98] fix hidden all annotations --- .../fetch_event_annotations_fn.ts | 1 - .../request_event_annotations.ts | 8 ++------ .../event_annotation_service/service.tsx | 20 ++++++++----------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/fetch_event_annotations_fn.ts b/src/plugins/event_annotation/common/fetch_event_annotations/fetch_event_annotations_fn.ts index 66b470aa0afb02..d5ea46a4f33677 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/fetch_event_annotations_fn.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/fetch_event_annotations_fn.ts @@ -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', diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts index 91ffb6ff2a0c9a..745042a9f13d91 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/request_event_annotations.ts @@ -73,10 +73,10 @@ export const requestEventAnnotations = ( getStartDependencies: () => Promise ) => { return defer(async () => { - const { aggs, dataViews, searchSource, getNow, uiSettings } = await getStartDependencies(); - if (!input?.timeRange) { + 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) { @@ -289,10 +289,6 @@ function regroupForRequestOptimization( const outputGroups = groups .map((g) => { return g.annotations.reduce>((acc, current) => { - if (current.isHidden) { - return acc; - } - if (isManualAnnotation(current)) { if (!isInRange(current, input?.timeRange)) { return acc; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index ffa611843e1cea..41bf12c288cc12 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -35,7 +35,7 @@ export function getEventAnnotationService(): EventAnnotationServiceType { for (const annotation of manualBasedAnnotations) { if (isRangeAnnotationConfig(annotation)) { - const { label, isHidden, color, key, outside, id } = annotation; + const { label, color, key, outside, id } = annotation; const { timestamp: time, endTimestamp: endTime } = key; expressions.push({ type: 'expression' as const, @@ -50,14 +50,12 @@ export function getEventAnnotationService(): EventAnnotationServiceType { label: [label || defaultAnnotationLabel], color: [color || defaultAnnotationRangeColor], outside: [Boolean(outside)], - isHidden: [Boolean(isHidden)], }, }, ], }); } else { - const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility, id } = - annotation; + const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation; expressions.push({ type: 'expression' as const, chain: [ @@ -73,7 +71,6 @@ export function getEventAnnotationService(): EventAnnotationServiceType { lineStyle: [lineStyle || 'solid'], icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], - isHidden: [Boolean(isHidden)], }, }, ], @@ -85,7 +82,6 @@ export function getEventAnnotationService(): EventAnnotationServiceType { const { id, label, - isHidden, color, lineStyle, lineWidth, @@ -112,7 +108,6 @@ export function getEventAnnotationService(): EventAnnotationServiceType { icon: hasIcon(icon) ? [icon] : ['triangle'], textVisibility: [textVisibility || false], textField: textVisibility && textField ? [textField] : [], - isHidden: [Boolean(isHidden)], filter: filter ? [queryToAst(filter)] : [], extraFields: extraFields || [], }, @@ -129,8 +124,9 @@ export function getEventAnnotationService(): EventAnnotationServiceType { return []; } - const groupsExpressions = groups.map( - ({ annotations, indexPatternId }): ExpressionAstExpression => { + const groupsExpressions = groups + .filter((g) => g.annotations.some((a) => !a.isHidden)) + .map(({ annotations, indexPatternId }): ExpressionAstExpression => { const indexPatternExpression: ExpressionAstExpression = { type: 'expression', chain: [ @@ -143,6 +139,7 @@ export function getEventAnnotationService(): EventAnnotationServiceType { }, ], }; + const annotationExpressions = annotationsToExpression(annotations); return { type: 'expression', chain: [ @@ -151,13 +148,12 @@ export function getEventAnnotationService(): EventAnnotationServiceType { function: 'event_annotation_group', arguments: { dataView: [indexPatternExpression], - annotations: [...annotationsToExpression(annotations)], + annotations: [...annotationExpressions], }, }, ], }; - } - ); + }); const fetchExpression: ExpressionAstExpression = { type: 'expression', From 77b5598ab75cd29b2c8c559ac24a14a0738db86f Mon Sep 17 00:00:00 2001 From: dej611 Date: Wed, 7 Sep 2022 18:40:28 +0200 Subject: [PATCH 97/98] :white_check_mark: Fix tests after ishidden removal --- .../public/event_annotation_service/service.test.ts | 7 ------- .../__snapshots__/fetch_event_annotations.test.ts.snap | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index 64bbf050a6dd1e..0fea0e7c38ea76 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -86,7 +86,6 @@ describe('Event Annotation Service', () => { lineStyle: ['solid'], icon: ['triangle'], textVisibility: [false], - isHidden: [false], }, }, ], @@ -121,7 +120,6 @@ describe('Event Annotation Service', () => { label: ['Hello'], color: ['#F04E981A'], outside: [false], - isHidden: [false], }, }, ], @@ -159,7 +157,6 @@ describe('Event Annotation Service', () => { icon: ['triangle'], textVisibility: [false], textField: [], - isHidden: [false], filter: [ { chain: [ @@ -230,7 +227,6 @@ describe('Event Annotation Service', () => { lineStyle: ['solid'], icon: ['triangle'], textVisibility: [false], - isHidden: [false], }, }, ], @@ -248,7 +244,6 @@ describe('Event Annotation Service', () => { label: ['Hello Range'], color: ['#F04E981A'], outside: [false], - isHidden: [false], }, }, ], @@ -269,7 +264,6 @@ describe('Event Annotation Service', () => { icon: ['triangle'], textVisibility: [false], textField: [], - isHidden: [false], filter: [ { chain: [ @@ -332,7 +326,6 @@ describe('Event Annotation Service', () => { icon: ['triangle'], textVisibility: [textVisibility], textField: expected ? [expected] : [], - isHidden: [false], filter: [ { chain: [ diff --git a/src/plugins/event_annotation/public/fetch_event_annotations/__snapshots__/fetch_event_annotations.test.ts.snap b/src/plugins/event_annotation/public/fetch_event_annotations/__snapshots__/fetch_event_annotations.test.ts.snap index daefa3f4af7cea..25729bfd5b80e0 100644 --- a/src/plugins/event_annotation/public/fetch_event_annotations/__snapshots__/fetch_event_annotations.test.ts.snap +++ b/src/plugins/event_annotation/public/fetch_event_annotations/__snapshots__/fetch_event_annotations.test.ts.snap @@ -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", From 1af226a699cf1a508ec86f6b11beedbd74642096 Mon Sep 17 00:00:00 2001 From: dej611 Date: Thu, 8 Sep 2022 12:53:48 +0200 Subject: [PATCH 98/98] :bug: Revert references migration to an in app solution --- .../public/visualizations/xy/state_helpers.ts | 10 ++- .../lens/public/visualizations/xy/types.ts | 3 +- .../visualizations/xy/visualization.test.ts | 76 +++++++++++++++++++ .../make_lens_embeddable_factory.ts | 8 -- .../server/migrations/common_migrations.ts | 27 ------- .../saved_object_migrations.test.ts | 12 --- .../migrations/saved_object_migrations.ts | 16 +--- 7 files changed, 85 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts index 026a0ae3af7f3a..37c08fc7e8668f 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/state_helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DistributiveOmit } from '@elastic/eui'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { SavedObjectReference } from '@kbn/core/public'; import { isQueryAnnotationConfig } from '@kbn/event-annotation-plugin/public'; @@ -120,7 +121,7 @@ function getLayerReferenceName(layerId: string) { export function extractReferences(state: XYState) { const savedObjectReferences: SavedObjectReference[] = []; - const persistableLayers: Array> = []; + const persistableLayers: Array> = []; state.layers.forEach((layer) => { if (isAnnotationsLayer(layer)) { const { indexPatternId, ...persistableLayer } = layer; @@ -144,6 +145,7 @@ export function injectReferences( if (!references || !references.length) { return state as XYState; } + const fallbackIndexPatternId = references.find(({ type }) => type === 'index-pattern')!.id; return { ...state, layers: state.layers.map((layer) => { @@ -152,9 +154,9 @@ export function injectReferences( } return { ...layer, - indexPatternId: references.find( - ({ name }) => name === getLayerReferenceName(layer.layerId) - )!.id, + indexPatternId: + references.find(({ name }) => name === getLayerReferenceName(layer.layerId))?.id || + fallbackIndexPatternId, }; }), }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index 70ded80018efbd..203718e4b9f8cf 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -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'; @@ -163,7 +164,7 @@ export interface XYState { export type State = XYState; export type XYPersistedState = Omit & { - layers: Array>; + layers: Array>; }; export type PersistedState = XYPersistedState; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index a68eabda65d7ee..596f20d9fe2e7c 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -2683,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], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index 7d21e542c447f0..bbac9821500004 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -11,7 +11,6 @@ import { mergeMigrationFunctionMaps, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; -import { SavedObjectReference } from '@kbn/core-saved-objects-common'; import { DOC_TYPE } from '../../common'; import { commonEnhanceTableRowHeight, @@ -31,7 +30,6 @@ import { commonExplicitAnnotationType, getLensDataViewMigrations, commonMigrateMetricIds, - commonAnnotationAddDataViewIdReferences, commonMigratePartitionChartGroups, } from '../migrations/common_migrations'; import { @@ -140,17 +138,12 @@ export const makeLensEmbeddableFactory = '8.5.0': (state) => { const lensState = state as unknown as { attributes: LensDocShape850; - references: SavedObjectReference[] | undefined; }; let migratedLensState = commonMigrateMetricIds(lensState.attributes); migratedLensState = commonExplicitAnnotationType( migratedLensState as LensDocShape850 ); - const migratedReferences = commonAnnotationAddDataViewIdReferences( - migratedLensState as LensDocShape850, - lensState.references - ); migratedLensState = commonMigratePartitionChartGroups( migratedLensState as LensDocShape850<{ shape: string; @@ -160,7 +153,6 @@ export const makeLensEmbeddableFactory = return { ...lensState, attributes: migratedLensState, - references: migratedReferences, } as unknown as SerializableRecord; }, }), diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 7b2650cda088f4..89dca4829c583c 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -13,7 +13,6 @@ import { MigrateFunction, MigrateFunctionsObject, } from '@kbn/kibana-utils-plugin/common'; -import { SavedObjectReference } from '@kbn/core-saved-objects-common'; import { LensDocShapePre712, OperationTypePre712, @@ -457,32 +456,6 @@ export const commonExplicitAnnotationType = ( }; }; -export const commonAnnotationAddDataViewIdReferences = ( - attributes: LensDocShape850, - references: SavedObjectReference[] | undefined -): SavedObjectReference[] | undefined => { - if ( - !references || - references.every(({ type }) => type !== 'index-pattern') || - attributes.visualizationType !== 'lnsXY' || - attributes.state.visualization.layers.every((l) => l.layerType !== 'annotations') - ) { - return references; - } - const dataViewRef = references.find(({ type }) => type === 'index-pattern'); - if (!dataViewRef) { - return references; - } - const annotationReferences: SavedObjectReference[] = attributes.state.visualization.layers - .filter(({ layerType }) => layerType === 'annotations') - .map(({ layerId }) => ({ - type: 'index-pattern', - id: dataViewRef.id, - name: `xy-visualization-layer-${layerId}`, - })); - return [...references, ...annotationReferences]; -}; - export const commonMigrateMetricIds = ( attributes: LensDocShape850 ): LensDocShape850 => { diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index f543f33fd68fd8..b07f801e53f117 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -2330,18 +2330,6 @@ describe('Lens migrations', () => { annotations: [{ id: 'annotation-id', type: 'manual' }], }); }); - - it('adds dataView references for annotation layers', () => { - const result = migrations['8.5.0']( - { - ...example, - references: [{ id: 'dataViewId', type: 'index-pattern', name: 'datasource1' }], - }, - context - ) as ReturnType>; - // A new reference has been added for the annotation layer - expect(result.references).toHaveLength(2); - }); }); describe('8.5.0 migrates metric IDs', () => { diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index fda1ae0d27c031..e48f46ad885c50 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -58,7 +58,6 @@ import { commonExplicitAnnotationType, getLensDataViewMigrations, commonMigrateMetricIds, - commonAnnotationAddDataViewIdReferences, commonMigratePartitionChartGroups, } from './common_migrations'; @@ -529,14 +528,6 @@ const addEventAnnotationType: SavedObjectMigrationFn< return { ...newDoc, attributes: commonExplicitAnnotationType(newDoc.attributes) }; }; -const addEventAnnotationDataViewReferences: SavedObjectMigrationFn< - LensDocShape850, - LensDocShape850 -> = (doc) => ({ - ...doc, - references: commonAnnotationAddDataViewIdReferences(doc.attributes, doc.references), -}); - const migrateMetricIds: SavedObjectMigrationFn = (doc) => ({ ...doc, attributes: commonMigrateMetricIds(doc.attributes), @@ -574,12 +565,7 @@ const lensMigrations: SavedObjectMigrationMap = { enhanceTableRowHeight ), '8.3.0': flow(lockOldMetricVisSettings, preserveOldLegendSizeDefault, fixValueLabelsInXY), - '8.5.0': flow( - migrateMetricIds, - addEventAnnotationType, - addEventAnnotationDataViewReferences, - migratePartitionChartGroups - ), + '8.5.0': flow(migrateMetricIds, addEventAnnotationType, migratePartitionChartGroups), }; export const getAllMigrations = (