mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
02a2ff106e
commit
abfd590d4d
21 changed files with 154 additions and 164 deletions
|
@ -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());
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/core-mount-utils-browser",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/std",
|
||||
"@kbn/shared-ux-router"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -10,6 +10,5 @@
|
|||
"@kbn/presentation-publishing",
|
||||
"@kbn/core-mount-utils-browser",
|
||||
"@kbn/content-management-utils",
|
||||
"@kbn/utility-types",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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] };
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue