From cd970c6c33e16c1a5460d1fa99742ba722c0c39a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 12 Sep 2024 02:29:45 -0500 Subject: [PATCH] [Embeddables Rebuild] Move legacy viusalize embeddable to legacy/embeddable (#192002) ## Summary In visualize, renames the new `react_embeddable` folder to just `embeddable`, and moves the previous `embeddable` folder to `legacy/embeddable`. Keeps a few constants and interfaces that are reused in the new embeddable in the `embeddable` folder, and imports them into `legacy/embeddable` where needed. --------- Co-authored-by: Marco Liberati --- .../public/dynamically_add_panels_example.tsx | 2 +- .../public/actions/edit_in_lens_action.tsx | 2 +- .../create_vis_instance.ts | 0 .../get_expression_renderer_props.ts | 2 +- .../get_visualize_embeddable_factory_lazy.ts} | 5 +- .../visualizations/public/embeddable/index.ts | 8 +- .../save_to_library.ts | 0 .../state.test.ts | 0 .../{react_embeddable => embeddable}/state.ts | 0 .../{react_embeddable => embeddable}/types.ts | 2 +- .../embeddable/visualize_embeddable.tsx | 1176 +++++++---------- src/plugins/visualizations/public/index.ts | 13 +- .../{ => legacy}/embeddable/constants.ts | 2 +- .../create_vis_embeddable_from_object.ts | 6 +- .../{ => legacy}/embeddable/embeddables.scss | 0 .../public/legacy/embeddable/index.ts | 18 + .../embeddable/visualize_embeddable.tsx | 717 ++++++++++ .../embeddable/visualize_embeddable_async.ts | 0 .../visualize_embeddable_factory.test.ts | 0 .../visualize_embeddable_factory.tsx | 16 +- src/plugins/visualizations/public/plugin.ts | 11 +- .../react_embeddable/visualize_embeddable.tsx | 549 -------- .../save_with_confirmation.ts | 2 +- .../saved_visualization_references.ts | 2 +- .../public/visualize_app/types.ts | 2 +- .../utils/get_visualization_instance.ts | 2 +- 26 files changed, 1276 insertions(+), 1261 deletions(-) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/create_vis_instance.ts (100%) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/get_expression_renderer_props.ts (98%) rename src/plugins/visualizations/public/{react_embeddable/index.ts => embeddable/get_visualize_embeddable_factory_lazy.ts} (71%) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/save_to_library.ts (100%) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/state.test.ts (100%) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/state.ts (100%) rename src/plugins/visualizations/public/{react_embeddable => embeddable}/types.ts (98%) rename src/plugins/visualizations/public/{ => legacy}/embeddable/constants.ts (91%) rename src/plugins/visualizations/public/{ => legacy}/embeddable/create_vis_embeddable_from_object.ts (94%) rename src/plugins/visualizations/public/{ => legacy}/embeddable/embeddables.scss (100%) create mode 100644 src/plugins/visualizations/public/legacy/embeddable/index.ts create mode 100644 src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx rename src/plugins/visualizations/public/{ => legacy}/embeddable/visualize_embeddable_async.ts (100%) rename src/plugins/visualizations/public/{ => legacy}/embeddable/visualize_embeddable_factory.test.ts (100%) rename src/plugins/visualizations/public/{ => legacy}/embeddable/visualize_embeddable_factory.tsx (96%) delete mode 100644 src/plugins/visualizations/public/react_embeddable/visualize_embeddable.tsx diff --git a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx b/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx index fad894349491..3816beea9634 100644 --- a/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx +++ b/examples/portable_dashboards_example/public/dynamically_add_panels_example.tsx @@ -23,7 +23,7 @@ import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput, -} from '@kbn/visualizations-plugin/public/embeddable/visualize_embeddable'; +} from '@kbn/visualizations-plugin/public/legacy/embeddable/visualize_embeddable'; const INPUT_KEY = 'portableDashboard:saveExample:input'; diff --git a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx index 6d3a2ce697f7..8995b2abf738 100644 --- a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx +++ b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx @@ -26,7 +26,7 @@ import { import { Action } from '@kbn/ui-actions-plugin/public'; import React from 'react'; import { take } from 'rxjs'; -import { apiHasVisualizeConfig, HasVisualizeConfig } from '../embeddable'; +import { apiHasVisualizeConfig, HasVisualizeConfig } from '../legacy/embeddable'; import { apiHasExpressionVariables, HasExpressionVariables, diff --git a/src/plugins/visualizations/public/react_embeddable/create_vis_instance.ts b/src/plugins/visualizations/public/embeddable/create_vis_instance.ts similarity index 100% rename from src/plugins/visualizations/public/react_embeddable/create_vis_instance.ts rename to src/plugins/visualizations/public/embeddable/create_vis_instance.ts diff --git a/src/plugins/visualizations/public/react_embeddable/get_expression_renderer_props.ts b/src/plugins/visualizations/public/embeddable/get_expression_renderer_props.ts similarity index 98% rename from src/plugins/visualizations/public/react_embeddable/get_expression_renderer_props.ts rename to src/plugins/visualizations/public/embeddable/get_expression_renderer_props.ts index 67d38577b54d..69dfef84c2be 100644 --- a/src/plugins/visualizations/public/react_embeddable/get_expression_renderer_props.ts +++ b/src/plugins/visualizations/public/embeddable/get_expression_renderer_props.ts @@ -10,7 +10,7 @@ import type { KibanaExecutionContext } from '@kbn/core-execution-context-common'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { ExpressionRendererEvent, ExpressionRendererParams } from '@kbn/expressions-plugin/public'; -import { toExpressionAst } from '../embeddable/to_ast'; +import { toExpressionAst } from './to_ast'; import { getExecutionContext, getTimeFilter } from '../services'; import type { VisParams } from '../types'; import type { Vis } from '../vis'; diff --git a/src/plugins/visualizations/public/react_embeddable/index.ts b/src/plugins/visualizations/public/embeddable/get_visualize_embeddable_factory_lazy.ts similarity index 71% rename from src/plugins/visualizations/public/react_embeddable/index.ts rename to src/plugins/visualizations/public/embeddable/get_visualize_embeddable_factory_lazy.ts index 77f7ee433a99..b14d03ec5030 100644 --- a/src/plugins/visualizations/public/react_embeddable/index.ts +++ b/src/plugins/visualizations/public/embeddable/get_visualize_embeddable_factory_lazy.ts @@ -7,4 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { getVisualizeEmbeddableFactory } from './visualize_embeddable'; +export const getVisualizeEmbeddableFactoryLazy = async () => { + const { getVisualizeEmbeddableFactory } = await import('./visualize_embeddable'); + return getVisualizeEmbeddableFactory; +}; diff --git a/src/plugins/visualizations/public/embeddable/index.ts b/src/plugins/visualizations/public/embeddable/index.ts index c3855d3ab171..6d1649771c8e 100644 --- a/src/plugins/visualizations/public/embeddable/index.ts +++ b/src/plugins/visualizations/public/embeddable/index.ts @@ -7,11 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; -export { VISUALIZE_EMBEDDABLE_TYPE, COMMON_VISUALIZATION_GROUPING } from './constants'; +export { getVisualizeEmbeddableFactoryLazy } from './get_visualize_embeddable_factory_lazy'; export { VIS_EVENT_TO_TRIGGER } from './events'; -export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; - -export type { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; - -export { type HasVisualizeConfig, apiHasVisualizeConfig } from './interfaces/has_visualize_config'; diff --git a/src/plugins/visualizations/public/react_embeddable/save_to_library.ts b/src/plugins/visualizations/public/embeddable/save_to_library.ts similarity index 100% rename from src/plugins/visualizations/public/react_embeddable/save_to_library.ts rename to src/plugins/visualizations/public/embeddable/save_to_library.ts diff --git a/src/plugins/visualizations/public/react_embeddable/state.test.ts b/src/plugins/visualizations/public/embeddable/state.test.ts similarity index 100% rename from src/plugins/visualizations/public/react_embeddable/state.test.ts rename to src/plugins/visualizations/public/embeddable/state.test.ts diff --git a/src/plugins/visualizations/public/react_embeddable/state.ts b/src/plugins/visualizations/public/embeddable/state.ts similarity index 100% rename from src/plugins/visualizations/public/react_embeddable/state.ts rename to src/plugins/visualizations/public/embeddable/state.ts diff --git a/src/plugins/visualizations/public/react_embeddable/types.ts b/src/plugins/visualizations/public/embeddable/types.ts similarity index 98% rename from src/plugins/visualizations/public/react_embeddable/types.ts rename to src/plugins/visualizations/public/embeddable/types.ts index b0c6b296112b..2536b478debb 100644 --- a/src/plugins/visualizations/public/react_embeddable/types.ts +++ b/src/plugins/visualizations/public/embeddable/types.ts @@ -22,7 +22,7 @@ import { SerializedTitles, } from '@kbn/presentation-publishing'; import { DeepPartial } from '@kbn/utility-types'; -import { HasVisualizeConfig } from '../embeddable'; +import { HasVisualizeConfig } from '../legacy/embeddable'; import type { Vis, VisParams, VisSavedObject } from '../types'; import type { SerializedVis } from '../vis'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 6c684b58af88..8e1861af15a9 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -7,711 +7,543 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import _, { get } from 'lodash'; -import { Subscription, ReplaySubject, mergeMap } from 'rxjs'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { render } from 'react-dom'; -import { EuiLoadingChart } from '@elastic/eui'; -import { Filter, onlyDisabledFiltersChanged, Query, TimeRange } from '@kbn/es-query'; -import type { KibanaExecutionContext, SavedObjectAttributes } from '@kbn/core/public'; -import type { ErrorLike } from '@kbn/expressions-plugin/common'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { TimefilterContract } from '@kbn/data-plugin/public'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiLoadingChart } from '@elastic/eui'; +import { isChartSizeEvent } from '@kbn/chart-expressions-common'; +import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { Warnings } from '@kbn/charts-plugin/public'; -import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings'; +import { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; import { - Adapters, - AttributeService, - Embeddable, - EmbeddableInput, - EmbeddableOutput, - FilterableEmbeddable, - IContainer, - ReferenceOrValueEmbeddable, - SavedObjectEmbeddableInput, + EmbeddableStart, + ReactEmbeddableFactory, + SELECT_RANGE_TRIGGER, } from '@kbn/embeddable-plugin/public'; +import { ExpressionRendererParams, useExpressionRenderer } from '@kbn/expressions-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { dispatchRenderComplete } from '@kbn/kibana-utils-plugin/public'; +import { apiPublishesSettings } from '@kbn/presentation-containers'; import { - ExpressionAstExpression, - ExpressionLoader, - ExpressionRenderError, - IExpressionLoaderParams, -} from '@kbn/expressions-plugin/public'; -import type { RenderMode } from '@kbn/expressions-plugin/common'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; -import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; -import { isChartSizeEvent } from '@kbn/chart-expressions-common'; -import { isFallbackDataView } from '../visualize_app/utils'; -import { VisualizationMissedSavedObjectError } from '../components/visualization_missed_saved_object_error'; -import VisualizationError from '../components/visualization_error'; -import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import { SerializedVis, Vis } from '../vis'; -import { getApplication, getExecutionContext, getExpressions, getUiActions } from '../services'; + apiHasAppContext, + apiHasDisableTriggers, + apiHasExecutionContext, + apiIsOfType, + apiPublishesTimeRange, + apiPublishesTimeslice, + apiPublishesUnifiedSearch, + apiPublishesViewMode, + fetch$, + getUnchangingComparator, + initializeTimeRange, + initializeTitles, + useStateFromPublishingSubject, +} from '@kbn/presentation-publishing'; +import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; +import { get, isEmpty, isEqual, isNil, omitBy } from 'lodash'; +import React, { useEffect, useRef } from 'react'; +import { BehaviorSubject, switchMap } from 'rxjs'; +import { VISUALIZE_APP_NAME, VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; import { VIS_EVENT_TO_TRIGGER } from './events'; -import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; -import { getSavedVisualization } from '../utils/saved_visualize_utils'; -import { VisSavedObject } from '../types'; -import { toExpressionAst } from './to_ast'; - -export interface VisualizeEmbeddableConfiguration { - vis: Vis; - indexPatterns?: DataView[]; - editPath: string; - editUrl: string; - capabilities: { visualizeSave: boolean; dashboardSave: boolean; visualizeOpen: boolean }; - deps: VisualizeEmbeddableFactoryDeps; -} - -export interface VisualizeInput extends EmbeddableInput { - vis?: { - colors?: { [key: string]: string }; - }; - savedVis?: SerializedVis; - renderMode?: RenderMode; - table?: unknown; - query?: Query; - filters?: Filter[]; - timeRange?: TimeRange; - timeslice?: [number, number]; -} - -export interface VisualizeOutput extends EmbeddableOutput { - editPath: string; - editApp: string; - editUrl: string; - indexPatterns?: DataView[]; - visTypeName: string; -} - -export type VisualizeSavedObjectAttributes = SavedObjectAttributes & { - title: string; - vis?: Vis; - savedVis?: VisSavedObject; -}; -export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput; -export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput; - -/** @deprecated - * VisualizeEmbeddable is no longer registered with the legacy embeddable system and is only - * used within the visualize editor. - */ -export class VisualizeEmbeddable - extends Embeddable - implements - ReferenceOrValueEmbeddable, - FilterableEmbeddable -{ - private handler?: ExpressionLoader; - private timefilter: TimefilterContract; - private timeRange?: TimeRange; - private query?: Query; - private filters?: Filter[]; - private searchSessionId?: string; - private syncColors?: boolean; - private syncTooltips?: boolean; - private syncCursor?: boolean; - private embeddableTitle?: string; - private visCustomizations?: Pick; - private subscriptions: Subscription[] = []; - private expression?: ExpressionAstExpression; - private vis: Vis; - private domNode: any; - private warningDomNode: any; - public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - private abortController?: AbortController; - private readonly deps: VisualizeEmbeddableFactoryDeps; - private readonly inspectorAdapters?: Adapters; - private attributeService?: AttributeService< - VisualizeSavedObjectAttributes, - VisualizeByValueInput, - VisualizeByReferenceInput - >; - private expressionVariables: Record | undefined; - private readonly expressionVariablesSubject = new ReplaySubject< - Record | undefined - >(1); - - constructor( - timefilter: TimefilterContract, - { vis, editPath, editUrl, indexPatterns, deps, capabilities }: VisualizeEmbeddableConfiguration, - initialInput: VisualizeInput, - attributeService?: AttributeService< - VisualizeSavedObjectAttributes, - VisualizeByValueInput, - VisualizeByReferenceInput - >, - parent?: IContainer - ) { - super( - initialInput, - { - defaultTitle: vis.title, - defaultDescription: vis.description, - editPath, - editApp: 'visualize', - editUrl, - indexPatterns, - visTypeName: vis.type.name, - }, - parent - ); - this.deps = deps; - this.timefilter = timefilter; - this.syncColors = this.input.syncColors; - this.syncTooltips = this.input.syncTooltips; - this.syncCursor = this.input.syncCursor; - this.searchSessionId = this.input.searchSessionId; - this.query = this.input.query; - this.embeddableTitle = this.getTitle(); - - this.vis = vis; - this.vis.uiState.on('change', this.uiStateChangeHandler); - this.vis.uiState.on('reload', this.reload); - this.attributeService = attributeService; - - if (this.attributeService) { - const readOnly = Boolean(vis.type.disableEdit); - const isByValue = !this.inputIsRefType(initialInput); - const editable = readOnly - ? false - : capabilities.visualizeSave || - (isByValue && capabilities.dashboardSave && capabilities.visualizeOpen); - this.updateOutput({ ...this.getOutput(), editable }); - } - - this.subscriptions.push( - this.getInput$().subscribe(() => { - const isDirty = this.handleChanges(); +import { getCapabilities, getInspector, getUiActions, getUsageCollection } from '../services'; +import { ACTION_CONVERT_TO_LENS } from '../triggers'; +import { urlFor } from '../utils/saved_visualize_utils'; +import type { SerializedVis, Vis } from '../vis'; +import { createVisInstance } from './create_vis_instance'; +import { getExpressionRendererProps } from './get_expression_renderer_props'; +import { saveToLibrary } from './save_to_library'; +import { deserializeState, serializeState } from './state'; +import { + ExtraSavedObjectProperties, + VisualizeApi, + VisualizeOutputState, + VisualizeRuntimeState, + VisualizeSerializedState, + isVisualizeSavedObjectState, +} from './types'; + +export const getVisualizeEmbeddableFactory: (deps: { + embeddableStart: EmbeddableStart; + embeddableEnhancedStart?: EmbeddableEnhancedPluginStart; +}) => ReactEmbeddableFactory = ({ + embeddableStart, + embeddableEnhancedStart, +}) => ({ + type: VISUALIZE_EMBEDDABLE_TYPE, + deserializeState, + buildEmbeddable: async (initialState: unknown, buildApi, uuid, parentApi) => { + // Handle state transfer from legacy visualize editor, which uses the legacy visualize embeddable and doesn't + // produce a snapshot state. If buildEmbeddable is passed only a savedObjectId in the state, this means deserializeState + // was never run, and it needs to be invoked manually + const state = isVisualizeSavedObjectState(initialState) + ? await deserializeState({ + rawState: initialState, + }) + : (initialState as VisualizeRuntimeState); - if (isDirty && this.handler) { - this.updateHandler(); - } - }) + // Initialize dynamic actions + const dynamicActionsApi = embeddableEnhancedStart?.initializeReactEmbeddableDynamicActions( + uuid, + () => titlesApi.panelTitle.getValue(), + state ); + // if it is provided, start the dynamic actions manager + const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions(); + + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); + + // Count renders; mostly used for testing. + const renderCount$ = new BehaviorSubject(0); + const hasRendered$ = new BehaviorSubject(false); + + // Track vis data and initialize it into a vis instance + const serializedVis$ = new BehaviorSubject(state.serializedVis); + const initialVisInstance = await createVisInstance(state.serializedVis); + const vis$ = new BehaviorSubject(initialVisInstance); + + // Track UI state + const onUiStateChange = () => serializedVis$.next(vis$.getValue().serialize()); + initialVisInstance.uiState.on('change', onUiStateChange); + vis$.subscribe((vis) => vis.uiState.on('change', onUiStateChange)); + + // When the serialized vis changes, update the vis instance + serializedVis$ + .pipe( + switchMap(async (serializedVis) => { + const currentVis = vis$.getValue(); + if (currentVis) currentVis.uiState.off('change', onUiStateChange); + const vis = await createVisInstance(serializedVis); + const { params, abortController } = await getExpressionParams(); + return { vis, params, abortController }; + }) + ) + .subscribe(({ vis, params, abortController }) => { + vis$.next(vis); + if (params) expressionParams$.next(params); + expressionAbortController$.next(abortController); + }); - const inspectorAdapters = this.vis.type.inspectorAdapters; - - if (inspectorAdapters) { - this.inspectorAdapters = - typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters; - } - } - - public reportsEmbeddableLoad() { - return true; - } - - public getVis() { - return this.vis; - } - - /** - * Gets the Visualize embeddable's local filters - * @returns Local/panel-level array of filters for Visualize embeddable - */ - public getFilters() { - const filters = this.vis.serialize().data.searchSource?.filter ?? []; - // must clone the filters so that it's not read only, because mapAndFlattenFilters modifies the array - return mapAndFlattenFilters(_.cloneDeep(filters)); - } - - /** - * Gets the Visualize embeddable's local query - * @returns Local/panel-level query for Visualize embeddable - */ - public getQuery() { - return this.vis.serialize().data.searchSource.query; - } - - public getInspectorAdapters = () => { - if (!this.handler || (this.inspectorAdapters && !Object.keys(this.inspectorAdapters).length)) { - return undefined; - } - return this.handler.inspect(); - }; - - public openInspector = () => { - if (!this.handler) return; - - const adapters = this.handler.inspect(); - if (!adapters) return; + // Track visualizations linked to a saved object in the library + const savedObjectId$ = new BehaviorSubject( + state.savedObjectId ?? state.serializedVis.id + ); + const savedObjectProperties$ = new BehaviorSubject( + undefined + ); + const linkedToLibrary$ = new BehaviorSubject(state.linkedToLibrary); - return this.deps.start().plugins.inspector.open(adapters, { - title: - this.getTitle() || - i18n.translate('visualizations.embeddable.inspectorTitle', { - defaultMessage: 'Inspector', - }), + // Track the vis expression + const expressionParams$ = new BehaviorSubject({ + expression: '', }); - }; - - /** - * Transfers all changes in the containerState.customization into - * the uiState of this visualization. - */ - public transferCustomizationsToUiState() { - // Check for changes that need to be forwarded to the uiState - // Since the vis has an own listener on the uiState we don't need to - // pass anything from here to the handler.update method - const visCustomizations = { vis: this.input.vis, table: this.input.table }; - if (visCustomizations.vis || visCustomizations.table) { - if (!_.isEqual(visCustomizations, this.visCustomizations)) { - this.visCustomizations = visCustomizations; - // Turn this off or the uiStateChangeHandler will fire for every modification. - this.vis.uiState.off('change', this.uiStateChangeHandler); - this.vis.uiState.clearAllKeys(); - - Object.entries(visCustomizations).forEach(([key, value]) => { - if (value) { - this.vis.uiState.set(key, value); - } - }); - - this.vis.uiState.on('change', this.uiStateChangeHandler); - } - } else if (this.parent) { - this.vis.uiState.clearAllKeys(); - } - } - private handleChanges(): boolean { - this.transferCustomizationsToUiState(); + const expressionAbortController$ = new BehaviorSubject(new AbortController()); + let getExpressionParams: () => ReturnType = async () => ({ + params: expressionParams$.getValue(), + abortController: expressionAbortController$.getValue(), + }); - let dirty = false; + const { + api: customTimeRangeApi, + serialize: serializeCustomTimeRange, + comparators: customTimeRangeComparators, + } = initializeTimeRange(state); - // Check if timerange has changed - const nextTimeRange = - this.input.timeslice !== undefined - ? { - from: new Date(this.input.timeslice[0]).toISOString(), - to: new Date(this.input.timeslice[1]).toISOString(), - mode: 'absolute' as 'absolute', - } - : this.input.timeRange; - if (!_.isEqual(nextTimeRange, this.timeRange)) { - this.timeRange = _.cloneDeep(nextTimeRange); - dirty = true; - } + const searchSessionId$ = new BehaviorSubject(''); - // Check if filters has changed - if (!onlyDisabledFiltersChanged(this.input.filters, this.filters)) { - this.filters = this.input.filters; - dirty = true; - } + const viewMode$ = apiPublishesViewMode(parentApi) + ? parentApi.viewMode + : new BehaviorSubject('view'); - // Check if query has changed - if (!_.isEqual(this.input.query, this.query)) { - this.query = this.input.query; - dirty = true; - } + const executionContext = apiHasExecutionContext(parentApi) + ? parentApi.executionContext + : undefined; - if (this.searchSessionId !== this.input.searchSessionId) { - this.searchSessionId = this.input.searchSessionId; - dirty = true; - } + const disableTriggers = apiHasDisableTriggers(parentApi) + ? parentApi.disableTriggers + : undefined; - if (this.syncColors !== this.input.syncColors) { - this.syncColors = this.input.syncColors; - dirty = true; - } + const parentApiContext = apiHasAppContext(parentApi) ? parentApi.getAppContext() : undefined; - if (this.syncTooltips !== this.input.syncTooltips) { - this.syncTooltips = this.input.syncTooltips; - dirty = true; - } + const inspectorAdapters$ = new BehaviorSubject>({}); - if (this.syncCursor !== this.input.syncCursor) { - this.syncCursor = this.input.syncCursor; - dirty = true; + // Track data views + let initialDataViews: DataView[] | undefined = []; + if (initialVisInstance.data.indexPattern) + initialDataViews = [initialVisInstance.data.indexPattern]; + if (initialVisInstance.type.getUsedIndexPattern) { + initialDataViews = await initialVisInstance.type.getUsedIndexPattern( + initialVisInstance.params + ); } - if (this.embeddableTitle !== this.getTitle()) { - this.embeddableTitle = this.getTitle(); - dirty = true; - } + const dataLoading$ = new BehaviorSubject(true); - if (this.vis.description && this.domNode) { - this.domNode.setAttribute('data-description', this.vis.description); - } + const defaultPanelTitle = new BehaviorSubject(initialVisInstance.title); - return dirty; - } - - private handleWarnings() { - const warnings: React.ReactNode[] = []; - if (this.getInspectorAdapters()?.requests) { - this.deps - .start() - .plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => { - if (hasUnsupportedDownsampledAggregationFailure(warning)) { - warnings.push( - i18n.translate('visualizations.embeddable.tsdbRollupWarning', { - defaultMessage: - 'Visualization uses a function that is unsupported by rolled up data. Select a different function or change the time range.', - }) + const api = buildApi( + { + ...customTimeRangeApi, + ...titlesApi, + ...(dynamicActionsApi?.dynamicActionsApi ?? {}), + defaultPanelTitle, + dataLoading: dataLoading$, + dataViews: new BehaviorSubject(initialDataViews), + supportedTriggers: () => [ + ACTION_CONVERT_TO_LENS, + APPLY_FILTER_TRIGGER, + SELECT_RANGE_TRIGGER, + ], + serializeState: () => { + const savedObjectProperties = savedObjectProperties$.getValue(); + return serializeState({ + serializedVis: vis$.getValue().serialize(), + titles: serializeTitles(), + id: savedObjectId$.getValue(), + linkedToLibrary: + // In the visualize editor, linkedToLibrary should always be false to force the full state to be serialized, + // instead of just passing a reference to the linked saved object. Other contexts like dashboards should + // serialize the state with just the savedObjectId so that the current revision of the vis is always used + apiIsOfType(parentApi, VISUALIZE_APP_NAME) ? false : linkedToLibrary$.getValue(), + ...(savedObjectProperties ? { savedObjectProperties } : {}), + ...(dynamicActionsApi?.serializeDynamicActions?.() ?? {}), + ...serializeCustomTimeRange(), + }); + }, + getVis: () => vis$.getValue(), + getInspectorAdapters: () => inspectorAdapters$.getValue(), + getTypeDisplayName: () => + i18n.translate('visualizations.displayName', { + defaultMessage: 'visualization', + }), + onEdit: async () => { + const stateTransferService = embeddableStart.getStateTransfer(); + const visId = savedObjectId$.getValue(); + const editPath = visId ? urlFor(visId) : '#/edit_by_value'; + const parentTimeRange = apiPublishesTimeRange(parentApi) + ? parentApi.timeRange$.getValue() + : {}; + const customTimeRange = customTimeRangeApi.timeRange$.getValue(); + + await stateTransferService.navigateToEditor('visualize', { + path: editPath, + state: { + embeddableId: uuid, + valueInput: { + savedVis: vis$.getValue().serialize(), + title: api.panelTitle?.getValue(), + description: api.panelDescription?.getValue(), + timeRange: customTimeRange ?? parentTimeRange, + }, + originatingApp: parentApiContext?.currentAppId ?? '', + searchSessionId: searchSessionId$.getValue() || undefined, + originatingPath: parentApiContext?.getCurrentPath?.(), + }, + }); + }, + isEditingEnabled: () => { + if (viewMode$.getValue() !== 'edit') return false; + const readOnly = Boolean(vis$.getValue().type.disableEdit); + if (readOnly) return false; + const capabilities = getCapabilities(); + const isByValue = !savedObjectId$.getValue(); + if (isByValue) + return Boolean( + capabilities.dashboard?.showWriteControls && capabilities.visualize?.show ); - return true; - } - if (this.vis.type.suppressWarnings?.()) { - // if the vis type wishes to supress all warnings, return true so the default logic won't pick it up - return true; + else return Boolean(capabilities.visualize?.save); + }, + updateVis: async (visUpdates) => { + const currentSerializedVis = vis$.getValue().serialize(); + serializedVis$.next({ + ...currentSerializedVis, + ...visUpdates, + params: { + ...currentSerializedVis.params, + ...visUpdates.params, + }, + data: { + ...currentSerializedVis.data, + ...visUpdates.data, + }, + } as SerializedVis); + if (visUpdates.title) { + titlesApi.setPanelTitle(visUpdates.title); } - }); - } - - if (this.warningDomNode) { - const { core } = this.deps.start(); - render( - - - , - this.warningDomNode - ); - } - } - - // this is a hack to make editor still work, will be removed once we clean up editor - // @ts-ignore - hasInspector = () => Boolean(this.getInspectorAdapters()); - - onContainerLoading = () => { - this.renderComplete.dispatchInProgress(); - this.updateOutput({ - ...this.getOutput(), - loading: true, - rendered: false, - error: undefined, - }); - }; - - onContainerData = () => { - this.handleWarnings(); - this.updateOutput({ - ...this.getOutput(), - loading: false, - }); - }; - - onContainerRender = () => { - this.renderComplete.dispatchComplete(); - this.updateOutput({ - ...this.getOutput(), - rendered: true, - }); - }; - - onContainerError = (error: ExpressionRenderError) => { - if (this.abortController) { - this.abortController.abort(); - } - this.renderComplete.dispatchError(); - - if (isFallbackDataView(this.vis.data.indexPattern)) { - error = new Error( - i18n.translate('visualizations.missedDataView.errorMessage', { - defaultMessage: `Could not find the {type}: {id}`, - values: { - id: this.vis.data.indexPattern.id ?? '-', - type: this.vis.data.savedSearchId - ? i18n.translate('visualizations.noSearch.label', { - defaultMessage: 'search', - }) - : i18n.translate('visualizations.noDataView.label', { - defaultMessage: 'data view', - }), + }, + openInspector: () => { + const adapters = inspectorAdapters$.getValue(); + if (!adapters) return; + const inspector = getInspector(); + if (!inspector.isAvailable(adapters)) return; + return getInspector().open(adapters, { + title: + titlesApi.panelTitle?.getValue() || + i18n.translate('visualizations.embeddable.inspectorTitle', { + defaultMessage: 'Inspector', + }), + }); + }, + // Library transforms + saveToLibrary: (newTitle: string) => { + titlesApi.setPanelTitle(newTitle); + const { rawState, references } = serializeState({ + serializedVis: vis$.getValue().serialize(), + titles: { + ...serializeTitles(), + title: newTitle, + }, + }); + return saveToLibrary({ + uiState: vis$.getValue().uiState, + rawState: rawState as VisualizeOutputState, + references, + }); + }, + canLinkToLibrary: () => !state.linkedToLibrary, + canUnlinkFromLibrary: () => !!state.linkedToLibrary, + checkForDuplicateTitle: () => false, // Handled by saveToLibrary action + getByValueState: () => ({ + savedVis: vis$.getValue().serialize(), + ...serializeTitles(), + }), + getByReferenceState: (libraryId) => + serializeState({ + serializedVis: vis$.getValue().serialize(), + titles: serializeTitles(), + id: libraryId, + linkedToLibrary: true, + }).rawState, + }, + { + ...titleComparators, + ...customTimeRangeComparators, + ...(dynamicActionsApi?.dynamicActionsComparator ?? { + enhancements: getUnchangingComparator(), + }), + serializedVis: [ + serializedVis$, + (value) => { + serializedVis$.next(value); }, - }) - ); - } - - this.updateOutput({ - ...this.getOutput(), - rendered: true, - error, - }); - }; - - /** - * - * @param {Element} domNode - */ - public async render(domNode: HTMLElement) { - this.timeRange = _.cloneDeep(this.input.timeRange); - - this.transferCustomizationsToUiState(); - - const div = document.createElement('div'); - div.className = `visualize panel-content panel-content--fullWidth`; - domNode.appendChild(div); - - const warningDiv = document.createElement('div'); - warningDiv.className = 'visPanel__warnings'; - domNode.appendChild(warningDiv); - this.warningDomNode = warningDiv; - - this.domNode = div; - super.render(this.domNode); - const { core } = this.deps.start(); - - render( - -
- -
-
, - this.domNode + (a, b) => { + const visA = a + ? { + ...omitBy(a, isEmpty), + data: omitBy(a.data, isNil), + params: omitBy(a.params, isNil), + } + : {}; + const visB = b + ? { + ...omitBy(b, isEmpty), + data: omitBy(b.data, isNil), + params: omitBy(b.params, isNil), + } + : {}; + return isEqual(visA, visB); + }, + ], + savedObjectId: [ + savedObjectId$, + (value) => savedObjectId$.next(value), + (a, b) => { + if (!a && !b) return true; + return a === b; + }, + ], + savedObjectProperties: getUnchangingComparator(), + linkedToLibrary: [linkedToLibrary$, (value) => linkedToLibrary$.next(value)], + } ); - const expressions = getExpressions(); - this.handler = await expressions.loader(this.domNode, undefined, { - renderMode: this.input.renderMode || 'view', - onRenderError: (element: HTMLElement, error: ExpressionRenderError) => { - this.onContainerError(error); - }, - executionContext: this.getExecutionContext(), - }); - - this.subscriptions.push( - this.handler.events$ - .pipe( - mergeMap(async (event) => { - // Visualize doesn't respond to sizing events, so ignore. - if (isChartSizeEvent(event)) { - return; - } - if (!this.input.disableTriggers) { - const triggerId = get(VIS_EVENT_TO_TRIGGER, event.name, VIS_EVENT_TO_TRIGGER.filter); - let context; - - if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) { - context = { - embeddable: this, - timeFieldName: this.vis.data.indexPattern?.timeFieldName!, - ...event.data, - }; - } else { - context = { - embeddable: this, - data: { - timeFieldName: this.vis.data.indexPattern?.timeFieldName!, - ...event.data, - }, - }; + const fetchSubscription = fetch$(api) + .pipe( + switchMap(async (data) => { + const unifiedSearch = apiPublishesUnifiedSearch(parentApi) + ? { + query: data.query, + filters: data.filters, } + : {}; + const searchSessionId = apiPublishesSearchSession(parentApi) ? data.searchSessionId : ''; + searchSessionId$.next(searchSessionId); + const settings = apiPublishesSettings(parentApi) + ? { + syncColors: parentApi.settings.syncColors$.getValue(), + syncCursor: parentApi.settings.syncCursor$.getValue(), + syncTooltips: parentApi.settings.syncTooltips$.getValue(), + } + : {}; - await getUiActions().getTrigger(triggerId).exec(context); - } - }) - ) - .subscribe() - ); - - if (this.vis.description) { - div.setAttribute('data-description', this.vis.description); - } - - div.setAttribute('data-test-subj', 'visualizationLoader'); - div.setAttribute('data-shared-item', ''); - - this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading)); - this.subscriptions.push(this.handler.data$.subscribe(this.onContainerData)); - this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender)); - - this.subscriptions.push( - this.getUpdated$().subscribe(() => { - const { error } = this.getOutput(); - - if (error) { - render(this.renderError(error), this.domNode); - } - }) - ); - - await this.updateHandler(); - } - - private renderError(error: ErrorLike | string) { - const { core } = this.deps.start(); - if (isFallbackDataView(this.vis.data.indexPattern)) { - return ( - - - - ); - } - - return ( - - - - ); - } + dataLoading$.next(true); - public destroy() { - super.destroy(); - this.subscriptions.forEach((s) => s.unsubscribe()); - this.vis.uiState.off('change', this.uiStateChangeHandler); - this.vis.uiState.off('reload', this.reload); + const timeslice = apiPublishesTimeslice(parentApi) + ? parentApi.timeslice$.getValue() + : undefined; - if (this.handler) { - this.handler.destroy(); - this.handler.getElement().remove(); - } - } - - public reload = async () => { - await this.handleVisUpdate(); - }; - - private getExecutionContext() { - const parentContext = this.parent?.getInput().executionContext || getExecutionContext().get(); - const child: KibanaExecutionContext = { - type: 'agg_based', - name: this.vis.type.name, - id: this.vis.id ?? 'new', - description: this.vis.title || this.input.title || this.vis.type.name, - url: this.output.editUrl, - }; + const customTimeRange = customTimeRangeApi.timeRange$.getValue(); + const parentTimeRange = apiPublishesTimeRange(parentApi) ? data.timeRange : undefined; + const timesliceTimeRange = timeslice + ? { + from: new Date(timeslice[0]).toISOString(), + to: new Date(timeslice[1]).toISOString(), + mode: 'absolute' as 'absolute', + } + : undefined; + + // Precedence should be: + // custom time range from state > + // timeslice time range > + // parent API time range from e.g. unified search + const timeRangeToRender = customTimeRange ?? timesliceTimeRange ?? parentTimeRange; + + getExpressionParams = async () => { + return await getExpressionRendererProps({ + unifiedSearch, + vis: vis$.getValue(), + settings, + disableTriggers, + searchSessionId, + parentExecutionContext: executionContext, + abortController: expressionAbortController$.getValue(), + timeRange: timeRangeToRender, + onRender: async (renderCount) => { + if (renderCount === renderCount$.getValue()) return; + renderCount$.next(renderCount); + const visInstance = vis$.getValue(); + const visTypeName = visInstance.type.name; + + let telemetryVisTypeName = visTypeName; + if (visTypeName === 'metrics') { + telemetryVisTypeName = 'legacy_metric'; + } + if (visTypeName === 'pie' && visInstance.params.isDonut) { + telemetryVisTypeName = 'donut'; + } + if ( + visTypeName === 'area' && + visInstance.params.seriesParams.some( + (seriesParams: { mode: string }) => seriesParams.mode === 'stacked' + ) + ) { + telemetryVisTypeName = 'area_stacked'; + } + + getUsageCollection().reportUiCounter( + executionContext?.type ?? '', + 'count', + `render_agg_based_${telemetryVisTypeName}` + ); + + if (hasRendered$.getValue() === true) return; + hasRendered$.next(true); + hasRendered$.complete(); + }, + onEvent: async (event) => { + // Visualize doesn't respond to sizing events, so ignore. + if (isChartSizeEvent(event)) { + return; + } + const currentVis = vis$.getValue(); + if (!disableTriggers) { + const triggerId = get( + VIS_EVENT_TO_TRIGGER, + event.name, + VIS_EVENT_TO_TRIGGER.filter + ); + let context; + + if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) { + context = { + embeddable: api, + timeFieldName: currentVis.data.indexPattern?.timeFieldName!, + ...event.data, + }; + } else { + context = { + embeddable: api, + data: { + timeFieldName: currentVis.data.indexPattern?.timeFieldName!, + ...event.data, + }, + }; + } + await getUiActions().getTrigger(triggerId).exec(context); + } + }, + onData: (_, inspectorAdapters) => { + inspectorAdapters$.next( + typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters + ); + dataLoading$.next(false); + }, + }); + }; + return await getExpressionParams(); + }) + ) + .subscribe(({ params, abortController }) => { + if (params) expressionParams$.next(params); + expressionAbortController$.next(abortController); + }); return { - ...parentContext, - child, - }; - } - - private async updateHandler() { - const context = this.getExecutionContext(); - - this.expressionVariables = await this.vis.type.getExpressionVariables?.( - this.vis, - this.timefilter - ); - - this.expressionVariablesSubject.next(this.expressionVariables); - - const expressionParams: IExpressionLoaderParams = { - searchContext: { - timeRange: this.timeRange, - query: this.input.query, - filters: this.input.filters, - disableWarningToasts: true, - }, - variables: { - embeddableTitle: this.getTitle(), - ...this.expressionVariables, + api, + Component: () => { + const expressionParams = useStateFromPublishingSubject(expressionParams$); + const renderCount = useStateFromPublishingSubject(renderCount$); + const hasRendered = useStateFromPublishingSubject(hasRendered$); + const domNode = useRef(null); + const { error, isLoading } = useExpressionRenderer(domNode, expressionParams); + + useEffect(() => { + return () => { + fetchSubscription.unsubscribe(); + maybeStopDynamicActions?.stopDynamicActions(); + }; + }, []); + + useEffect(() => { + if (hasRendered && domNode.current) { + dispatchRenderComplete(domNode.current); + } + }, [hasRendered]); + + return ( +
+ {/* Replicate the loading state for the expression renderer to avoid FOUC */} + + {isLoading && } + {!isLoading && error && ( + + {i18n.translate('visualizations.embeddable.errorTitle', { + defaultMessage: 'Unable to load visualization ', + })} + + } + body={ +

+ {error.name}: {error.message} +

+ } + /> + )} +
+
+ ); }, - searchSessionId: this.input.searchSessionId, - syncColors: this.input.syncColors, - syncTooltips: this.input.syncTooltips, - syncCursor: this.input.syncCursor, - uiState: this.vis.uiState, - interactive: !this.input.disableTriggers, - inspectorAdapters: this.inspectorAdapters, - executionContext: context, - }; - if (this.abortController) { - this.abortController.abort(); - } - this.abortController = new AbortController(); - const abortController = this.abortController; - - try { - this.expression = await toExpressionAst(this.vis, { - timefilter: this.timefilter, - timeRange: this.timeRange, - abortSignal: this.abortController!.signal, - }); - } catch (e) { - this.onContainerError(e); - } - - if (this.handler && !abortController.signal.aborted) { - this.handler.update(this.expression, expressionParams); - } - } - - private handleVisUpdate = async () => { - this.handleChanges(); - await this.updateHandler(); - }; - - private uiStateChangeHandler = () => { - this.updateInput({ - ...this.vis.uiState.toJSON(), - }); - }; - - public supportedTriggers(): string[] { - return this.vis.type.getSupportedTriggers?.(this.vis.params) ?? []; - } - - public getExpressionVariables$() { - return this.expressionVariablesSubject.asObservable(); - } - - public getExpressionVariables() { - return this.expressionVariables; - } - - inputIsRefType = (input: VisualizeInput): input is VisualizeByReferenceInput => { - if (!this.attributeService) { - throw new Error('AttributeService must be defined for getInputAsRefType'); - } - return this.attributeService.inputIsRefType(input as VisualizeByReferenceInput); - }; - - getInputAsValueType = async (): Promise => { - const input = { - savedVis: this.vis.serialize(), - }; - delete input.savedVis.id; - _.unset(input, 'savedVis.title'); - return new Promise((resolve) => { - resolve({ ...(input as VisualizeByValueInput) }); - }); - }; - - getInputAsRefType = async (): Promise => { - const { plugins, core } = this.deps.start(); - const { data, spaces, savedObjectsTaggingOss } = plugins; - const savedVis = await getSavedVisualization({ - search: data.search, - dataViews: data.dataViews, - spaces, - savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), - ...core, - }); - if (!savedVis) { - throw new Error('Error creating a saved vis object'); - } - if (!this.attributeService) { - throw new Error('AttributeService must be defined for getInputAsRefType'); - } - const saveModalTitle = this.getTitle() - ? this.getTitle() - : i18n.translate('visualizations.embeddable.placeholderTitle', { - defaultMessage: 'Placeholder Title', - }); - // @ts-ignore - const attributes: VisualizeSavedObjectAttributes = { - savedVis, - vis: this.vis, - title: this.vis.title, }; - return this.attributeService.getInputAsRefType( - { - id: this.id, - attributes, - }, - { showSaveModal: true, saveModalTitle } - ); - }; -} + }, +}); diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 09048ba87d83..3de1bfc01f2e 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -10,7 +10,7 @@ import { PublicContract } from '@kbn/utility-types'; import { PluginInitializerContext } from '@kbn/core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; -import type { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; +import type { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './legacy/embeddable'; export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); @@ -18,11 +18,8 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { TypesService } from './vis_types/types_service'; -export { - apiHasVisualizeConfig, - VIS_EVENT_TO_TRIGGER, - COMMON_VISUALIZATION_GROUPING, -} from './embeddable'; +export { VIS_EVENT_TO_TRIGGER } from './embeddable'; +export { apiHasVisualizeConfig, COMMON_VISUALIZATION_GROUPING } from './legacy/embeddable'; export { VisualizationContainer } from './components'; export { getVisSchemas } from './vis_schemas'; @@ -38,13 +35,13 @@ export type { VisualizationClient, SerializableAttributes, } from './vis_types'; -export type { VisualizeEditorInput } from './react_embeddable/types'; +export type { VisualizeEditorInput } from './embeddable/types'; export type { Vis, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export type { SchemaConfig } from '../common/types'; export { updateOldState } from './legacy/vis_update_state'; -export type { VisualizeInput, VisualizeEmbeddable, HasVisualizeConfig } from './embeddable'; +export type { VisualizeInput, VisualizeEmbeddable, HasVisualizeConfig } from './legacy/embeddable'; export type { PersistedState } from './persisted_state'; export type { ISavedVis, diff --git a/src/plugins/visualizations/public/embeddable/constants.ts b/src/plugins/visualizations/public/legacy/embeddable/constants.ts similarity index 91% rename from src/plugins/visualizations/public/embeddable/constants.ts rename to src/plugins/visualizations/public/legacy/embeddable/constants.ts index 85c855426554..79d87ec59b1e 100644 --- a/src/plugins/visualizations/public/embeddable/constants.ts +++ b/src/plugins/visualizations/public/legacy/embeddable/constants.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; -export { VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; +export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../common/constants'; export const COMMON_VISUALIZATION_GROUPING = [ { diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts similarity index 94% rename from src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts rename to src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts index b496be0a9e81..69ed12302f4e 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts @@ -9,7 +9,7 @@ import { IContainer, ErrorEmbeddable, AttributeService } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { Vis } from '../types'; +import { Vis } from '../../types'; import type { VisualizeInput, VisualizeEmbeddable, @@ -17,8 +17,8 @@ import type { VisualizeByReferenceInput, VisualizeSavedObjectAttributes, } from './visualize_embeddable'; -import { getHttp, getTimeFilter, getCapabilities } from '../services'; -import { urlFor } from '../utils/saved_visualize_utils'; +import { getHttp, getTimeFilter, getCapabilities } from '../../services'; +import { urlFor } from '../../utils/saved_visualize_utils'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { createVisualizeEmbeddableAsync } from './visualize_embeddable_async'; diff --git a/src/plugins/visualizations/public/embeddable/embeddables.scss b/src/plugins/visualizations/public/legacy/embeddable/embeddables.scss similarity index 100% rename from src/plugins/visualizations/public/embeddable/embeddables.scss rename to src/plugins/visualizations/public/legacy/embeddable/embeddables.scss diff --git a/src/plugins/visualizations/public/legacy/embeddable/index.ts b/src/plugins/visualizations/public/legacy/embeddable/index.ts new file mode 100644 index 000000000000..6afee494e6f4 --- /dev/null +++ b/src/plugins/visualizations/public/legacy/embeddable/index.ts @@ -0,0 +1,18 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; +export { VISUALIZE_EMBEDDABLE_TYPE, COMMON_VISUALIZATION_GROUPING } from './constants'; +export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; + +export type { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; +export { + type HasVisualizeConfig, + apiHasVisualizeConfig, +} from '../../embeddable/interfaces/has_visualize_config'; diff --git a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx new file mode 100644 index 000000000000..85166441a163 --- /dev/null +++ b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx @@ -0,0 +1,717 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import _, { get } from 'lodash'; +import { Subscription, ReplaySubject, mergeMap } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { render } from 'react-dom'; +import { EuiLoadingChart } from '@elastic/eui'; +import { Filter, onlyDisabledFiltersChanged, Query, TimeRange } from '@kbn/es-query'; +import type { KibanaExecutionContext, SavedObjectAttributes } from '@kbn/core/public'; +import type { ErrorLike } from '@kbn/expressions-plugin/common'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { TimefilterContract } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { Warnings } from '@kbn/charts-plugin/public'; +import { hasUnsupportedDownsampledAggregationFailure } from '@kbn/search-response-warnings'; +import { + Adapters, + AttributeService, + Embeddable, + EmbeddableInput, + EmbeddableOutput, + FilterableEmbeddable, + IContainer, + ReferenceOrValueEmbeddable, + SavedObjectEmbeddableInput, +} from '@kbn/embeddable-plugin/public'; +import { + ExpressionAstExpression, + ExpressionLoader, + ExpressionRenderError, + IExpressionLoaderParams, +} from '@kbn/expressions-plugin/public'; +import type { RenderMode } from '@kbn/expressions-plugin/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; +import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; +import { isChartSizeEvent } from '@kbn/chart-expressions-common'; +import { isFallbackDataView } from '../../visualize_app/utils'; +import { VisualizationMissedSavedObjectError } from '../../components/visualization_missed_saved_object_error'; +import VisualizationError from '../../components/visualization_error'; +import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; +import { SerializedVis, Vis } from '../../vis'; +import { getApplication, getExecutionContext, getExpressions, getUiActions } from '../../services'; +import { VIS_EVENT_TO_TRIGGER } from '../../embeddable/events'; +import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; +import { getSavedVisualization } from '../../utils/saved_visualize_utils'; +import { VisSavedObject } from '../../types'; +import { toExpressionAst } from '../../embeddable/to_ast'; + +export interface VisualizeEmbeddableConfiguration { + vis: Vis; + indexPatterns?: DataView[]; + editPath: string; + editUrl: string; + capabilities: { visualizeSave: boolean; dashboardSave: boolean; visualizeOpen: boolean }; + deps: VisualizeEmbeddableFactoryDeps; +} + +export interface VisualizeInput extends EmbeddableInput { + vis?: { + colors?: { [key: string]: string }; + }; + savedVis?: SerializedVis; + renderMode?: RenderMode; + table?: unknown; + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + timeslice?: [number, number]; +} + +export interface VisualizeOutput extends EmbeddableOutput { + editPath: string; + editApp: string; + editUrl: string; + indexPatterns?: DataView[]; + visTypeName: string; +} + +export type VisualizeSavedObjectAttributes = SavedObjectAttributes & { + title: string; + vis?: Vis; + savedVis?: VisSavedObject; +}; +export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput; +export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput; + +/** @deprecated + * VisualizeEmbeddable is no longer registered with the legacy embeddable system and is only + * used within the visualize editor. + */ +export class VisualizeEmbeddable + extends Embeddable + implements + ReferenceOrValueEmbeddable, + FilterableEmbeddable +{ + private handler?: ExpressionLoader; + private timefilter: TimefilterContract; + private timeRange?: TimeRange; + private query?: Query; + private filters?: Filter[]; + private searchSessionId?: string; + private syncColors?: boolean; + private syncTooltips?: boolean; + private syncCursor?: boolean; + private embeddableTitle?: string; + private visCustomizations?: Pick; + private subscriptions: Subscription[] = []; + private expression?: ExpressionAstExpression; + private vis: Vis; + private domNode: any; + private warningDomNode: any; + public readonly type = VISUALIZE_EMBEDDABLE_TYPE; + private abortController?: AbortController; + private readonly deps: VisualizeEmbeddableFactoryDeps; + private readonly inspectorAdapters?: Adapters; + private attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >; + private expressionVariables: Record | undefined; + private readonly expressionVariablesSubject = new ReplaySubject< + Record | undefined + >(1); + + constructor( + timefilter: TimefilterContract, + { vis, editPath, editUrl, indexPatterns, deps, capabilities }: VisualizeEmbeddableConfiguration, + initialInput: VisualizeInput, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, + parent?: IContainer + ) { + super( + initialInput, + { + defaultTitle: vis.title, + defaultDescription: vis.description, + editPath, + editApp: 'visualize', + editUrl, + indexPatterns, + visTypeName: vis.type.name, + }, + parent + ); + this.deps = deps; + this.timefilter = timefilter; + this.syncColors = this.input.syncColors; + this.syncTooltips = this.input.syncTooltips; + this.syncCursor = this.input.syncCursor; + this.searchSessionId = this.input.searchSessionId; + this.query = this.input.query; + this.embeddableTitle = this.getTitle(); + + this.vis = vis; + this.vis.uiState.on('change', this.uiStateChangeHandler); + this.vis.uiState.on('reload', this.reload); + this.attributeService = attributeService; + + if (this.attributeService) { + const readOnly = Boolean(vis.type.disableEdit); + const isByValue = !this.inputIsRefType(initialInput); + const editable = readOnly + ? false + : capabilities.visualizeSave || + (isByValue && capabilities.dashboardSave && capabilities.visualizeOpen); + this.updateOutput({ ...this.getOutput(), editable }); + } + + this.subscriptions.push( + this.getInput$().subscribe(() => { + const isDirty = this.handleChanges(); + + if (isDirty && this.handler) { + this.updateHandler(); + } + }) + ); + + const inspectorAdapters = this.vis.type.inspectorAdapters; + + if (inspectorAdapters) { + this.inspectorAdapters = + typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters; + } + } + + public reportsEmbeddableLoad() { + return true; + } + + public getVis() { + return this.vis; + } + + /** + * Gets the Visualize embeddable's local filters + * @returns Local/panel-level array of filters for Visualize embeddable + */ + public getFilters() { + const filters = this.vis.serialize().data.searchSource?.filter ?? []; + // must clone the filters so that it's not read only, because mapAndFlattenFilters modifies the array + return mapAndFlattenFilters(_.cloneDeep(filters)); + } + + /** + * Gets the Visualize embeddable's local query + * @returns Local/panel-level query for Visualize embeddable + */ + public getQuery() { + return this.vis.serialize().data.searchSource.query; + } + + public getInspectorAdapters = () => { + if (!this.handler || (this.inspectorAdapters && !Object.keys(this.inspectorAdapters).length)) { + return undefined; + } + return this.handler.inspect(); + }; + + public openInspector = () => { + if (!this.handler) return; + + const adapters = this.handler.inspect(); + if (!adapters) return; + + return this.deps.start().plugins.inspector.open(adapters, { + title: + this.getTitle() || + i18n.translate('visualizations.embeddable.inspectorTitle', { + defaultMessage: 'Inspector', + }), + }); + }; + + /** + * Transfers all changes in the containerState.customization into + * the uiState of this visualization. + */ + public transferCustomizationsToUiState() { + // Check for changes that need to be forwarded to the uiState + // Since the vis has an own listener on the uiState we don't need to + // pass anything from here to the handler.update method + const visCustomizations = { vis: this.input.vis, table: this.input.table }; + if (visCustomizations.vis || visCustomizations.table) { + if (!_.isEqual(visCustomizations, this.visCustomizations)) { + this.visCustomizations = visCustomizations; + // Turn this off or the uiStateChangeHandler will fire for every modification. + this.vis.uiState.off('change', this.uiStateChangeHandler); + this.vis.uiState.clearAllKeys(); + + Object.entries(visCustomizations).forEach(([key, value]) => { + if (value) { + this.vis.uiState.set(key, value); + } + }); + + this.vis.uiState.on('change', this.uiStateChangeHandler); + } + } else if (this.parent) { + this.vis.uiState.clearAllKeys(); + } + } + + private handleChanges(): boolean { + this.transferCustomizationsToUiState(); + + let dirty = false; + + // Check if timerange has changed + const nextTimeRange = + this.input.timeslice !== undefined + ? { + from: new Date(this.input.timeslice[0]).toISOString(), + to: new Date(this.input.timeslice[1]).toISOString(), + mode: 'absolute' as 'absolute', + } + : this.input.timeRange; + if (!_.isEqual(nextTimeRange, this.timeRange)) { + this.timeRange = _.cloneDeep(nextTimeRange); + dirty = true; + } + + // Check if filters has changed + if (!onlyDisabledFiltersChanged(this.input.filters, this.filters)) { + this.filters = this.input.filters; + dirty = true; + } + + // Check if query has changed + if (!_.isEqual(this.input.query, this.query)) { + this.query = this.input.query; + dirty = true; + } + + if (this.searchSessionId !== this.input.searchSessionId) { + this.searchSessionId = this.input.searchSessionId; + dirty = true; + } + + if (this.syncColors !== this.input.syncColors) { + this.syncColors = this.input.syncColors; + dirty = true; + } + + if (this.syncTooltips !== this.input.syncTooltips) { + this.syncTooltips = this.input.syncTooltips; + dirty = true; + } + + if (this.syncCursor !== this.input.syncCursor) { + this.syncCursor = this.input.syncCursor; + dirty = true; + } + + if (this.embeddableTitle !== this.getTitle()) { + this.embeddableTitle = this.getTitle(); + dirty = true; + } + + if (this.vis.description && this.domNode) { + this.domNode.setAttribute('data-description', this.vis.description); + } + + return dirty; + } + + private handleWarnings() { + const warnings: React.ReactNode[] = []; + if (this.getInspectorAdapters()?.requests) { + this.deps + .start() + .plugins.data.search.showWarnings(this.getInspectorAdapters()!.requests!, (warning) => { + if (hasUnsupportedDownsampledAggregationFailure(warning)) { + warnings.push( + i18n.translate('visualizations.embeddable.tsdbRollupWarning', { + defaultMessage: + 'Visualization uses a function that is unsupported by rolled up data. Select a different function or change the time range.', + }) + ); + return true; + } + if (this.vis.type.suppressWarnings?.()) { + // if the vis type wishes to supress all warnings, return true so the default logic won't pick it up + return true; + } + }); + } + + if (this.warningDomNode) { + const { core } = this.deps.start(); + render( + + + , + this.warningDomNode + ); + } + } + + // this is a hack to make editor still work, will be removed once we clean up editor + // @ts-ignore + hasInspector = () => Boolean(this.getInspectorAdapters()); + + onContainerLoading = () => { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ + ...this.getOutput(), + loading: true, + rendered: false, + error: undefined, + }); + }; + + onContainerData = () => { + this.handleWarnings(); + this.updateOutput({ + ...this.getOutput(), + loading: false, + }); + }; + + onContainerRender = () => { + this.renderComplete.dispatchComplete(); + this.updateOutput({ + ...this.getOutput(), + rendered: true, + }); + }; + + onContainerError = (error: ExpressionRenderError) => { + if (this.abortController) { + this.abortController.abort(); + } + this.renderComplete.dispatchError(); + + if (isFallbackDataView(this.vis.data.indexPattern)) { + error = new Error( + i18n.translate('visualizations.missedDataView.errorMessage', { + defaultMessage: `Could not find the {type}: {id}`, + values: { + id: this.vis.data.indexPattern.id ?? '-', + type: this.vis.data.savedSearchId + ? i18n.translate('visualizations.noSearch.label', { + defaultMessage: 'search', + }) + : i18n.translate('visualizations.noDataView.label', { + defaultMessage: 'data view', + }), + }, + }) + ); + } + + this.updateOutput({ + ...this.getOutput(), + rendered: true, + error, + }); + }; + + /** + * + * @param {Element} domNode + */ + public async render(domNode: HTMLElement) { + this.timeRange = _.cloneDeep(this.input.timeRange); + + this.transferCustomizationsToUiState(); + + const div = document.createElement('div'); + div.className = `visualize panel-content panel-content--fullWidth`; + domNode.appendChild(div); + + const warningDiv = document.createElement('div'); + warningDiv.className = 'visPanel__warnings'; + domNode.appendChild(warningDiv); + this.warningDomNode = warningDiv; + + this.domNode = div; + super.render(this.domNode); + const { core } = this.deps.start(); + + render( + +
+ +
+
, + this.domNode + ); + + const expressions = getExpressions(); + this.handler = await expressions.loader(this.domNode, undefined, { + renderMode: this.input.renderMode || 'view', + onRenderError: (element: HTMLElement, error: ExpressionRenderError) => { + this.onContainerError(error); + }, + executionContext: this.getExecutionContext(), + }); + + this.subscriptions.push( + this.handler.events$ + .pipe( + mergeMap(async (event) => { + // Visualize doesn't respond to sizing events, so ignore. + if (isChartSizeEvent(event)) { + return; + } + if (!this.input.disableTriggers) { + const triggerId = get(VIS_EVENT_TO_TRIGGER, event.name, VIS_EVENT_TO_TRIGGER.filter); + let context; + + if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) { + context = { + embeddable: this, + timeFieldName: this.vis.data.indexPattern?.timeFieldName!, + ...event.data, + }; + } else { + context = { + embeddable: this, + data: { + timeFieldName: this.vis.data.indexPattern?.timeFieldName!, + ...event.data, + }, + }; + } + + await getUiActions().getTrigger(triggerId).exec(context); + } + }) + ) + .subscribe() + ); + + if (this.vis.description) { + div.setAttribute('data-description', this.vis.description); + } + + div.setAttribute('data-test-subj', 'visualizationLoader'); + div.setAttribute('data-shared-item', ''); + + this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading)); + this.subscriptions.push(this.handler.data$.subscribe(this.onContainerData)); + this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender)); + + this.subscriptions.push( + this.getUpdated$().subscribe(() => { + const { error } = this.getOutput(); + + if (error) { + render(this.renderError(error), this.domNode); + } + }) + ); + + await this.updateHandler(); + } + + private renderError(error: ErrorLike | string) { + const { core } = this.deps.start(); + if (isFallbackDataView(this.vis.data.indexPattern)) { + return ( + + + + ); + } + + return ( + + + + ); + } + + public destroy() { + super.destroy(); + this.subscriptions.forEach((s) => s.unsubscribe()); + this.vis.uiState.off('change', this.uiStateChangeHandler); + this.vis.uiState.off('reload', this.reload); + + if (this.handler) { + this.handler.destroy(); + this.handler.getElement().remove(); + } + } + + public reload = async () => { + await this.handleVisUpdate(); + }; + + private getExecutionContext() { + const parentContext = this.parent?.getInput().executionContext || getExecutionContext().get(); + const child: KibanaExecutionContext = { + type: 'agg_based', + name: this.vis.type.name, + id: this.vis.id ?? 'new', + description: this.vis.title || this.input.title || this.vis.type.name, + url: this.output.editUrl, + }; + + return { + ...parentContext, + child, + }; + } + + private async updateHandler() { + const context = this.getExecutionContext(); + + this.expressionVariables = await this.vis.type.getExpressionVariables?.( + this.vis, + this.timefilter + ); + + this.expressionVariablesSubject.next(this.expressionVariables); + + const expressionParams: IExpressionLoaderParams = { + searchContext: { + timeRange: this.timeRange, + query: this.input.query, + filters: this.input.filters, + disableWarningToasts: true, + }, + variables: { + embeddableTitle: this.getTitle(), + ...this.expressionVariables, + }, + searchSessionId: this.input.searchSessionId, + syncColors: this.input.syncColors, + syncTooltips: this.input.syncTooltips, + syncCursor: this.input.syncCursor, + uiState: this.vis.uiState, + interactive: !this.input.disableTriggers, + inspectorAdapters: this.inspectorAdapters, + executionContext: context, + }; + if (this.abortController) { + this.abortController.abort(); + } + this.abortController = new AbortController(); + const abortController = this.abortController; + + try { + this.expression = await toExpressionAst(this.vis, { + timefilter: this.timefilter, + timeRange: this.timeRange, + abortSignal: this.abortController!.signal, + }); + } catch (e) { + this.onContainerError(e); + } + + if (this.handler && !abortController.signal.aborted) { + this.handler.update(this.expression, expressionParams); + } + } + + private handleVisUpdate = async () => { + this.handleChanges(); + await this.updateHandler(); + }; + + private uiStateChangeHandler = () => { + this.updateInput({ + ...this.vis.uiState.toJSON(), + }); + }; + + public supportedTriggers(): string[] { + return this.vis.type.getSupportedTriggers?.(this.vis.params) ?? []; + } + + public getExpressionVariables$() { + return this.expressionVariablesSubject.asObservable(); + } + + public getExpressionVariables() { + return this.expressionVariables; + } + + inputIsRefType = (input: VisualizeInput): input is VisualizeByReferenceInput => { + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + return this.attributeService.inputIsRefType(input as VisualizeByReferenceInput); + }; + + getInputAsValueType = async (): Promise => { + const input = { + savedVis: this.vis.serialize(), + }; + delete input.savedVis.id; + _.unset(input, 'savedVis.title'); + return new Promise((resolve) => { + resolve({ ...(input as VisualizeByValueInput) }); + }); + }; + + getInputAsRefType = async (): Promise => { + const { plugins, core } = this.deps.start(); + const { data, spaces, savedObjectsTaggingOss } = plugins; + const savedVis = await getSavedVisualization({ + search: data.search, + dataViews: data.dataViews, + spaces, + savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), + ...core, + }); + if (!savedVis) { + throw new Error('Error creating a saved vis object'); + } + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + const saveModalTitle = this.getTitle() + ? this.getTitle() + : i18n.translate('visualizations.embeddable.placeholderTitle', { + defaultMessage: 'Placeholder Title', + }); + // @ts-ignore + const attributes: VisualizeSavedObjectAttributes = { + savedVis, + vis: this.vis, + title: this.vis.title, + }; + return this.attributeService.getInputAsRefType( + { + id: this.id, + attributes, + }, + { showSaveModal: true, saveModalTitle } + ); + }; +} diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_async.ts similarity index 100% rename from src/plugins/visualizations/public/embeddable/visualize_embeddable_async.ts rename to src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_async.ts diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.test.ts b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.test.ts similarity index 100% rename from src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.test.ts rename to src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.test.ts diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx similarity index 96% rename from src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx rename to src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx index e38924a76ef2..7594c8d42f2e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx @@ -28,7 +28,7 @@ import { AttributeService, } from '@kbn/embeddable-plugin/public'; import type { StartServicesGetter } from '@kbn/kibana-utils-plugin/public'; -import { checkForDuplicateTitle } from '../utils/saved_objects_utils/check_for_duplicate_title'; +import { checkForDuplicateTitle } from '../../utils/saved_objects_utils/check_for_duplicate_title'; import type { VisualizeByReferenceInput, VisualizeByValueInput, @@ -38,24 +38,24 @@ import type { VisualizeSavedObjectAttributes, } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import type { SerializedVis, Vis } from '../vis'; -import { createVisAsync } from '../vis_async'; -import { getCapabilities, getTypes } from '../services'; -import { showNewVisModal } from '../wizard'; +import type { SerializedVis, Vis } from '../../vis'; +import { createVisAsync } from '../../vis_async'; +import { getCapabilities, getTypes } from '../../services'; +import { showNewVisModal } from '../../wizard'; import { convertToSerializedVis, getSavedVisualization, saveVisualization, getFullPath, -} from '../utils/saved_visualize_utils'; +} from '../../utils/saved_visualize_utils'; import { extractControlsReferences, extractTimeSeriesReferences, injectTimeSeriesReferences, injectControlsReferences, -} from '../utils/saved_visualization_references'; +} from '../../utils/saved_visualization_references'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; -import type { VisualizationsStartDeps } from '../plugin'; +import type { VisualizationsStartDeps } from '../../plugin'; interface VisualizationAttributes extends SavedObjectAttributes { title: string; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index e8b13639fa09..24a2c488e0f7 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -126,7 +126,8 @@ import { VisualizationSavedObjectAttributes, } from '../common/content_management'; import { AddAggVisualizationPanelAction } from './actions/add_agg_vis_action'; -import { VisualizeSerializedState } from './react_embeddable/types'; +import type { VisualizeSerializedState } from './embeddable/types'; +import { getVisualizeEmbeddableFactoryLazy } from './embeddable'; /** * Interface for this plugin's returned setup/start contracts. @@ -308,7 +309,10 @@ export class VisualizationsPlugin * this should be replaced to use only scoped history after moving legacy apps to browser routing */ const history = createHashHistory(); - const { createVisEmbeddableFromObject } = await import('./embeddable'); + const [{ createVisEmbeddableFromObject }, { renderApp }] = await Promise.all([ + import('./legacy/embeddable'), + import('./visualize_app'), + ]); const services: VisualizeServices = { ...coreStart, history, @@ -352,7 +356,6 @@ export class VisualizationsPlugin }; params.element.classList.add('visAppWrapper'); - const { renderApp } = await import('./visualize_app'); if (pluginsStart.screenshotMode.isScreenshotMode()) { params.element.classList.add('visEditorScreenshotModeActive'); // @ts-expect-error TS error, cannot find type declaration for scss @@ -407,7 +410,7 @@ export class VisualizationsPlugin plugins: { embeddable: embeddableStart, embeddableEnhanced: embeddableEnhancedStart }, } = start(); - const { getVisualizeEmbeddableFactory } = await import('./react_embeddable'); + const getVisualizeEmbeddableFactory = await getVisualizeEmbeddableFactoryLazy(); return getVisualizeEmbeddableFactory({ embeddableStart, embeddableEnhancedStart }); }); embeddable.registerReactEmbeddableSavedObject({ diff --git a/src/plugins/visualizations/public/react_embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/react_embeddable/visualize_embeddable.tsx deleted file mode 100644 index ee95e8d0d94b..000000000000 --- a/src/plugins/visualizations/public/react_embeddable/visualize_embeddable.tsx +++ /dev/null @@ -1,549 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EuiEmptyPrompt, EuiFlexGroup, EuiLoadingChart } from '@elastic/eui'; -import { isChartSizeEvent } from '@kbn/chart-expressions-common'; -import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { EmbeddableEnhancedPluginStart } from '@kbn/embeddable-enhanced-plugin/public'; -import { - EmbeddableStart, - ReactEmbeddableFactory, - SELECT_RANGE_TRIGGER, -} from '@kbn/embeddable-plugin/public'; -import { ExpressionRendererParams, useExpressionRenderer } from '@kbn/expressions-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { dispatchRenderComplete } from '@kbn/kibana-utils-plugin/public'; -import { apiPublishesSettings } from '@kbn/presentation-containers'; -import { - apiHasAppContext, - apiHasDisableTriggers, - apiHasExecutionContext, - apiIsOfType, - apiPublishesTimeRange, - apiPublishesTimeslice, - apiPublishesUnifiedSearch, - apiPublishesViewMode, - fetch$, - getUnchangingComparator, - initializeTimeRange, - initializeTitles, - useStateFromPublishingSubject, -} from '@kbn/presentation-publishing'; -import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; -import { get, isEmpty, isEqual, isNil, omitBy } from 'lodash'; -import React, { useEffect, useRef } from 'react'; -import { BehaviorSubject, switchMap } from 'rxjs'; -import { VISUALIZE_APP_NAME, VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; -import { VIS_EVENT_TO_TRIGGER } from '../embeddable'; -import { getCapabilities, getInspector, getUiActions, getUsageCollection } from '../services'; -import { ACTION_CONVERT_TO_LENS } from '../triggers'; -import { urlFor } from '../utils/saved_visualize_utils'; -import type { SerializedVis, Vis } from '../vis'; -import { createVisInstance } from './create_vis_instance'; -import { getExpressionRendererProps } from './get_expression_renderer_props'; -import { saveToLibrary } from './save_to_library'; -import { deserializeState, serializeState } from './state'; -import { - ExtraSavedObjectProperties, - VisualizeApi, - VisualizeOutputState, - VisualizeRuntimeState, - VisualizeSerializedState, - isVisualizeSavedObjectState, -} from './types'; - -export const getVisualizeEmbeddableFactory: (deps: { - embeddableStart: EmbeddableStart; - embeddableEnhancedStart?: EmbeddableEnhancedPluginStart; -}) => ReactEmbeddableFactory = ({ - embeddableStart, - embeddableEnhancedStart, -}) => ({ - type: VISUALIZE_EMBEDDABLE_TYPE, - deserializeState, - buildEmbeddable: async (initialState: unknown, buildApi, uuid, parentApi) => { - // Handle state transfer from legacy visualize editor, which uses the legacy visualize embeddable and doesn't - // produce a snapshot state. If buildEmbeddable is passed only a savedObjectId in the state, this means deserializeState - // was never run, and it needs to be invoked manually - const state = isVisualizeSavedObjectState(initialState) - ? await deserializeState({ - rawState: initialState, - }) - : (initialState as VisualizeRuntimeState); - - // Initialize dynamic actions - const dynamicActionsApi = embeddableEnhancedStart?.initializeReactEmbeddableDynamicActions( - uuid, - () => titlesApi.panelTitle.getValue(), - state - ); - // if it is provided, start the dynamic actions manager - const maybeStopDynamicActions = dynamicActionsApi?.startDynamicActions(); - - const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); - - // Count renders; mostly used for testing. - const renderCount$ = new BehaviorSubject(0); - const hasRendered$ = new BehaviorSubject(false); - - // Track vis data and initialize it into a vis instance - const serializedVis$ = new BehaviorSubject(state.serializedVis); - const initialVisInstance = await createVisInstance(state.serializedVis); - const vis$ = new BehaviorSubject(initialVisInstance); - - // Track UI state - const onUiStateChange = () => serializedVis$.next(vis$.getValue().serialize()); - initialVisInstance.uiState.on('change', onUiStateChange); - vis$.subscribe((vis) => vis.uiState.on('change', onUiStateChange)); - - // When the serialized vis changes, update the vis instance - serializedVis$ - .pipe( - switchMap(async (serializedVis) => { - const currentVis = vis$.getValue(); - if (currentVis) currentVis.uiState.off('change', onUiStateChange); - const vis = await createVisInstance(serializedVis); - const { params, abortController } = await getExpressionParams(); - return { vis, params, abortController }; - }) - ) - .subscribe(({ vis, params, abortController }) => { - vis$.next(vis); - if (params) expressionParams$.next(params); - expressionAbortController$.next(abortController); - }); - - // Track visualizations linked to a saved object in the library - const savedObjectId$ = new BehaviorSubject( - state.savedObjectId ?? state.serializedVis.id - ); - const savedObjectProperties$ = new BehaviorSubject( - undefined - ); - const linkedToLibrary$ = new BehaviorSubject(state.linkedToLibrary); - - // Track the vis expression - const expressionParams$ = new BehaviorSubject({ - expression: '', - }); - - const expressionAbortController$ = new BehaviorSubject(new AbortController()); - let getExpressionParams: () => ReturnType = async () => ({ - params: expressionParams$.getValue(), - abortController: expressionAbortController$.getValue(), - }); - - const { - api: customTimeRangeApi, - serialize: serializeCustomTimeRange, - comparators: customTimeRangeComparators, - } = initializeTimeRange(state); - - const searchSessionId$ = new BehaviorSubject(''); - - const viewMode$ = apiPublishesViewMode(parentApi) - ? parentApi.viewMode - : new BehaviorSubject('view'); - - const executionContext = apiHasExecutionContext(parentApi) - ? parentApi.executionContext - : undefined; - - const disableTriggers = apiHasDisableTriggers(parentApi) - ? parentApi.disableTriggers - : undefined; - - const parentApiContext = apiHasAppContext(parentApi) ? parentApi.getAppContext() : undefined; - - const inspectorAdapters$ = new BehaviorSubject>({}); - - // Track data views - let initialDataViews: DataView[] | undefined = []; - if (initialVisInstance.data.indexPattern) - initialDataViews = [initialVisInstance.data.indexPattern]; - if (initialVisInstance.type.getUsedIndexPattern) { - initialDataViews = await initialVisInstance.type.getUsedIndexPattern( - initialVisInstance.params - ); - } - - const dataLoading$ = new BehaviorSubject(true); - - const defaultPanelTitle = new BehaviorSubject(initialVisInstance.title); - - const api = buildApi( - { - ...customTimeRangeApi, - ...titlesApi, - ...(dynamicActionsApi?.dynamicActionsApi ?? {}), - defaultPanelTitle, - dataLoading: dataLoading$, - dataViews: new BehaviorSubject(initialDataViews), - supportedTriggers: () => [ - ACTION_CONVERT_TO_LENS, - APPLY_FILTER_TRIGGER, - SELECT_RANGE_TRIGGER, - ], - serializeState: () => { - const savedObjectProperties = savedObjectProperties$.getValue(); - return serializeState({ - serializedVis: vis$.getValue().serialize(), - titles: serializeTitles(), - id: savedObjectId$.getValue(), - linkedToLibrary: - // In the visualize editor, linkedToLibrary should always be false to force the full state to be serialized, - // instead of just passing a reference to the linked saved object. Other contexts like dashboards should - // serialize the state with just the savedObjectId so that the current revision of the vis is always used - apiIsOfType(parentApi, VISUALIZE_APP_NAME) ? false : linkedToLibrary$.getValue(), - ...(savedObjectProperties ? { savedObjectProperties } : {}), - ...(dynamicActionsApi?.serializeDynamicActions?.() ?? {}), - ...serializeCustomTimeRange(), - }); - }, - getVis: () => vis$.getValue(), - getInspectorAdapters: () => inspectorAdapters$.getValue(), - getTypeDisplayName: () => - i18n.translate('visualizations.displayName', { - defaultMessage: 'visualization', - }), - onEdit: async () => { - const stateTransferService = embeddableStart.getStateTransfer(); - const visId = savedObjectId$.getValue(); - const editPath = visId ? urlFor(visId) : '#/edit_by_value'; - const parentTimeRange = apiPublishesTimeRange(parentApi) - ? parentApi.timeRange$.getValue() - : {}; - const customTimeRange = customTimeRangeApi.timeRange$.getValue(); - - await stateTransferService.navigateToEditor('visualize', { - path: editPath, - state: { - embeddableId: uuid, - valueInput: { - savedVis: vis$.getValue().serialize(), - title: api.panelTitle?.getValue(), - description: api.panelDescription?.getValue(), - timeRange: customTimeRange ?? parentTimeRange, - }, - originatingApp: parentApiContext?.currentAppId ?? '', - searchSessionId: searchSessionId$.getValue() || undefined, - originatingPath: parentApiContext?.getCurrentPath?.(), - }, - }); - }, - isEditingEnabled: () => { - if (viewMode$.getValue() !== 'edit') return false; - const readOnly = Boolean(vis$.getValue().type.disableEdit); - if (readOnly) return false; - const capabilities = getCapabilities(); - const isByValue = !savedObjectId$.getValue(); - if (isByValue) - return Boolean( - capabilities.dashboard?.showWriteControls && capabilities.visualize?.show - ); - else return Boolean(capabilities.visualize?.save); - }, - updateVis: async (visUpdates) => { - const currentSerializedVis = vis$.getValue().serialize(); - serializedVis$.next({ - ...currentSerializedVis, - ...visUpdates, - params: { - ...currentSerializedVis.params, - ...visUpdates.params, - }, - data: { - ...currentSerializedVis.data, - ...visUpdates.data, - }, - } as SerializedVis); - if (visUpdates.title) { - titlesApi.setPanelTitle(visUpdates.title); - } - }, - openInspector: () => { - const adapters = inspectorAdapters$.getValue(); - if (!adapters) return; - const inspector = getInspector(); - if (!inspector.isAvailable(adapters)) return; - return getInspector().open(adapters, { - title: - titlesApi.panelTitle?.getValue() || - i18n.translate('visualizations.embeddable.inspectorTitle', { - defaultMessage: 'Inspector', - }), - }); - }, - // Library transforms - saveToLibrary: (newTitle: string) => { - titlesApi.setPanelTitle(newTitle); - const { rawState, references } = serializeState({ - serializedVis: vis$.getValue().serialize(), - titles: { - ...serializeTitles(), - title: newTitle, - }, - }); - return saveToLibrary({ - uiState: vis$.getValue().uiState, - rawState: rawState as VisualizeOutputState, - references, - }); - }, - canLinkToLibrary: () => !state.linkedToLibrary, - canUnlinkFromLibrary: () => !!state.linkedToLibrary, - checkForDuplicateTitle: () => false, // Handled by saveToLibrary action - getByValueState: () => ({ - savedVis: vis$.getValue().serialize(), - ...serializeTitles(), - }), - getByReferenceState: (libraryId) => - serializeState({ - serializedVis: vis$.getValue().serialize(), - titles: serializeTitles(), - id: libraryId, - linkedToLibrary: true, - }).rawState, - }, - { - ...titleComparators, - ...customTimeRangeComparators, - ...(dynamicActionsApi?.dynamicActionsComparator ?? { - enhancements: getUnchangingComparator(), - }), - serializedVis: [ - serializedVis$, - (value) => { - serializedVis$.next(value); - }, - (a, b) => { - const visA = a - ? { - ...omitBy(a, isEmpty), - data: omitBy(a.data, isNil), - params: omitBy(a.params, isNil), - } - : {}; - const visB = b - ? { - ...omitBy(b, isEmpty), - data: omitBy(b.data, isNil), - params: omitBy(b.params, isNil), - } - : {}; - return isEqual(visA, visB); - }, - ], - savedObjectId: [ - savedObjectId$, - (value) => savedObjectId$.next(value), - (a, b) => { - if (!a && !b) return true; - return a === b; - }, - ], - savedObjectProperties: getUnchangingComparator(), - linkedToLibrary: [linkedToLibrary$, (value) => linkedToLibrary$.next(value)], - } - ); - - const fetchSubscription = fetch$(api) - .pipe( - switchMap(async (data) => { - const unifiedSearch = apiPublishesUnifiedSearch(parentApi) - ? { - query: data.query, - filters: data.filters, - } - : {}; - const searchSessionId = apiPublishesSearchSession(parentApi) ? data.searchSessionId : ''; - searchSessionId$.next(searchSessionId); - const settings = apiPublishesSettings(parentApi) - ? { - syncColors: parentApi.settings.syncColors$.getValue(), - syncCursor: parentApi.settings.syncCursor$.getValue(), - syncTooltips: parentApi.settings.syncTooltips$.getValue(), - } - : {}; - - dataLoading$.next(true); - - const timeslice = apiPublishesTimeslice(parentApi) - ? parentApi.timeslice$.getValue() - : undefined; - - const customTimeRange = customTimeRangeApi.timeRange$.getValue(); - const parentTimeRange = apiPublishesTimeRange(parentApi) ? data.timeRange : undefined; - const timesliceTimeRange = timeslice - ? { - from: new Date(timeslice[0]).toISOString(), - to: new Date(timeslice[1]).toISOString(), - mode: 'absolute' as 'absolute', - } - : undefined; - - // Precedence should be: - // custom time range from state > - // timeslice time range > - // parent API time range from e.g. unified search - const timeRangeToRender = customTimeRange ?? timesliceTimeRange ?? parentTimeRange; - - getExpressionParams = async () => { - return await getExpressionRendererProps({ - unifiedSearch, - vis: vis$.getValue(), - settings, - disableTriggers, - searchSessionId, - parentExecutionContext: executionContext, - abortController: expressionAbortController$.getValue(), - timeRange: timeRangeToRender, - onRender: async (renderCount) => { - if (renderCount === renderCount$.getValue()) return; - renderCount$.next(renderCount); - const visInstance = vis$.getValue(); - const visTypeName = visInstance.type.name; - - let telemetryVisTypeName = visTypeName; - if (visTypeName === 'metrics') { - telemetryVisTypeName = 'legacy_metric'; - } - if (visTypeName === 'pie' && visInstance.params.isDonut) { - telemetryVisTypeName = 'donut'; - } - if ( - visTypeName === 'area' && - visInstance.params.seriesParams.some( - (seriesParams: { mode: string }) => seriesParams.mode === 'stacked' - ) - ) { - telemetryVisTypeName = 'area_stacked'; - } - - getUsageCollection().reportUiCounter( - executionContext?.type ?? '', - 'count', - `render_agg_based_${telemetryVisTypeName}` - ); - - if (hasRendered$.getValue() === true) return; - hasRendered$.next(true); - hasRendered$.complete(); - }, - onEvent: async (event) => { - // Visualize doesn't respond to sizing events, so ignore. - if (isChartSizeEvent(event)) { - return; - } - const currentVis = vis$.getValue(); - if (!disableTriggers) { - const triggerId = get( - VIS_EVENT_TO_TRIGGER, - event.name, - VIS_EVENT_TO_TRIGGER.filter - ); - let context; - - if (triggerId === VIS_EVENT_TO_TRIGGER.applyFilter) { - context = { - embeddable: api, - timeFieldName: currentVis.data.indexPattern?.timeFieldName!, - ...event.data, - }; - } else { - context = { - embeddable: api, - data: { - timeFieldName: currentVis.data.indexPattern?.timeFieldName!, - ...event.data, - }, - }; - } - await getUiActions().getTrigger(triggerId).exec(context); - } - }, - onData: (_, inspectorAdapters) => { - inspectorAdapters$.next( - typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters - ); - dataLoading$.next(false); - }, - }); - }; - return await getExpressionParams(); - }) - ) - .subscribe(({ params, abortController }) => { - if (params) expressionParams$.next(params); - expressionAbortController$.next(abortController); - }); - - return { - api, - Component: () => { - const expressionParams = useStateFromPublishingSubject(expressionParams$); - const renderCount = useStateFromPublishingSubject(renderCount$); - const hasRendered = useStateFromPublishingSubject(hasRendered$); - const domNode = useRef(null); - const { error, isLoading } = useExpressionRenderer(domNode, expressionParams); - - useEffect(() => { - return () => { - fetchSubscription.unsubscribe(); - maybeStopDynamicActions?.stopDynamicActions(); - }; - }, []); - - useEffect(() => { - if (hasRendered && domNode.current) { - dispatchRenderComplete(domNode.current); - } - }, [hasRendered]); - - return ( -
- {/* Replicate the loading state for the expression renderer to avoid FOUC */} - - {isLoading && } - {!isLoading && error && ( - - {i18n.translate('visualizations.embeddable.errorTitle', { - defaultMessage: 'Unable to load visualization ', - })} - - } - body={ -

- {error.name}: {error.message} -

- } - /> - )} -
-
- ); - }, - }; - }, -}); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts index 18d6fc578d07..a7417585795f 100644 --- a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts @@ -15,7 +15,7 @@ import { confirmModalPromise } from './confirm_modal_promise'; import type { StartServices } from '../../types'; import { visualizationsClient } from '../../content_management'; import { VisualizationSavedObjectAttributes, VisualizationSavedObject } from '../../../common'; -import { VisualizeOutputState } from '../../react_embeddable/types'; +import { VisualizeOutputState } from '../../embeddable/types'; /** * Attempts to create the current object using the serialized source. If an object already diff --git a/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts index f972b0682bd8..7e8e86b469c5 100644 --- a/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts +++ b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts @@ -16,7 +16,7 @@ import { import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import { isObject } from 'lodash'; import { Reference } from '../../../common/content_management'; -import { VisualizeSavedVisInputState } from '../../react_embeddable/types'; +import { VisualizeSavedVisInputState } from '../../embeddable/types'; import { SavedVisState, SerializedVis, VisSavedObject } from '../../types'; import type { SerializableAttributes } from '../../vis_types/vis_type_alias_registry'; import { extractControlsReferences, injectControlsReferences } from './controls_references'; diff --git a/src/plugins/visualizations/public/visualize_app/types.ts b/src/plugins/visualizations/public/visualize_app/types.ts index 7a80dd2e4a8e..53fb35114d0f 100644 --- a/src/plugins/visualizations/public/visualize_app/types.ts +++ b/src/plugins/visualizations/public/visualize_app/types.ts @@ -53,7 +53,7 @@ import type { } from '..'; import type { ListingViewRegistry, SavedVisState } from '../types'; -import type { createVisEmbeddableFromObject } from '../embeddable'; +import type { createVisEmbeddableFromObject } from '../legacy/embeddable'; import type { VisEditorsRegistry } from '../vis_editors_registry'; export interface VisualizeAppState { diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts index 1ad0803edf38..3a3898093a8e 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualization_instance.ts @@ -16,7 +16,7 @@ import { createVisAsync } from '../../vis_async'; import { convertToSerializedVis, getSavedVisualization } from '../../utils/saved_visualize_utils'; import { SerializedVis, Vis, VisSavedObject, VisualizeEmbeddableContract } from '../..'; import type { VisInstance, VisualizeServices } from '../types'; -import { VisualizeInput } from '../../embeddable'; +import { VisualizeInput } from '../../legacy/embeddable'; function isErrorRelatedToRuntimeFields(error: ExpressionValueError['error']) { const originalError = error.original || error;