[Embeddables Rebuild] Make Serialize Function Synchronous (#203662)

changes the signature of the `serializeState` function so that
it no longer returns MaybePromise
This commit is contained in:
Devon Thomson 2024-12-12 21:25:03 -05:00 committed by GitHub
parent 02a2ff106e
commit abfd590d4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 154 additions and 164 deletions

View file

@ -371,10 +371,10 @@ export const ReactControlExample = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={async () => {
onClick={() => {
if (controlGroupApi) {
saveNotification$.next();
setControlGroupSerializedState(await controlGroupApi.serializeState());
setControlGroupSerializedState(controlGroupApi.serializeState());
}
}}
>

View file

@ -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) {

View file

@ -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<void>();
}, []);
@ -123,16 +121,13 @@ export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStar
<EuiFlexItem grow={false}>
<EuiButton
disabled={isSaving || !hasUnsavedChanges}
onClick={async () => {
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);

View file

@ -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<BookRuntimeState>({
panelType: SAVED_BOOK_ID,

View file

@ -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(
<SavedBookEditor
attributesManager={attributesManager}
api={api}
isCreate={isCreate}
attributesManager={attributesManager}
onCancel={() => {
// 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<void>;
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 = ({
})}
>
<EuiFieldText
disabled={saving}
value={authorName ?? ''}
onChange={(e) => attributesManager.authorName.next(e.target.value)}
/>
@ -140,6 +158,7 @@ export const SavedBookEditor = ({
})}
>
<EuiFieldText
disabled={saving}
value={bookTitle ?? ''}
onChange={(e) => attributesManager.bookTitle.next(e.target.value)}
/>
@ -150,6 +169,7 @@ export const SavedBookEditor = ({
})}
>
<EuiFieldNumber
disabled={saving}
value={numberOfPages ?? ''}
onChange={(e) => attributesManager.numberOfPages.next(+e.target.value)}
/>
@ -160,6 +180,7 @@ export const SavedBookEditor = ({
})}
>
<EuiTextArea
disabled={saving}
value={synopsis ?? ''}
onChange={(e) => attributesManager.bookSynopsis.next(e.target.value)}
/>
@ -168,7 +189,7 @@ export const SavedBookEditor = ({
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onCancel} flush="left">
<EuiButtonEmpty disabled={saving} iconType="cross" onClick={onCancel} flush="left">
{i18n.translate('embeddableExamples.savedBook.editor.cancel', {
defaultMessage: 'Discard changes',
})}
@ -176,19 +197,25 @@ export const SavedBookEditor = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
{isCreate && (
<EuiFlexItem grow={false}>
<EuiSwitch
label={i18n.translate('embeddableExamples.savedBook.editor.addToLibrary', {
defaultMessage: 'Save to library',
})}
checked={addToLibrary}
onChange={() => setAddToLibrary(!addToLibrary)}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton onClick={() => onSubmit(addToLibrary)} fill>
<EuiSwitch
label={i18n.translate('embeddableExamples.savedBook.editor.addToLibrary', {
defaultMessage: 'Save to library',
})}
checked={addToLibrary}
disabled={saving}
onChange={() => setAddToLibrary(!addToLibrary)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isLoading={saving}
onClick={() => {
setSaving(true);
onSubmit(addToLibrary);
}}
fill
>
{isCreate
? i18n.translate('embeddableExamples.savedBook.editor.create', {
defaultMessage: 'Create book',

View file

@ -23,7 +23,7 @@ export const saveBookAttributes = async (
maybeId?: string,
attributes?: BookAttributes
): Promise<string> => {
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;

View file

@ -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 };
},

View file

@ -40,7 +40,6 @@
"@kbn/kibana-utils-plugin",
"@kbn/core-mount-utils-browser",
"@kbn/react-kibana-mount",
"@kbn/std",
"@kbn/shared-ux-router"
]
}

View file

@ -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<State extends object = object> {
* Serializes all state into a format that can be saved into
* some external store. The opposite of `deserialize` in the {@link ReactEmbeddableFactory}
*/
serializeState: () => MaybePromise<SerializedPanelState<State>>;
serializeState: () => SerializedPanelState<State>;
}
export const apiHasSerializableState = (api: unknown | null): api is HasSerializableState => {

View file

@ -10,6 +10,5 @@
"@kbn/presentation-publishing",
"@kbn/core-mount-utils-browser",
"@kbn/content-management-utils",
"@kbn/utility-types",
]
}

View file

@ -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<DefaultControlState> &
HasParentApi<ControlGroupApi> & {
setDataLoading: (loading: boolean) => void;
setBlockingError: (error: Error | undefined) => void;
grow: PublishingSubject<boolean | undefined>;
width: PublishingSubject<ControlWidth | undefined>;
// Can not use HasSerializableState interface
// HasSerializableState types serializeState as function returning 'MaybePromise'
// Controls serializeState is sync
serializeState: () => SerializedPanelState<DefaultControlState>;
};
export type ControlApiRegistration<ControlApi extends DefaultControlApi = DefaultControlApi> = Omit<

View file

@ -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) {

View file

@ -111,8 +111,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(),
@ -124,7 +124,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;
}
@ -177,7 +177,7 @@ export function getDashboardApi({
isManaged,
lastSavedId: savedObjectId$.value,
viewMode: viewModeManager.api.viewMode.value,
...(await getState()),
...getState(),
});
if (saveResult) {
@ -200,7 +200,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,

View file

@ -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<object> }>
> = [];
for (const uuid of Object.keys(panels)) {
const api = children$.value[uuid];
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 } };
if (apiHasSerializableState(api)) {
serializePromises.push(
(async () => {
const serialized = await api.serializeState();
return { uuid, serialized };
})()
);
}
}
references.push(...prefixReferencesFromPanel(id, serializeResult.references ?? []));
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 ?? [])
);
}
return acc;
}, {} as DashboardPanelMap);
return { panels, references };
},

View file

@ -153,7 +153,7 @@ export type DashboardApi = CanExpandPanels &
focusedPanelId$: PublishingSubject<string | undefined>;
forceRefresh: () => void;
getSettings: () => DashboardStateFromSettingsFlyout;
getDashboardPanelFromId: (id: string) => Promise<DashboardPanelState>;
getDashboardPanelFromId: (id: string) => DashboardPanelState;
hasOverlays$: PublishingSubject<boolean>;
hasUnsavedChanges$: PublishingSubject<boolean>;
highlightPanel: (panelRef: HTMLDivElement) => void;

View file

@ -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: [

View file

@ -68,9 +68,13 @@ export interface NonPersistedDisplayOptions {
enableFilters?: boolean;
}
export type EditableSavedSearchAttributes = Partial<
Pick<SavedSearchAttributes, (typeof EDITABLE_SAVED_SEARCH_KEYS)[number]>
>;
export type SearchEmbeddableSerializedState = SerializedTitles &
SerializedTimeRange &
Partial<Pick<SavedSearchAttributes, (typeof EDITABLE_SAVED_SEARCH_KEYS)[number]>> & {
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;

View file

@ -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({

View file

@ -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<SearchEmbeddableSerializedState>> => {
}): SerializedPanelState<SearchEmbeddableSerializedState> => {
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] };

View file

@ -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<SerializedState>
>)
: Promise.resolve(apiRegistration.serializeState());
})
map(() => apiRegistration.serializeState())
)
.subscribe((nextSerializedState) => {
onAnyStateChange(nextSerializedState);

View file

@ -121,7 +121,7 @@ export const getLinksEmbeddableFactory = () => {
delete snapshot.savedObjectId;
return snapshot;
},
serializeState: async (): Promise<SerializedPanelState<LinksSerializedState>> => {
serializeState: (): SerializedPanelState<LinksSerializedState> => {
if (savedObjectId$.value !== undefined) {
const linksByReferenceState: LinksByReferenceSerializedState = {
savedObjectId: savedObjectId$.value,