diff --git a/examples/embeddable_examples/public/app/presentation_container_example/components/presentation_container_example.tsx b/examples/embeddable_examples/public/app/presentation_container_example/components/presentation_container_example.tsx
index 5cd4b48eacfe5..b0591d06bf832 100644
--- a/examples/embeddable_examples/public/app/presentation_container_example/components/presentation_container_example.tsx
+++ b/examples/embeddable_examples/public/app/presentation_container_example/components/presentation_container_example.tsx
@@ -94,7 +94,7 @@ export const PresentationContainerExample = ({ uiActions }: { uiActions: UiActio
diff --git a/examples/embeddable_examples/public/app/presentation_container_example/components/top_nav.tsx b/examples/embeddable_examples/public/app/presentation_container_example/components/top_nav.tsx
index a7950f863b2db..791b4934c4f91 100644
--- a/examples/embeddable_examples/public/app/presentation_container_example/components/top_nav.tsx
+++ b/examples/embeddable_examples/public/app/presentation_container_example/components/top_nav.tsx
@@ -14,7 +14,7 @@ import type { PublishesUnsavedChanges } from '@kbn/presentation-publishing';
interface Props {
onSave: () => Promise;
- resetUnsavedChanges: PublishesUnsavedChanges['resetUnsavedChanges'];
+ onReset: () => void;
hasUnsavedChanges$: PublishesUnsavedChanges['hasUnsavedChanges$'];
}
@@ -40,7 +40,7 @@ export function TopNav(props: Props) {
Unsaved changes
-
+
Reset
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 1c0b2c8fe600e..7cf749edf1f79 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
@@ -20,7 +20,6 @@ import type {
import {
apiHasSerializableState,
apiPublishesDataLoading,
- apiPublishesUnsavedChanges,
childrenUnsavedChanges$,
combineCompatibleChildrenApis,
} from '@kbn/presentation-publishing';
@@ -156,6 +155,31 @@ export function getPageApi() {
unsavedChangesSessionStorage.save(serializePage());
});
+ const applySerializedState = (state?: PageState) => {
+ const nextState = state ?? initialState;
+ timeRange$.next(nextState.timeRange);
+ const { layout: nextLayout, childState: nextChildState } = deserializePanels(nextState.panels);
+ layout$.next(nextLayout);
+ currentChildState = nextChildState;
+ let childrenModified = false;
+ const currentChildren = { ...children$.value };
+ for (const uuid of Object.keys(currentChildren)) {
+ const existsInNextLayout = nextLayout.some(({ id }) => id === uuid);
+ if (existsInNextLayout) {
+ const child = currentChildren[uuid];
+ if (apiHasSerializableState(child)) {
+ void child.applySerializedState(nextChildState[uuid]);
+ }
+ } else {
+ // if reset resulted in panel removal, we need to update the list of children
+ delete currentChildren[uuid];
+ delete currentChildState[uuid];
+ childrenModified = true;
+ }
+ }
+ if (childrenModified) children$.next(currentChildren);
+ };
+
return {
cleanUp: () => {
childrenDataLoadingSubscripiton.unsubscribe();
@@ -176,6 +200,9 @@ export function getPageApi() {
lastSavedStateSessionStorage.save(serializedPage);
unsavedChangesSessionStorage.clear();
},
+ onReset: () => {
+ applySerializedState(lastSavedState$.value);
+ },
layout$,
setChild: (id: string, api: unknown) => {
children$.next({
@@ -219,30 +246,8 @@ export function getPageApi() {
lastSavedStateForChild$: (panelId: string) =>
lastSavedState$.pipe(map(() => getLastSavedStateForChild(panelId))),
getLastSavedStateForChild,
- resetUnsavedChanges: () => {
- const lastSavedState = lastSavedState$.value;
- timeRange$.next(lastSavedState.timeRange);
- const { layout: lastSavedLayout, childState: lastSavedChildState } = deserializePanels(
- lastSavedState.panels
- );
- layout$.next(lastSavedLayout);
- currentChildState = lastSavedChildState;
- let childrenModified = false;
- const currentChildren = { ...children$.value };
- for (const uuid of Object.keys(currentChildren)) {
- const existsInLastSavedLayout = lastSavedLayout.some(({ id }) => id === uuid);
- if (existsInLastSavedLayout) {
- const child = currentChildren[uuid];
- if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
- } else {
- // if reset resulted in panel removal, we need to update the list of children
- delete currentChildren[uuid];
- delete currentChildState[uuid];
- childrenModified = true;
- }
- }
- if (childrenModified) children$.next(currentChildren);
- },
+ serializeState: serializePage,
+ applySerializedState,
timeRange$,
hasUnsavedChanges$,
} as PageApi,
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 0a570e76e4a69..dbe643d789bc0 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
@@ -134,7 +134,9 @@ export const StateManagementExample = ({ uiActions }: { uiActions: UiActionsStar
{
- bookApi?.resetUnsavedChanges();
+ bookApi?.applySerializedState(
+ parentApi.getLastSavedStateForChild(BOOK_EMBEDDABLE_ID) as BookEmbeddableState
+ );
}}
>
Reset
diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_embeddable.tsx
index 787c31074491f..b94ac8c751fc3 100644
--- a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_embeddable.tsx
+++ b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_embeddable.tsx
@@ -109,7 +109,7 @@ export const getFieldListFactory = (
})
);
- function serializeState() {
+ function serializeState(): FieldListSerializedState {
const { dataViews: selectedDataViews, ...rest } = fieldListStateManager.getLatestState();
return {
...titleManager.getLatestState(),
@@ -117,7 +117,7 @@ export const getFieldListFactory = (
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
serializeState,
diff --git a/examples/portable_dashboards_example/public/filter_debugger_embeddable.tsx b/examples/portable_dashboards_example/public/filter_debugger_embeddable.tsx
index 8dbd437be4f9f..f7ad5275d7ba2 100644
--- a/examples/portable_dashboards_example/public/filter_debugger_embeddable.tsx
+++ b/examples/portable_dashboards_example/public/filter_debugger_embeddable.tsx
@@ -22,6 +22,7 @@ export const factory: EmbeddableFactory<{}, Api> = {
buildEmbeddable: async ({ finalizeApi, parentApi }) => {
const api = finalizeApi({
serializeState: () => ({}),
+ applySerializedState: () => undefined,
});
return {
diff --git a/src/platform/packages/private/kbn-controls-renderer/src/components/control_panel.test.tsx b/src/platform/packages/private/kbn-controls-renderer/src/components/control_panel.test.tsx
index 9eeed1d181a1e..20bfdd61276f4 100644
--- a/src/platform/packages/private/kbn-controls-renderer/src/components/control_panel.test.tsx
+++ b/src/platform/packages/private/kbn-controls-renderer/src/components/control_panel.test.tsx
@@ -55,6 +55,7 @@ const mockOptionsListFactory: EmbeddableFactory<{ type: typeof OPTIONS_LIST_CONT
serializeState: () => ({
type: OPTIONS_LIST_CONTROL,
}),
+ applySerializedState: () => undefined,
});
return {
Component: () => Options list control
,
diff --git a/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.test.tsx b/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.test.tsx
index d70a8297905ae..fe6ca8f34b04a 100644
--- a/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.test.tsx
+++ b/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.test.tsx
@@ -14,7 +14,7 @@ import {
type EmbeddableFactory,
} from '@kbn/embeddable-plugin/public/react_embeddable_system';
import type { Filter } from '@kbn/es-query';
-import type { PublishesUnsavedChanges } from '@kbn/presentation-publishing';
+import type { HasSerializableState } from '@kbn/presentation-publishing';
import { act, render, waitFor } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
@@ -44,13 +44,13 @@ const getTestEmbeddableFactory = () =>
serializeState: () => ({
selection: initialState.selection,
}),
+ applySerializedState: jest.fn(),
});
return {
Component: () => {initialState.selection}
,
api: {
...api,
hasUnsavedChanges$: new BehaviorSubject(false),
- resetUnsavedChanges: jest.fn(),
},
};
},
@@ -87,7 +87,7 @@ describe('control group renderer', () => {
return { component, api: controlGroupApi! as ControlGroupRendererApi };
};
- test('calling `updateInput` forces each child to be reset', async () => {
+ test('calling `updateInput` applies the updated child state', async () => {
const { api } = await mountControlGroupRenderer({
getCreationOptions: jest.fn().mockResolvedValue({
initialState: {
@@ -99,9 +99,9 @@ describe('control group renderer', () => {
},
}),
});
- const resetSpy = jest.spyOn(
- api.children$.getValue().test as PublishesUnsavedChanges,
- 'resetUnsavedChanges'
+ const applySpy = jest.spyOn(
+ api.children$.getValue().test as HasSerializableState,
+ 'applySerializedState'
);
act(() =>
api.updateInput({
@@ -114,7 +114,10 @@ describe('control group renderer', () => {
})
);
- expect(resetSpy).toBeCalledTimes(1);
+ expect(applySpy).toBeCalledWith({
+ type: 'testControl',
+ selection: 'test selection',
+ });
});
test('filter changes are dispatched to control parent API if they are different', async () => {
diff --git a/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.tsx b/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.tsx
index fb2079265160f..8bcb5e53e6003 100644
--- a/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.tsx
+++ b/src/platform/packages/shared/controls/control-group-renderer/src/control_group_renderer.tsx
@@ -22,7 +22,7 @@ import { ControlsRenderer, type ControlsRendererParentApi } from '@kbn/controls-
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
- apiPublishesUnsavedChanges,
+ apiHasSerializableState,
useSearchApi,
type EmbeddableApiContext,
type ViewMode,
@@ -172,17 +172,15 @@ export const ControlGroupRenderer = ({
getInput$: () => input$,
getInput: () => input$.value,
updateInput: (newInput: Partial) => {
- /** Set the last saved state to the new input and then reset each child to this state */
- const newState = lastSavedState$Ref.current.getValue();
+ const newState = { ...lastSavedState$Ref.current.getValue() };
Object.entries(newInput.initialChildControlState ?? {}).forEach(([id, control]) => {
newState[id] = {
...lastSavedState$Ref.current.value[id],
...control,
};
});
- lastSavedState$Ref.current.next(newState);
- asyncForEach(Object.values(parentApi.children$.getValue()), async (child) => {
- if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
+ asyncForEach(Object.entries(parentApi.children$.getValue()), async ([id, child]) => {
+ if (apiHasSerializableState(child)) child.applySerializedState(newState[id]);
});
},
};
diff --git a/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts b/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts
index be68dc3a82094..5107fe18fa9da 100644
--- a/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts
+++ b/src/platform/packages/shared/kbn-esql-utils/src/utils/get_esql_controls.test.ts
@@ -42,6 +42,7 @@ const createControlApi = (
uuid,
type,
serializeState: () => state,
+ applySerializedState: () => undefined,
});
describe('getEsqlControls', () => {
diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.test.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.test.ts
index 0be5d2d3384e0..70eae36ebe305 100644
--- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.test.ts
+++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.test.ts
@@ -15,12 +15,10 @@ describe('childrenUnsavedChanges$', () => {
const child1Api = {
uuid: 'child1',
hasUnsavedChanges$: new BehaviorSubject(false),
- resetUnsavedChanges: () => undefined,
};
const child2Api = {
uuid: 'child2',
hasUnsavedChanges$: new BehaviorSubject(false),
- resetUnsavedChanges: () => undefined,
};
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
const onFireMock = jest.fn();
@@ -118,7 +116,6 @@ describe('childrenUnsavedChanges$', () => {
child3: {
uuid: 'child3',
hasUnsavedChanges$: new BehaviorSubject(true),
- resetUnsavedChanges: () => undefined,
},
});
diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts
index 61c9a367a43e9..c7f65d2f6da10 100644
--- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts
+++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts
@@ -10,6 +10,7 @@
import type { Observable } from 'rxjs';
import type { MaybePromise } from '@kbn/utility-types';
import { combineLatestWith, debounceTime, map, of } from 'rxjs';
+import type { HasSerializableState } from '../../has_serializable_state';
import type { PublishesUnsavedChanges } from '../../publishes_unsaved_changes';
import { type StateComparators, areComparatorsEqual } from '../../../state_manager';
import { getTitle } from '../../titles/publishes_title';
@@ -33,13 +34,17 @@ export const initializeUnsavedChanges = ({
serializeState: () => StateType;
getComparators: () => StateComparators;
defaultState?: Partial;
- onReset: (lastSavedPanelState?: StateType) => MaybePromise;
+ onReset?: (lastSavedPanelState?: StateType) => MaybePromise;
checkRefEquality?: boolean;
-}): PublishesUnsavedChanges => {
+}): PublishesUnsavedChanges & Pick, 'applySerializedState'> => {
+ const applySerializedState = async (state?: StateType) => {
+ await onReset?.(state);
+ };
+
if (!apiHasLastSavedChildState(parentApi)) {
return {
+ applySerializedState,
hasUnsavedChanges$: of(false),
- resetUnsavedChanges: () => Promise.resolve(),
};
}
@@ -67,10 +72,5 @@ export const initializeUnsavedChanges = ({
})
);
- const resetUnsavedChanges = async () => {
- const lastSavedState = parentApi.getLastSavedStateForChild(uuid);
- await onReset(lastSavedState);
- };
-
- return { hasUnsavedChanges$, resetUnsavedChanges };
+ return { applySerializedState, hasUnsavedChanges$ };
};
diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/has_serializable_state.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/has_serializable_state.ts
index e3eeac73e427c..eceeb9273d9d0 100644
--- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/has_serializable_state.ts
+++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/has_serializable_state.ts
@@ -7,14 +7,24 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { MaybePromise } from '@kbn/utility-types';
+
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: () => SerializedState;
+
+ /**
+ * Applies a serialized state snapshot owned by the parent container.
+ */
+ applySerializedState: (state?: SerializedState) => MaybePromise;
}
export const apiHasSerializableState = (api: unknown | null): api is HasSerializableState => {
- return Boolean((api as HasSerializableState)?.serializeState);
+ return Boolean(
+ (api as HasSerializableState)?.serializeState &&
+ (api as HasSerializableState)?.applySerializedState
+ );
};
diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts
index 596872cbe927a..aae093f25c0ff 100644
--- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts
+++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/publishes_unsaved_changes.ts
@@ -7,18 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type { MaybePromise } from '@kbn/utility-types';
import type { Observable } from 'rxjs';
export interface PublishesUnsavedChanges {
hasUnsavedChanges$: Observable; // Observable rather than publishingSubject because it should be derived.
- resetUnsavedChanges: () => MaybePromise;
}
export const apiPublishesUnsavedChanges = (api: unknown): api is PublishesUnsavedChanges => {
- return Boolean(
- api &&
- (api as PublishesUnsavedChanges).hasUnsavedChanges$ &&
- (api as PublishesUnsavedChanges).resetUnsavedChanges
- );
+ return Boolean(api && (api as PublishesUnsavedChanges).hasUnsavedChanges$);
};
diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx
index ae2525c4d536c..aac114c8e9611 100644
--- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx
+++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.tsx
@@ -22,11 +22,7 @@ import {
} from 'rxjs';
import { OPTIONS_LIST_CONTROL, DEFAULT_DSL_OPTIONS_LIST_STATE } from '@kbn/controls-constants';
-import type {
- OptionsListSelection,
- OptionsListControlState,
- OptionsListDSLControlState,
-} from '@kbn/controls-schemas';
+import type { OptionsListSelection, OptionsListControlState } from '@kbn/controls-schemas';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import {
apiHasPinnedPanels,
@@ -239,7 +235,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory<
}
);
- function serializeState(): OptionsListDSLControlState {
+ function serializeState(): OptionsListControlState {
return {
...dataControlManager.getLatestState(),
...selectionsManager.getLatestState(),
@@ -250,7 +246,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory<
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
serializeState,
diff --git a/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.test.tsx
index 49d49f1edacb6..86230ab93c7b5 100644
--- a/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.test.tsx
+++ b/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.test.tsx
@@ -269,7 +269,7 @@ describe('TimeSliderControlApi', () => {
expect(new Date(api.appliedTimeslice$.value![1]).toISOString()).toEqual(
'2024-06-09T18:00:00.000Z'
);
- await api.resetUnsavedChanges();
+ await api.applySerializedState(controlState);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(new Date(api.appliedTimeslice$.value![0]).toISOString()).toEqual(
diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/clone_panel_action.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
index 93b3aa8e59bff..ecfdf7859dd38 100644
--- a/src/platform/plugins/shared/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
+++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
@@ -24,6 +24,7 @@ describe('Clone panel action', () => {
uuid: 'superId',
viewMode$: new BehaviorSubject('edit'),
serializeState: () => ({}),
+ applySerializedState: () => undefined,
parentApi: {
duplicatePanel: jest.fn(),
},
diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts
index 86e465f14abc9..9661f5769df36 100644
--- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts
+++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts
@@ -23,6 +23,7 @@ import type {
import { initializeTitleManager } from '@kbn/presentation-publishing';
import type { DashboardState } from '../../../common';
+import { getSampleDashboardState } from '../../mocks';
import type { initializeTrackPanel } from '../track_panel';
import type { initializeViewModeManager } from '../view_mode_manager';
import { initializeLayoutManager } from './layout_manager';
@@ -84,6 +85,7 @@ describe('layout manager', () => {
phase$: {} as unknown as PublishingSubject,
...titleManager.api,
serializeState: () => titleManager.getLatestState(),
+ applySerializedState: jest.fn(),
};
const section1 = {
@@ -108,6 +110,55 @@ describe('layout manager', () => {
expect(layoutManager.api.children$.getValue()[PANEL_ONE_ID]).toBe(panel1Api);
});
+ test('should apply incoming serialized child state during reset when supported', async () => {
+ const layoutManager = initializeLayoutManager(
+ viewModeManagerMock,
+ undefined,
+ [panel1],
+ [],
+ trackPanelMock
+ );
+ const applySerializedState = jest.fn().mockResolvedValue(undefined);
+
+ layoutManager.api.registerChildApi({
+ ...panel1Api,
+ applySerializedState,
+ hasUnsavedChanges$: new BehaviorSubject(false),
+ } as DefaultEmbeddableApi);
+
+ layoutManager.internalApi.reset(
+ getSampleDashboardState({
+ panels: [{ ...panel1, config: { title: 'Updated title' } }],
+ pinned_panels: [],
+ })
+ );
+
+ expect(applySerializedState).toHaveBeenCalledWith({ title: 'Updated title' });
+ });
+
+ test('should ignore child state application when child does not support it', async () => {
+ const layoutManager = initializeLayoutManager(
+ viewModeManagerMock,
+ undefined,
+ [panel1],
+ [],
+ trackPanelMock
+ );
+ layoutManager.api.registerChildApi({
+ ...panel1Api,
+ hasUnsavedChanges$: new BehaviorSubject(false),
+ } as DefaultEmbeddableApi);
+
+ layoutManager.internalApi.reset(
+ getSampleDashboardState({
+ panels: [{ ...panel1, config: { title: 'Updated title' } }],
+ pinned_panels: [],
+ })
+ );
+
+ expect(layoutManager.api.children$.getValue()[PANEL_ONE_ID]).toBeDefined();
+ });
+
test('should append incoming embeddables to existing panels', () => {
const incomingEmbeddables = [
{
diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts
index 036f53d7cd7bb..79f4cb37112e9 100644
--- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts
+++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts
@@ -37,7 +37,6 @@ import {
apiHasLibraryTransforms,
apiHasSerializableState,
apiPublishesTitle,
- apiPublishesUnsavedChanges,
getTitle,
logStateDiff,
shouldLogStateDiff,
@@ -159,7 +158,10 @@ export function initializeLayoutManager(
for (const uuid of Object.keys(currentChildren)) {
if (layoutToApply.panels[uuid] || layoutToApply.pinnedPanels[uuid]) {
const child = currentChildren[uuid];
- if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
+ const nextChildState = childStateToApply[uuid];
+ if (apiHasSerializableState(child)) {
+ child.applySerializedState(nextChildState);
+ }
} else {
// if reset resulted in panel removal, we need to update the list of children
delete currentChildren[uuid];
diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx
index 3032fa6df0c68..2db894df7838a 100644
--- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx
+++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx
@@ -21,6 +21,7 @@ import { act, render, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
+import type { EmbeddableApiRegistration } from '@kbn/embeddable-plugin/public/react_embeddable_system/types';
import { createDataViewDataSource } from '../../common/data_sources';
import type { SearchEmbeddableState } from '../../common/embeddable/types';
import { discoverServiceMock } from '../__mocks__/services';
@@ -126,9 +127,10 @@ describe('saved search embeddable', () => {
};
const finalizeApiMock = (
- api: Omit
+ api: EmbeddableApiRegistration
) => ({
...api,
+ applySerializedState: () => undefined,
uuid,
type: factory.type,
parentApi: mockedDashboardApi,
@@ -146,9 +148,10 @@ describe('saved search embeddable', () => {
};
const finalizeEditableApiMock = (
- api: Omit
+ api: EmbeddableApiRegistration
) => ({
...api,
+ applySerializedState: () => undefined,
uuid,
type: factory.type,
parentApi: mockedEditableDashboardApi,
diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
index 46073329983f4..580bb4a7d5c97 100644
--- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
+++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
@@ -26,6 +26,7 @@ const testEmbeddableFactory: EmbeddableFactory<{ name: string; bork: string }> =
name: initialState.name,
bork: initialState.bork,
}),
+ applySerializedState: jest.fn(),
});
return {
Component: () => (
@@ -157,6 +158,7 @@ describe('embeddable renderer', () => {
phase$: expect.any(Object),
hasLockedHoverActions$: expect.any(Object),
lockHoverActions: expect.any(Function),
+ applySerializedState: expect.any(Function),
isCustomizable: true,
isDuplicable: true,
isExpandable: true,
@@ -332,6 +334,7 @@ describe('reactEmbeddable phase events', () => {
name: initialState.name,
bork: initialState.bork,
}),
+ applySerializedState: jest.fn(),
dataLoading$,
});
return {
diff --git a/x-pack/platform/plugins/private/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_factory.tsx b/x-pack/platform/plugins/private/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_factory.tsx
index 6f4f0c90f9f6b..7343db8a86b1e 100644
--- a/x-pack/platform/plugins/private/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_factory.tsx
+++ b/x-pack/platform/plugins/private/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_factory.tsx
@@ -199,7 +199,7 @@ export const getFieldStatsChartEmbeddableFactory = (
};
};
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
serializeState,
diff --git a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.test.tsx b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.test.tsx
index d1e25845e9e1c..bbb0cc085cd52 100644
--- a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.test.tsx
+++ b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.test.tsx
@@ -8,7 +8,7 @@
// Write a test that verifies that the `AlertsTableEmbeddable` component renders the `AlertsTable` component with the correct props.
import React from 'react';
-import { render } from '@testing-library/react';
+import { act, render, waitFor } from '@testing-library/react';
import type { EmbeddableAlertsTablePublicStartDependencies } from '../types';
import { coreMock } from '@kbn/core/public/mocks';
import { getMockPresentationContainer } from '@kbn/presentation-publishing/interfaces/containers/mocks';
@@ -67,6 +67,10 @@ describe('getEmbeddableAlertsTableFactory', () => {
parentApi: {} as any,
};
+ beforeEach(() => {
+ mockEmbeddableAlertsTable.mockClear();
+ });
+
it('should render AlertsTable with the correct props', async () => {
const { Component, api } = await factory.buildEmbeddable(embeddableParams);
@@ -92,4 +96,48 @@ describe('getEmbeddableAlertsTableFactory', () => {
expect(api.isEditingEnabled()).toBeFalsy();
});
+
+ it('should restore the saved query after a user edits the panel config and resets changes', async () => {
+ const { Component, api } = await factory.buildEmbeddable(embeddableParams);
+ const updatedQuery = {
+ type: 'alertsFilters' as const,
+ filters: [{ filter: { field: 'kibana.alert.rule.name', value: 'updated' } }],
+ };
+
+ render();
+
+ // simulate the user applying a new query to the panel
+ await act(async () => {
+ await api.applySerializedState({
+ ...embeddableParams.initialState,
+ tableConfig: {
+ solution: 'observability',
+ query: updatedQuery,
+ },
+ });
+ });
+
+ await waitFor(() => {
+ expect(mockEmbeddableAlertsTable).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ query: updatedQuery,
+ }),
+ {}
+ );
+ });
+
+ // simulate the user resetting the changes
+ await act(async () => {
+ await api.applySerializedState(embeddableParams.initialState);
+ });
+
+ await waitFor(() => {
+ expect(mockEmbeddableAlertsTable).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ query: embeddableParams.initialState.tableConfig.query,
+ }),
+ {}
+ );
+ });
+ });
});
diff --git a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.tsx b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.tsx
index 44d31dbea3cd5..390ce1e1a2e70 100644
--- a/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.tsx
+++ b/x-pack/platform/plugins/shared/embeddable_alerts_table/public/factories/alerts_table_embeddable_factory.tsx
@@ -51,13 +51,13 @@ export const getAlertsTableEmbeddableFactory = (
const initialTableConfig = initialState.tableConfig;
const tableConfig$ = new BehaviorSubject(initialTableConfig);
- const serializeState = () => ({
+ const serializeState = (): EmbeddableAlertsTableSerializedState => ({
...titleManager.getLatestState(),
...timeRangeManager.getLatestState(),
tableConfig: tableConfig$.getValue(),
});
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
anyStateChange$: merge(
@@ -74,6 +74,9 @@ export const getAlertsTableEmbeddableFactory = (
onReset: (lastSaved) => {
titleManager.reinitializeState(lastSaved);
timeRangeManager.reinitializeState(lastSaved);
+ if (lastSaved?.tableConfig) {
+ tableConfig$.next(lastSaved.tableConfig);
+ }
},
});
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts
index 8de213dbdfa80..7b8fca9908d2c 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/initializers/initialize_integrations.ts
@@ -31,7 +31,7 @@ export function initializeIntegrations(getLatestState: GetStateType): {
| 'updateDataLoading'
| 'getTriggerCompatibleActions'
> &
- HasSerializableState &
+ Pick, 'serializeState'> &
LegacyLensStateApi;
} {
return {
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx
index 11017aba69cfa..92164fba64194 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/mocks/index.tsx
@@ -109,7 +109,7 @@ function getDefaultLensApiMock() {
rendered$: new BehaviorSubject(false),
searchSessionId$: new BehaviorSubject(undefined),
hasUnsavedChanges$: new BehaviorSubject(false),
- resetUnsavedChanges: jest.fn(),
+ applySerializedState: jest.fn(),
projectRoutingOverrides$: new BehaviorSubject(undefined),
};
return LensApiMock;
diff --git a/x-pack/platform/plugins/shared/ml/public/ui_actions/get_embeddable_time_range.ts b/x-pack/platform/plugins/shared/ml/public/ui_actions/get_embeddable_time_range.ts
index 422ff90d99047..08ab2438ead66 100644
--- a/x-pack/platform/plugins/shared/ml/public/ui_actions/get_embeddable_time_range.ts
+++ b/x-pack/platform/plugins/shared/ml/public/ui_actions/get_embeddable_time_range.ts
@@ -9,7 +9,9 @@ import type { TimeRange } from '@kbn/es-query';
import { apiHasParentApi, apiPublishesTimeRange } from '@kbn/presentation-publishing';
import type { MlEmbeddableBaseApi } from '../embeddables';
-export const getEmbeddableTimeRange = (embeddable: MlEmbeddableBaseApi): TimeRange | undefined => {
+export const getEmbeddableTimeRange = (
+ embeddable: MlEmbeddableBaseApi
+): TimeRange | undefined => {
let timeRange = embeddable.timeRange$?.getValue();
if (!timeRange && apiHasParentApi(embeddable) && apiPublishesTimeRange(embeddable.parentApi)) {
diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_failed_transactions_chart/react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_failed_transactions_chart/react_embeddable_factory.tsx
index 94825b9143035..fb81ecb71793e 100644
--- a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_failed_transactions_chart/react_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_failed_transactions_chart/react_embeddable_factory.tsx
@@ -40,7 +40,7 @@ export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: Emb
const kuery$ = new BehaviorSubject(state.kuery);
const filters$ = new BehaviorSubject(state.filters);
- function serializeState() {
+ function serializeState(): EmbeddableApmAlertingVizProps {
return {
...titleManager.getLatestState(),
serviceName: serviceName$.getValue(),
@@ -56,7 +56,7 @@ export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: Emb
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
parentApi,
uuid,
serializeState,
diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_latency_chart/react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_latency_chart/react_embeddable_factory.tsx
index f28f6a2671c93..f1a4381ad2567 100644
--- a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_latency_chart/react_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_latency_chart/react_embeddable_factory.tsx
@@ -43,7 +43,7 @@ export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps
const kuery$ = new BehaviorSubject(state.kuery);
const filters$ = new BehaviorSubject(state.filters);
- function serializeState() {
+ function serializeState(): EmbeddableApmAlertingLatencyVizProps {
return {
...titleManager.getLatestState(),
serviceName: serviceName$.getValue(),
@@ -60,7 +60,7 @@ export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
parentApi,
uuid,
serializeState,
diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_throughput_chart/react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_throughput_chart/react_embeddable_factory.tsx
index ff28a019fd253..7abdf0eb97934 100644
--- a/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_throughput_chart/react_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/apm/public/embeddable/alerting/alerting_throughput_chart/react_embeddable_factory.tsx
@@ -39,7 +39,7 @@ export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableD
const kuery$ = new BehaviorSubject(state.kuery);
const filters$ = new BehaviorSubject(state.filters);
- function serializeState() {
+ function serializeState(): EmbeddableApmAlertingVizProps {
return {
...titleManager.getLatestState(),
serviceName: serviceName$.getValue(),
@@ -55,7 +55,7 @@ export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableD
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
parentApi,
uuid,
serializeState,
diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx
index fc59e828aaeb8..c279ab887bc58 100644
--- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx
@@ -73,14 +73,14 @@ export function getAlertsEmbeddableFactory({
const defaultTitle$ = new BehaviorSubject(getAlertsPanelTitle());
const reload$ = new Subject();
- function serializeState() {
+ function serializeState(): SloAlertsEmbeddableState {
return {
...titleManager.getLatestState(),
...sloAlertsStateManager.getLatestState(),
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
serializeState,
diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx
index 4a9e502c11737..e4f12b64aa401 100644
--- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/burn_rate/burn_rate_react_embeddable_factory.tsx
@@ -41,7 +41,13 @@ export const getBurnRateEmbeddableFactory = ({
}) => {
const factory: EmbeddableFactory = {
type: SLO_BURN_RATE_EMBEDDABLE_ID,
- buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => {
+ buildEmbeddable: async ({
+ initialState,
+ finalizeApi,
+ uuid,
+ parentApi,
+ initializeDrilldownsManager,
+ }) => {
const deps = { ...coreStart, ...pluginsStart };
const titleManager = initializeTitleManager(initialState);
const defaultTitle$ = new BehaviorSubject(getTitle());
@@ -50,22 +56,29 @@ export const getBurnRateEmbeddableFactory = ({
slo_instance_id: '*',
duration: '',
});
+ const drilldownsManager = await initializeDrilldownsManager(uuid, initialState);
const reload$ = new Subject();
- function serializeState() {
+ function serializeState(): BurnRateEmbeddableState {
return {
...titleManager.getLatestState(),
...sloBurnRateManager.getLatestState(),
+ ...drilldownsManager.getLatestState(),
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
- anyStateChange$: merge(titleManager.anyStateChange$, sloBurnRateManager.anyStateChange$),
+ anyStateChange$: merge(
+ titleManager.anyStateChange$,
+ sloBurnRateManager.anyStateChange$,
+ drilldownsManager.anyStateChange$
+ ),
serializeState,
getComparators: () => ({
...titleComparators,
+ ...drilldownsManager.comparators,
slo_id: 'referenceEquality',
slo_instance_id: 'referenceEquality',
duration: 'referenceEquality',
@@ -73,12 +86,14 @@ export const getBurnRateEmbeddableFactory = ({
onReset: (lastSaved) => {
sloBurnRateManager.reinitializeState(lastSaved);
titleManager.reinitializeState(lastSaved);
+ drilldownsManager.reinitializeState(lastSaved ?? {});
},
});
const api = finalizeApi({
...titleManager.api,
...unsavedChangesApi,
+ ...drilldownsManager.api,
defaultTitle$,
serializeState,
});
@@ -101,6 +116,7 @@ export const getBurnRateEmbeddableFactory = ({
useEffect(() => {
return () => {
fetchSubscription.unsubscribe();
+ drilldownsManager.cleanup();
};
}, []);
diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx
index 0e15c553a2afb..48b74e090eafe 100644
--- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx
+++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/error_budget/error_budget_react_embeddable_factory.tsx
@@ -63,7 +63,7 @@ export const getErrorBudgetEmbeddableFactory = ({
});
const reload$ = new Subject();
- function serializeState() {
+ function serializeState(): ErrorBudgetEmbeddableState {
return {
...titleManager.getLatestState(),
...drilldownsManager.getLatestState(),
@@ -71,7 +71,7 @@ export const getErrorBudgetEmbeddableFactory = ({
};
}
- const unsavedChangesApi = initializeUnsavedChanges({
+ const unsavedChangesApi = initializeUnsavedChanges({
uuid,
parentApi,
serializeState,