diff --git a/examples/controls_example/public/app/react_control_example/react_control_example.tsx b/examples/controls_example/public/app/react_control_example/react_control_example.tsx index 30111c21f1927..b6cb97720d79b 100644 --- a/examples/controls_example/public/app/react_control_example/react_control_example.tsx +++ b/examples/controls_example/public/app/react_control_example/react_control_example.tsx @@ -371,10 +371,10 @@ export const ReactControlExample = ({ { + onClick={() => { if (controlGroupApi) { saveNotification$.next(); - setControlGroupSerializedState(await controlGroupApi.serializeState()); + setControlGroupSerializedState(controlGroupApi.serializeState()); } }} > diff --git a/examples/embeddable_examples/public/app/presentation_container_example/page_api.ts b/examples/embeddable_examples/public/app/presentation_container_example/page_api.ts index 59f06847a1538..c08ba7a499b98 100644 --- a/examples/embeddable_examples/public/app/presentation_container_example/page_api.ts +++ b/examples/embeddable_examples/public/app/presentation_container_example/page_api.ts @@ -9,7 +9,6 @@ import { BehaviorSubject, Subject, combineLatest, map, merge } from 'rxjs'; import { v4 as generateId } from 'uuid'; -import { asyncForEach } from '@kbn/std'; import { TimeRange } from '@kbn/es-query'; import { PanelPackage, @@ -146,14 +145,14 @@ export function getPageApi() { }, onSave: async () => { const panelsState: LastSavedState['panelsState'] = []; - await asyncForEach(panels$.value, async ({ id, type }) => { + panels$.value.forEach(({ id, type }) => { try { const childApi = children$.value[id]; if (apiHasSerializableState(childApi)) { panelsState.push({ id, type, - panelState: await childApi.serializeState(), + panelState: childApi.serializeState(), }); } } catch (error) { diff --git a/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx b/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx index a3dcd06618155..18ff194769b3d 100644 --- a/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx +++ b/examples/embeddable_examples/public/app/state_management_example/state_management_example.tsx @@ -21,7 +21,6 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/presentation-publishing'; import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { BehaviorSubject, Subject } from 'rxjs'; -import useMountedState from 'react-use/lib/useMountedState'; import { SAVED_BOOK_ID } from '../../react_embeddables/saved_book/constants'; import { BookApi, @@ -32,7 +31,6 @@ import { lastSavedStateSessionStorage } from './last_saved_state'; import { unsavedChangesSessionStorage } from './unsaved_changes'; export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStart }) => { - const isMounted = useMountedState(); const saveNotification$ = useMemo(() => { return new Subject(); }, []); @@ -123,16 +121,13 @@ export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStar { + onClick={() => { if (!bookApi) { return; } setIsSaving(true); - const bookSerializedState = await bookApi.serializeState(); - if (!isMounted()) { - return; - } + const bookSerializedState = bookApi.serializeState(); lastSavedStateSessionStorage.save(bookSerializedState); saveNotification$.next(); // signals embeddable unsaved change tracking to update last saved state setHasUnsavedChanges(false); diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx b/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx index dfcff31dd8a8d..09f0e30f4a6ec 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/create_saved_book_action.tsx @@ -11,7 +11,7 @@ import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import { ADD_PANEL_TRIGGER, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; import { embeddableExamplesGrouping } from '../embeddable_examples_grouping'; import { @@ -21,7 +21,6 @@ import { } from './book_state'; import { ADD_SAVED_BOOK_ACTION_ID, SAVED_BOOK_ID } from './constants'; import { openSavedBookEditor } from './saved_book_editor'; -import { saveBookAttributes } from './saved_book_library'; import { BookRuntimeState } from './types'; export const registerCreateSavedBookAction = (uiActions: UiActionsPublicStart, core: CoreStart) => { @@ -36,19 +35,17 @@ export const registerCreateSavedBookAction = (uiActions: UiActionsPublicStart, c if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); const newPanelStateManager = stateManagerFromAttributes(defaultBookAttributes); - const { addToLibrary } = await openSavedBookEditor(newPanelStateManager, true, core, { - parentApi: embeddable, + const { savedBookId } = await openSavedBookEditor({ + attributesManager: newPanelStateManager, + parent: embeddable, + isCreate: true, + core, }); - const initialState: BookRuntimeState = await (async () => { - const bookAttributes = serializeBookAttributes(newPanelStateManager); - // if we're adding this to the library, we only need to return the by reference state. - if (addToLibrary) { - const savedBookId = await saveBookAttributes(undefined, bookAttributes); - return { savedBookId, ...bookAttributes }; - } - return bookAttributes; - })(); + const bookAttributes = serializeBookAttributes(newPanelStateManager); + const initialState: BookRuntimeState = savedBookId + ? { savedBookId, ...bookAttributes } + : { ...bookAttributes }; embeddable.addNewPanel({ panelType: SAVED_BOOK_ID, diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_editor.tsx b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_editor.tsx index c73b6e0bc3bda..0222e682b7b0d 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_editor.tsx +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_editor.tsx @@ -27,26 +27,32 @@ import { OverlayRef } from '@kbn/core-mount-utils-browser'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { - apiHasParentApi, + apiHasInPlaceLibraryTransforms, apiHasUniqueId, useBatchedOptionalPublishingSubjects, } from '@kbn/presentation-publishing'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import React from 'react'; +import React, { useState } from 'react'; import { serializeBookAttributes } from './book_state'; -import { BookAttributesManager } from './types'; +import { BookApi, BookAttributesManager } from './types'; +import { saveBookAttributes } from './saved_book_library'; -export const openSavedBookEditor = ( - attributesManager: BookAttributesManager, - isCreate: boolean, - core: CoreStart, - api: unknown -): Promise<{ addToLibrary: boolean }> => { +export const openSavedBookEditor = ({ + attributesManager, + isCreate, + core, + parent, + api, +}: { + attributesManager: BookAttributesManager; + isCreate: boolean; + core: CoreStart; + parent?: unknown; + api?: BookApi; +}): Promise<{ savedBookId?: string }> => { return new Promise((resolve) => { const closeOverlay = (overlayRef: OverlayRef) => { - if (apiHasParentApi(api) && tracksOverlays(api.parentApi)) { - api.parentApi.clearOverlays(); - } + if (tracksOverlays(parent)) parent.clearOverlays(); overlayRef.close(); }; @@ -54,8 +60,9 @@ export const openSavedBookEditor = ( const overlay = core.overlays.openFlyout( toMountPoint( { // set the state back to the initial state and reject attributesManager.authorName.next(initialState.authorName); @@ -64,16 +71,23 @@ export const openSavedBookEditor = ( attributesManager.numberOfPages.next(initialState.numberOfPages); closeOverlay(overlay); }} - onSubmit={(addToLibrary: boolean) => { + onSubmit={async (addToLibrary: boolean) => { + const savedBookId = addToLibrary + ? await saveBookAttributes( + apiHasInPlaceLibraryTransforms(api) ? api.libraryId$.value : undefined, + serializeBookAttributes(attributesManager) + ) + : undefined; + closeOverlay(overlay); - resolve({ addToLibrary }); + resolve({ savedBookId }); }} />, core ), { type: isCreate ? 'overlay' : 'push', - size: isCreate ? 'm' : 's', + size: 'm', onClose: () => closeOverlay(overlay), } ); @@ -83,9 +97,7 @@ export const openSavedBookEditor = ( * if our parent needs to know about the overlay, notify it. This allows the parent to close the overlay * when navigating away, or change certain behaviors based on the overlay being open. */ - if (apiHasParentApi(api) && tracksOverlays(api.parentApi)) { - api.parentApi.openOverlay(overlay, overlayOptions); - } + if (tracksOverlays(parent)) parent.openOverlay(overlay, overlayOptions); }); }; @@ -94,19 +106,24 @@ export const SavedBookEditor = ({ isCreate, onSubmit, onCancel, + api, }: { attributesManager: BookAttributesManager; isCreate: boolean; - onSubmit: (addToLibrary: boolean) => void; + onSubmit: (addToLibrary: boolean) => Promise; onCancel: () => void; + api?: BookApi; }) => { - const [addToLibrary, setAddToLibrary] = React.useState(false); - const [authorName, synopsis, bookTitle, numberOfPages] = useBatchedOptionalPublishingSubjects( - attributesManager.authorName, - attributesManager.bookSynopsis, - attributesManager.bookTitle, - attributesManager.numberOfPages - ); + const [libraryId, authorName, synopsis, bookTitle, numberOfPages] = + useBatchedOptionalPublishingSubjects( + api?.libraryId$, + attributesManager.authorName, + attributesManager.bookSynopsis, + attributesManager.bookTitle, + attributesManager.numberOfPages + ); + const [addToLibrary, setAddToLibrary] = useState(Boolean(libraryId)); + const [saving, setSaving] = useState(false); return ( <> @@ -130,6 +147,7 @@ export const SavedBookEditor = ({ })} > attributesManager.authorName.next(e.target.value)} /> @@ -140,6 +158,7 @@ export const SavedBookEditor = ({ })} > attributesManager.bookTitle.next(e.target.value)} /> @@ -150,6 +169,7 @@ export const SavedBookEditor = ({ })} > attributesManager.numberOfPages.next(+e.target.value)} /> @@ -160,6 +180,7 @@ export const SavedBookEditor = ({ })} > attributesManager.bookSynopsis.next(e.target.value)} /> @@ -168,7 +189,7 @@ export const SavedBookEditor = ({ - + {i18n.translate('embeddableExamples.savedBook.editor.cancel', { defaultMessage: 'Discard changes', })} @@ -176,19 +197,25 @@ export const SavedBookEditor = ({ - {isCreate && ( - - setAddToLibrary(!addToLibrary)} - /> - - )} - onSubmit(addToLibrary)} fill> + setAddToLibrary(!addToLibrary)} + /> + + + { + setSaving(true); + onSubmit(addToLibrary); + }} + fill + > {isCreate ? i18n.translate('embeddableExamples.savedBook.editor.create', { defaultMessage: 'Create book', diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_library.ts b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_library.ts index 3d989debf7092..ef22750d94578 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_library.ts +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_library.ts @@ -23,7 +23,7 @@ export const saveBookAttributes = async ( maybeId?: string, attributes?: BookAttributes ): Promise => { - await new Promise((r) => setTimeout(r, 100)); // simulate save to network. + await new Promise((r) => setTimeout(r, 500)); // simulate save to network. const id = maybeId ?? v4(); storage.set(id, attributes); return id; diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx index 58f0f4de8555c..74cd81cf51bde 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx @@ -81,14 +81,22 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => { { ...titlesApi, onEdit: async () => { - openSavedBookEditor(bookAttributesManager, false, core, api); + openSavedBookEditor({ + attributesManager: bookAttributesManager, + parent: api.parentApi, + isCreate: false, + core, + api, + }).then((result) => { + savedBookId$.next(result.savedBookId); + }); }, isEditingEnabled: () => true, getTypeDisplayName: () => i18n.translate('embeddableExamples.savedbook.editBook.displayName', { defaultMessage: 'book', }), - serializeState: async () => { + serializeState: () => { if (!Boolean(savedBookId$.value)) { // if this book is currently by value, we serialize the entire state. const bookByValueState: BookByValueSerializedState = { @@ -98,16 +106,11 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => { return { rawState: bookByValueState }; } - // if this book is currently by reference, we serialize the reference and write to the external store. + // if this book is currently by reference, we serialize the reference only. const bookByReferenceState: BookByReferenceSerializedState = { savedBookId: savedBookId$.value!, ...serializeTitles(), }; - - await saveBookAttributes( - savedBookId$.value, - serializeBookAttributes(bookAttributesManager) - ); return { rawState: bookByReferenceState }; }, diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 8f59132f05fbc..d7aa8342de0c1 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -40,7 +40,6 @@ "@kbn/kibana-utils-plugin", "@kbn/core-mount-utils-browser", "@kbn/react-kibana-mount", - "@kbn/std", "@kbn/shared-ux-router" ] } diff --git a/packages/presentation/presentation_containers/interfaces/serialized_state.ts b/packages/presentation/presentation_containers/interfaces/serialized_state.ts index 21011d46b2402..0368bd751ce78 100644 --- a/packages/presentation/presentation_containers/interfaces/serialized_state.ts +++ b/packages/presentation/presentation_containers/interfaces/serialized_state.ts @@ -8,7 +8,6 @@ */ import type { Reference } from '@kbn/content-management-utils'; -import type { MaybePromise } from '@kbn/utility-types'; /** * A package containing the serialized Embeddable state, with references extracted. When saving Embeddables using any @@ -24,7 +23,7 @@ export interface HasSerializableState { * Serializes all state into a format that can be saved into * some external store. The opposite of `deserialize` in the {@link ReactEmbeddableFactory} */ - serializeState: () => MaybePromise>; + serializeState: () => SerializedPanelState; } export const apiHasSerializableState = (api: unknown | null): api is HasSerializableState => { diff --git a/packages/presentation/presentation_containers/tsconfig.json b/packages/presentation/presentation_containers/tsconfig.json index 15fe397861700..8e25a7b80c6e2 100644 --- a/packages/presentation/presentation_containers/tsconfig.json +++ b/packages/presentation/presentation_containers/tsconfig.json @@ -10,6 +10,5 @@ "@kbn/presentation-publishing", "@kbn/core-mount-utils-browser", "@kbn/content-management-utils", - "@kbn/utility-types", ] } diff --git a/src/plugins/controls/public/controls/types.ts b/src/plugins/controls/public/controls/types.ts index dd5d38e96346b..8cc33b3513263 100644 --- a/src/plugins/controls/public/controls/types.ts +++ b/src/plugins/controls/public/controls/types.ts @@ -9,7 +9,7 @@ import { BehaviorSubject } from 'rxjs'; -import { SerializedPanelState } from '@kbn/presentation-containers'; +import { HasSerializableState } from '@kbn/presentation-containers'; import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types'; import { HasParentApi, @@ -39,16 +39,12 @@ export type DefaultControlApi = PublishesDataLoading & CanClearSelections & HasType & HasUniqueId & + HasSerializableState & HasParentApi & { setDataLoading: (loading: boolean) => void; setBlockingError: (error: Error | undefined) => void; grow: PublishingSubject; width: PublishingSubject; - - // Can not use HasSerializableState interface - // HasSerializableState types serializeState as function returning 'MaybePromise' - // Controls serializeState is sync - serializeState: () => SerializedPanelState; }; export type ControlApiRegistration = Omit< diff --git a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx index 66b0ed367481d..858b241ad7e5c 100644 --- a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx @@ -50,9 +50,9 @@ export function CopyToDashboardModal({ api, closeModal }: CopyToDashboardModalPr const dashboardId = api.parentApi.savedObjectId.value; - const onSubmit = useCallback(async () => { + const onSubmit = useCallback(() => { const dashboard = api.parentApi; - const panelToCopy = await dashboard.getDashboardPanelFromId(api.uuid); + const panelToCopy = dashboard.getDashboardPanelFromId(api.uuid); const runtimeSnapshot = apiHasSnapshottableState(api) ? api.snapshotRuntimeState() : undefined; if (!panelToCopy && !runtimeSnapshot) { diff --git a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts index f545f5c95ee67..5fcb6522b0152 100644 --- a/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -108,8 +108,8 @@ export function getDashboardApi({ viewModeManager, unifiedSearchManager, }); - async function getState() { - const { panels, references: panelReferences } = await panelsManager.internalApi.getState(); + function getState() { + const { panels, references: panelReferences } = panelsManager.internalApi.getState(); const dashboardState: DashboardState = { ...settingsManager.internalApi.getState(), ...unifiedSearchManager.internalApi.getState(), @@ -121,7 +121,7 @@ export function getDashboardApi({ let controlGroupReferences: Reference[] | undefined; if (controlGroupApi) { const { rawState: controlGroupSerializedState, references: extractedReferences } = - await controlGroupApi.serializeState(); + controlGroupApi.serializeState(); controlGroupReferences = extractedReferences; dashboardState.controlGroupInput = controlGroupSerializedState; } @@ -174,7 +174,7 @@ export function getDashboardApi({ isManaged, lastSavedId: savedObjectId$.value, viewMode: viewModeManager.api.viewMode.value, - ...(await getState()), + ...getState(), }); if (saveResult) { @@ -197,7 +197,7 @@ export function getDashboardApi({ }, runQuickSave: async () => { if (isManaged) return; - const { controlGroupReferences, dashboardState, panelReferences } = await getState(); + const { controlGroupReferences, dashboardState, panelReferences } = getState(); const saveResult = await getDashboardContentManagementService().saveDashboardState({ controlGroupReferences, currentState: dashboardState, diff --git a/src/plugins/dashboard/public/dashboard_api/panels_manager.ts b/src/plugins/dashboard/public/dashboard_api/panels_manager.ts index 4f082d1c0484f..5188a604631b4 100644 --- a/src/plugins/dashboard/public/dashboard_api/panels_manager.ts +++ b/src/plugins/dashboard/public/dashboard_api/panels_manager.ts @@ -13,11 +13,7 @@ import { v4 } from 'uuid'; import { asyncForEach } from '@kbn/std'; import type { Reference } from '@kbn/content-management-utils'; import { METRIC_TYPE } from '@kbn/analytics'; -import { - PanelPackage, - SerializedPanelState, - apiHasSerializableState, -} from '@kbn/presentation-containers'; +import { PanelPackage, apiHasSerializableState } from '@kbn/presentation-containers'; import { DefaultEmbeddableApi, EmbeddablePackageState, @@ -32,7 +28,6 @@ import { getPanelTitle, stateHasTitles, } from '@kbn/presentation-publishing'; -import { cloneDeep } from 'lodash'; import { apiHasSnapshottableState } from '@kbn/presentation-containers/interfaces/serialized_state'; import { i18n } from '@kbn/i18n'; import { coreServices, usageCollectionService } from '../services/kibana_services'; @@ -156,13 +151,11 @@ export function initializePanelsManager( }); } - async function getDashboardPanelFromId(panelId: string) { + function getDashboardPanelFromId(panelId: string) { const panel = panels$.value[panelId]; const child = children$.value[panelId]; if (!child || !panel) throw new PanelNotFoundError(); - const serialized = apiHasSerializableState(child) - ? await child.serializeState() - : { rawState: {} }; + const serialized = apiHasSerializableState(child) ? child.serializeState() : { rawState: {} }; return { type: panel.type, explicitInput: { ...panel.explicitInput, ...serialized.rawState }, @@ -181,7 +174,7 @@ export function initializePanelsManager( return titles; } - async function duplicateReactEmbeddableInput( + function duplicateReactEmbeddableInput( childApi: unknown, panelToClone: DashboardPanelState, panelTitles: string[] @@ -198,7 +191,7 @@ export function initializePanelsManager( * use in-place library transforms */ if (apiHasLibraryTransforms(childApi)) { - const byValueSerializedState = await childApi.getByValueState(); + const byValueSerializedState = childApi.getByValueState(); if (panelToClone.references) { pushReferences(prefixReferencesFromPanel(id, panelToClone.references)); } @@ -284,9 +277,9 @@ export function initializePanelsManager( canRemovePanels: () => trackPanel.expandedPanelId.value === undefined, children$, duplicatePanel: async (idToDuplicate: string) => { - const panelToClone = await getDashboardPanelFromId(idToDuplicate); + const panelToClone = getDashboardPanelFromId(idToDuplicate); - const duplicatedPanelState = await duplicateReactEmbeddableInput( + const duplicatedPanelState = duplicateReactEmbeddableInput( children$.value[idToDuplicate], panelToClone, await getPanelTitles() @@ -414,36 +407,23 @@ export function initializePanelsManager( } if (resetChangedPanelCount) children$.next(currentChildren); }, - getState: async (): Promise<{ + getState: (): { panels: DashboardState['panels']; references: Reference[]; - }> => { + } => { const references: Reference[] = []; - const panels = cloneDeep(panels$.value); - - const serializePromises: Array< - Promise<{ uuid: string; serialized: SerializedPanelState }> - > = []; - for (const uuid of Object.keys(panels)) { - const api = children$.value[uuid]; - - if (apiHasSerializableState(api)) { - serializePromises.push( - (async () => { - const serialized = await api.serializeState(); - return { uuid, serialized }; - })() - ); - } - } - const serializeResults = await Promise.all(serializePromises); - for (const result of serializeResults) { - panels[result.uuid].explicitInput = { ...result.serialized.rawState, id: result.uuid }; - references.push( - ...prefixReferencesFromPanel(result.uuid, result.serialized.references ?? []) - ); - } + const panels = Object.keys(panels$.value).reduce((acc, id) => { + const childApi = children$.value[id]; + const serializeResult = apiHasSerializableState(childApi) + ? childApi.serializeState() + : { rawState: {} }; + acc[id] = { ...panels$.value[id], explicitInput: { ...serializeResult.rawState, id } }; + + references.push(...prefixReferencesFromPanel(id, serializeResult.references ?? [])); + + return acc; + }, {} as DashboardPanelMap); return { panels, references }; }, diff --git a/src/plugins/dashboard/public/dashboard_api/types.ts b/src/plugins/dashboard/public/dashboard_api/types.ts index 81dce821c0c10..b27f88a02aec9 100644 --- a/src/plugins/dashboard/public/dashboard_api/types.ts +++ b/src/plugins/dashboard/public/dashboard_api/types.ts @@ -145,7 +145,7 @@ export type DashboardApi = CanExpandPanels & fullScreenMode$: PublishingSubject; focusedPanelId$: PublishingSubject; forceRefresh: () => void; - getDashboardPanelFromId: (id: string) => Promise; + getDashboardPanelFromId: (id: string) => DashboardPanelState; getSettings: () => DashboardSettings; hasOverlays$: PublishingSubject; hasUnsavedChanges$: PublishingSubject; diff --git a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx index c1b5cf9f7695f..1f97e2de66390 100644 --- a/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/plugins/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { FetchContext, + getUnchangingComparator, initializeTimeRange, initializeTitles, useBatchedPublishingSubjects, @@ -186,7 +187,7 @@ export const getSearchEmbeddableFactory = ({ defaultPanelTitle$.next(undefined); defaultPanelDescription$.next(undefined); }, - serializeState: async () => + serializeState: () => serializeState({ uuid, initialState, @@ -194,7 +195,6 @@ export const getSearchEmbeddableFactory = ({ serializeTitles, serializeTimeRange: timeRange.serialize, savedObjectId: savedObjectId$.getValue(), - discoverServices, }), getInspectorAdapters: () => searchEmbeddable.stateManager.inspectorAdapters.getValue(), }, @@ -202,6 +202,7 @@ export const getSearchEmbeddableFactory = ({ ...titleComparators, ...timeRange.comparators, ...searchEmbeddable.comparators, + rawSavedObjectAttributes: getUnchangingComparator(), savedObjectId: [savedObjectId$, (value) => savedObjectId$.next(value)], savedObjectTitle: [defaultPanelTitle$, (value) => defaultPanelTitle$.next(value)], savedObjectDescription: [ diff --git a/src/plugins/discover/public/embeddable/types.ts b/src/plugins/discover/public/embeddable/types.ts index 94d0b10cc3f64..0443801ec7245 100644 --- a/src/plugins/discover/public/embeddable/types.ts +++ b/src/plugins/discover/public/embeddable/types.ts @@ -68,9 +68,13 @@ export interface NonPersistedDisplayOptions { enableFilters?: boolean; } +export type EditableSavedSearchAttributes = Partial< + Pick +>; + export type SearchEmbeddableSerializedState = SerializedTitles & SerializedTimeRange & - Partial> & { + EditableSavedSearchAttributes & { // by value attributes?: SavedSearchAttributes & { references: SavedSearch['references'] }; // by reference @@ -81,6 +85,7 @@ export type SearchEmbeddableSerializedState = SerializedTitles & export type SearchEmbeddableRuntimeState = SearchEmbeddableSerializedAttributes & SerializedTitles & SerializedTimeRange & { + rawSavedObjectAttributes?: EditableSavedSearchAttributes; savedObjectTitle?: string; savedObjectId?: string; savedObjectDescription?: string; diff --git a/src/plugins/discover/public/embeddable/utils/serialization_utils.test.ts b/src/plugins/discover/public/embeddable/utils/serialization_utils.test.ts index c91aacb89aff6..2a7a23b3600a7 100644 --- a/src/plugins/discover/public/embeddable/utils/serialization_utils.test.ts +++ b/src/plugins/discover/public/embeddable/utils/serialization_utils.test.ts @@ -121,7 +121,6 @@ describe('Serialization utils', () => { savedSearch, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), - discoverServices: discoverServiceMock, }); expect(serializedState).toEqual({ @@ -148,19 +147,16 @@ describe('Serialization utils', () => { searchSource, }; - beforeAll(() => { - discoverServiceMock.savedSearch.get = jest.fn().mockResolvedValue(savedSearch); - }); - - test('equal state', async () => { - const serializedState = await serializeState({ + test('equal state', () => { + const serializedState = serializeState({ uuid, - initialState: {}, + initialState: { + rawSavedObjectAttributes: savedSearch, + }, savedSearch, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), savedObjectId: 'test-id', - discoverServices: discoverServiceMock, }); expect(serializedState).toEqual({ @@ -171,15 +167,16 @@ describe('Serialization utils', () => { }); }); - test('overwrite state', async () => { - const serializedState = await serializeState({ + test('overwrite state', () => { + const serializedState = serializeState({ uuid, - initialState: {}, + initialState: { + rawSavedObjectAttributes: savedSearch, + }, savedSearch: { ...savedSearch, sampleSize: 500, sort: [['order_date', 'asc']] }, serializeTitles: jest.fn(), serializeTimeRange: jest.fn(), savedObjectId: 'test-id', - discoverServices: discoverServiceMock, }); expect(serializedState).toEqual({ diff --git a/src/plugins/discover/public/embeddable/utils/serialization_utils.ts b/src/plugins/discover/public/embeddable/utils/serialization_utils.ts index f193d52054a3c..397d078dba3e3 100644 --- a/src/plugins/discover/public/embeddable/utils/serialization_utils.ts +++ b/src/plugins/discover/public/embeddable/utils/serialization_utils.ts @@ -43,6 +43,7 @@ export const deserializeState = async ({ const { get } = discoverServices.savedSearch; const so = await get(savedObjectId, true); + const rawSavedObjectAttributes = pick(so, EDITABLE_SAVED_SEARCH_KEYS); const savedObjectOverride = pick(serializedState.rawState, EDITABLE_SAVED_SEARCH_KEYS); return { // ignore the time range from the saved object - only global time range + panel time range matter @@ -53,6 +54,9 @@ export const deserializeState = async ({ // Overwrite SO state with dashboard state for title, description, columns, sort, etc. ...panelState, ...savedObjectOverride, + + // back up the original saved object attributes for comparison + rawSavedObjectAttributes, }; } else { // by value @@ -72,14 +76,13 @@ export const deserializeState = async ({ } }; -export const serializeState = async ({ +export const serializeState = ({ uuid, initialState, savedSearch, serializeTitles, serializeTimeRange, savedObjectId, - discoverServices, }: { uuid: string; initialState: SearchEmbeddableRuntimeState; @@ -87,19 +90,17 @@ export const serializeState = async ({ serializeTitles: () => SerializedTitles; serializeTimeRange: () => SerializedTimeRange; savedObjectId?: string; - discoverServices: DiscoverServices; -}): Promise> => { +}): SerializedPanelState => { const searchSource = savedSearch.searchSource; const { searchSourceJSON, references: originalReferences } = searchSource.serialize(); const savedSearchAttributes = toSavedSearchAttributes(savedSearch, searchSourceJSON); if (savedObjectId) { - const { get } = discoverServices.savedSearch; - const so = await get(savedObjectId); + const editableAttributesBackup = initialState.rawSavedObjectAttributes ?? {}; // only save the current state that is **different** than the saved object state const overwriteState = EDITABLE_SAVED_SEARCH_KEYS.reduce((prev, key) => { - if (deepEqual(savedSearchAttributes[key], so[key])) { + if (deepEqual(savedSearchAttributes[key], editableAttributesBackup[key])) { return prev; } return { ...prev, [key]: savedSearchAttributes[key] }; diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index a9c4821d71a53..2119e6de03a8a 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -18,7 +18,7 @@ import { import { PresentationPanel, PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; import { ComparatorDefinition, StateComparators } from '@kbn/presentation-publishing'; import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'; -import { BehaviorSubject, combineLatest, debounceTime, skip, Subscription, switchMap } from 'rxjs'; +import { BehaviorSubject, combineLatest, debounceTime, map, skip, Subscription } from 'rxjs'; import { v4 as generateId } from 'uuid'; import { getReactEmbeddableFactory } from './react_embeddable_registry'; import { @@ -142,15 +142,7 @@ export const ReactEmbeddableRenderer = < .pipe( skip(1), debounceTime(ON_STATE_CHANGE_DEBOUNCE), - switchMap(() => { - const isAsync = - apiRegistration.serializeState.prototype?.name === 'AsyncFunction'; - return isAsync - ? (apiRegistration.serializeState() as Promise< - SerializedPanelState - >) - : Promise.resolve(apiRegistration.serializeState()); - }) + map(() => apiRegistration.serializeState()) ) .subscribe((nextSerializedState) => { onAnyStateChange(nextSerializedState); diff --git a/src/plugins/links/public/embeddable/links_embeddable.tsx b/src/plugins/links/public/embeddable/links_embeddable.tsx index 685f0a6c46a3b..a1bff3702c6ce 100644 --- a/src/plugins/links/public/embeddable/links_embeddable.tsx +++ b/src/plugins/links/public/embeddable/links_embeddable.tsx @@ -121,7 +121,7 @@ export const getLinksEmbeddableFactory = () => { delete snapshot.savedObjectId; return snapshot; }, - serializeState: async (): Promise> => { + serializeState: (): SerializedPanelState => { if (savedObjectId$.value !== undefined) { const linksByReferenceState: LinksByReferenceSerializedState = { savedObjectId: savedObjectId$.value,