From f8bf3f49caebddb641e39cd6b348abebccea36a2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 13 Feb 2025 10:09:29 -0700 Subject: [PATCH] [visualize] fix Save to library action from a by value panel breaks the chart panel (#210125) Fixes https://github.com/elastic/kibana/issues/206921 ### Problem The visualize embeddable is inconstant when passing runtime state to `buildEmbeddable`. Sometimes, only `{ savedObjectId }` is provided. The embeddable tried to work around this by calling `deserializeState` in `buildEmbeddable`. There was a different bug with the `deserializeState` guard in `buildEmbeddable` where state like `{ savedObjectId, savedVis: {} }` would not pass the guard. Dashboard adds runtime state so `savedVis` was getting added to `initialState` and thus failing the guard This resulted in the visualize embeddable trying to initialize with state `{ savedObjectId }` instead of state in the shape `{ savedObjectId, serializedVis: {} }`. This resulted in error message like "Could not read properties of undefined" when the embeddable tried to read from `state.serializedVis.type`. ### Solution The solution is to ensure that `buildEmbeddable` is always passed runtime state containing `serializedVis`. This pattern is in line with the lens embeddable. ### Test instructions * install sample web logs * create agg based visualization * create new dashboard, add agg based visualization. Open context menu of vis and select "Unlink from library". (Side note, removing legacy visualizations from add panel makes it hard to add by-value agg based visualizations to a dashboard) * save dashboard * edit agg based vis * Click "Save to library" and fill out form * Verify visualization is rendered in dashboard. --------- Co-authored-by: Elastic Machine (cherry picked from commit 109dcce33864a4d8be2e5dc6ac088d8a9976afb5) --- .../visualizations/public/embeddable/state.ts | 19 ++++++++++++------ .../visualizations/public/embeddable/types.ts | 20 ------------------- .../embeddable/visualize_embeddable.tsx | 15 +++++--------- .../shared/visualizations/public/plugin.ts | 9 +++++++-- .../utils/get_top_nav_config.tsx | 5 ++++- 5 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/state.ts b/src/platform/plugins/shared/visualizations/public/embeddable/state.ts index 2b1baea47c829..1965865371310 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/state.ts +++ b/src/platform/plugins/shared/visualizations/public/embeddable/state.ts @@ -30,13 +30,11 @@ import { import { getSavedVisualization } from '../utils/saved_visualize_utils'; import type { SerializedVis } from '../vis'; import { - isVisualizeSavedObjectState, VisualizeSavedObjectInputState, VisualizeSerializedState, VisualizeRuntimeState, VisualizeSavedVisInputState, ExtraSavedObjectProperties, - isVisualizeRuntimeState, } from './types'; export const deserializeState = async ( @@ -49,15 +47,24 @@ export const deserializeState = async ( }, } as VisualizeRuntimeState; let serializedState = cloneDeep(state.rawState); - if (isVisualizeSavedObjectState(serializedState)) { - serializedState = await deserializeSavedObjectState(serializedState); - } else if (isVisualizeRuntimeState(serializedState)) { + if ((serializedState as VisualizeSavedObjectInputState).savedObjectId) { + serializedState = await deserializeSavedObjectState( + serializedState as VisualizeSavedObjectInputState + ); + } else if ((serializedState as VisualizeRuntimeState).serializedVis) { + // TODO remove once embeddable only exposes SerializedState + // Canvas passes incoming embeddable state in getSerializedStateForChild + // without this early return, serializedVis gets replaced in deserializeSavedVisState + // and breaks adding a new by-value embeddable in Canvas return serializedState as VisualizeRuntimeState; } const references: Reference[] = state.references ?? []; - const deserializedSavedVis = deserializeSavedVisState(serializedState, references); + const deserializedSavedVis = deserializeSavedVisState( + serializedState as VisualizeSavedVisInputState, + references + ); return { ...serializedState, diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/types.ts b/src/platform/plugins/shared/visualizations/public/embeddable/types.ts index ad7a7c86dde58..84b5c84e5d60e 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/types.ts +++ b/src/platform/plugins/shared/visualizations/public/embeddable/types.ts @@ -71,26 +71,6 @@ export type VisualizeOutputState = VisualizeSavedVisInputState & Required> & ExtraSavedObjectProperties; -export const isVisualizeSavedObjectState = ( - state: unknown -): state is VisualizeSavedObjectInputState => { - return ( - typeof state !== 'undefined' && - (state as VisualizeSavedObjectInputState).savedObjectId !== undefined && - !!(state as VisualizeSavedObjectInputState).savedObjectId && - !('savedVis' in (state as VisualizeSavedObjectInputState)) && - !('serializedVis' in (state as VisualizeSavedObjectInputState)) - ); -}; - -export const isVisualizeRuntimeState = (state: unknown): state is VisualizeRuntimeState => { - return ( - !isVisualizeSavedObjectState(state) && - !('savedVis' in (state as VisualizeRuntimeState)) && - (state as VisualizeRuntimeState).serializedVis !== undefined - ); -}; - export type VisualizeApi = Partial & PublishesDataViews & PublishesDataLoading & diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx index 873e030fdfb9f..57c95cfc60700 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx @@ -54,7 +54,6 @@ import { VisualizeOutputState, VisualizeRuntimeState, VisualizeSerializedState, - isVisualizeSavedObjectState, } from './types'; import { initializeEditApi } from './initialize_edit_api'; @@ -67,15 +66,11 @@ export const getVisualizeEmbeddableFactory: (deps: { }) => ({ 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); + buildEmbeddable: async (initialState, buildApi, uuid, parentApi) => { + const state = { + ...initialState, + linkedToLibrary: Boolean(initialState.savedObjectId), + }; // Initialize dynamic actions const dynamicActionsApi = embeddableEnhancedStart?.initializeReactEmbeddableDynamicActions( diff --git a/src/platform/plugins/shared/visualizations/public/plugin.ts b/src/platform/plugins/shared/visualizations/public/plugin.ts index ca4634fdefe93..c338988706c7d 100644 --- a/src/platform/plugins/shared/visualizations/public/plugin.ts +++ b/src/platform/plugins/shared/visualizations/public/plugin.ts @@ -406,10 +406,15 @@ export class VisualizationsPlugin return getVisualizeEmbeddableFactory({ embeddableStart, embeddableEnhancedStart }); }); embeddable.registerAddFromLibraryType({ - onAdd: (container, savedObject) => { + onAdd: async (container, savedObject) => { + const { deserializeState } = await import('./embeddable/state'); + const initialState = await deserializeState({ + rawState: { savedObjectId: savedObject.id }, + references: savedObject.references, + }); container.addNewPanel({ panelType: VISUALIZE_EMBEDDABLE_TYPE, - initialState: { savedObjectId: savedObject.id }, + initialState, }); }, savedObjectType: VISUALIZE_EMBEDDABLE_TYPE, diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx index 3dd181a85e4e1..245791a7460f5 100644 --- a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx +++ b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_top_nav_config.tsx @@ -187,7 +187,10 @@ export const getTopNavConfig = ( stateTransfer.navigateToWithEmbeddablePackage(app, { state: { type: VISUALIZE_EMBEDDABLE_TYPE, - input: { savedObjectId: id }, + input: { + serializedVis: vis.serialize(), + savedObjectId: id, + }, embeddableId: saveOptions.copyOnSave ? undefined : embeddableId, searchSessionId: data.search.session.getSessionId(), },