diff --git a/examples/embeddable_examples/public/react_embeddables/data_table/data_table_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/data_table/data_table_react_embeddable.tsx index eb45a3db59aaa..e5de96fda2c1c 100644 --- a/examples/embeddable_examples/public/react_embeddables/data_table/data_table_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/data_table/data_table_react_embeddable.tsx @@ -16,7 +16,6 @@ import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { - initializeUnsavedChanges, initializeTimeRangeManager, initializeTitleManager, timeRangeComparators, @@ -38,7 +37,7 @@ export const getDataTableFactory = ( services: StartDeps ): EmbeddableFactory => ({ type: DATA_TABLE_ID, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, initializeStateApi, finalizeApi, parentApi, uuid }) => { const state = initialState; const timeRangeManager = initializeTimeRangeManager(state); const dataLoading$ = new BehaviorSubject(true); @@ -60,9 +59,7 @@ export const getDataTableFactory = ( }; }; - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, + const containerLinkApi = initializeStateApi({ serializeState, anyStateChange$: merge(titleManager.anyStateChange$, timeRangeManager.anyStateChange$), getComparators: () => { @@ -71,18 +68,17 @@ export const getDataTableFactory = ( ...timeRangeComparators, }; }, - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); }, }); const api = finalizeApi({ ...timeRangeManager.api, ...titleManager.api, - ...unsavedChangesApi, + ...containerLinkApi, dataLoading$, - serializeState, }); const queryService = await initializeDataTableQueries(services, api, dataLoading$); 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 b94ac8c751fc3..68f58eec69e8a 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 @@ -19,7 +19,6 @@ import { initializeTitleManager, titleComparators, useBatchedPublishingSubjects, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public'; import { @@ -85,7 +84,7 @@ export const getFieldListFactory = ( ) => { const fieldListEmbeddableFactory: EmbeddableFactory = { type: FIELD_LIST_ID, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const state = await deserializeState(dataViews, initialState); const allDataViews = await dataViews.getIdsWithTitle(); const subscriptions = new Subscription(); @@ -117,19 +116,17 @@ export const getFieldListFactory = ( }; } - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge(titleManager.anyStateChange$, fieldListStateManager.anyStateChange$), getComparators: () => ({ ...titleComparators, - selectedFieldNames: (a, b) => { + selectedFieldNames: (a: string[] | undefined, b: string[] | undefined) => { return (a?.slice().sort().join(',') ?? '') === (b?.slice().sort().join(',') ?? ''); }, dataViewId: 'referenceEquality', }), - onReset: async (lastSaved) => { + serializeState, + applySerializedState: async (lastSaved: FieldListSerializedState | undefined) => { const lastState = await deserializeState(dataViews, lastSaved); fieldListStateManager.reinitializeState(lastState); titleManager.reinitializeState(lastSaved); @@ -138,8 +135,7 @@ export const getFieldListFactory = ( const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, - serializeState, + ...stateApi, }); return { diff --git a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx index 956f3500c378c..d473056a96773 100644 --- a/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/saved_book/saved_book_react_embeddable.tsx @@ -28,7 +28,6 @@ import { initializeStateManager, titleComparators, apiIsPresentationContainer, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import React from 'react'; import { merge } from 'rxjs'; @@ -50,7 +49,7 @@ const bookStateComparators: StateComparators = { export const getSavedBookEmbeddableFactory = (core: CoreStart) => { const savedBookEmbeddableFactory: EmbeddableFactory = { type: BOOK_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const titleManager = initializeTitleManager(initialState); const savedObjectId = (initialState as BookByReferenceState).savedObjectId; const initialBookState = savedObjectId ? await loadBook(savedObjectId) : initialState; @@ -65,12 +64,7 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => { ...(id ? { savedObjectId: id } : bookStateManager.getLatestState()), }); - const serializeState = () => serializeBook(savedObjectId); - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge(titleManager.anyStateChange$, bookStateManager.anyStateChange$), getComparators: () => { return { @@ -79,14 +73,15 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => { savedObjectId: 'skip', // saved book id will not change over the lifetime of the embeddable. }; }, - onReset: async (lastSaved) => { - titleManager.reinitializeState(lastSaved); - if (!savedObjectId) bookStateManager.reinitializeState(lastSaved as BookState); + serializeState: () => serializeBook(savedObjectId), + applySerializedState: async (nextState) => { + titleManager.reinitializeState(nextState); + if (!savedObjectId) bookStateManager.reinitializeState(nextState as BookState); }, }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...titleManager.api, onEdit: async () => { openLazyFlyout({ @@ -122,7 +117,6 @@ export const getSavedBookEmbeddableFactory = (core: CoreStart) => { i18n.translate('embeddableExamples.savedbook.editBook.displayName', { defaultMessage: 'book', }), - serializeState, // library transforms getSavedObjectId: () => savedObjectId, diff --git a/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx index 9070a7e78e5da..214791f4245a3 100644 --- a/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx +++ b/examples/embeddable_examples/public/react_embeddables/search/search_react_embeddable.tsx @@ -17,7 +17,6 @@ import { initializeTimeRangeManager, timeRangeComparators, useBatchedPublishingSubjects, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import React, { useEffect } from 'react'; import { BehaviorSubject, switchMap, tap } from 'rxjs'; @@ -28,7 +27,7 @@ import type { SearchApi, Services, SearchSerializedState } from './types'; export const getSearchEmbeddableFactory = (services: Services) => { const factory: EmbeddableFactory = { type: SEARCH_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const timeRangeManager = initializeTimeRangeManager(initialState); const defaultDataView = await services.dataViews.getDefaultDataView(); const dataViews$ = new BehaviorSubject( @@ -53,10 +52,7 @@ export const getSearchEmbeddableFactory = (services: Services) => { }; } - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: timeRangeManager.anyStateChange$, getComparators: () => { /** @@ -66,13 +62,14 @@ export const getSearchEmbeddableFactory = (services: Services) => { */ return timeRangeComparators; }, - onReset: (lastSaved) => { + serializeState, + applySerializedState: (nextState) => { /** * if this embeddable had a difference between its runtime and serialized state, we could run the 'deserializeState' - * function here before resetting. onReset can be async so to support a potential async deserialize function. + * function here before applying the state. onReset can be async to support a potential async deserialize function. */ - timeRangeManager.reinitializeState(lastSaved); + timeRangeManager.reinitializeState(nextState); }, }); @@ -80,9 +77,8 @@ export const getSearchEmbeddableFactory = (services: Services) => { blockingError$, dataViews$, dataLoading$, - ...unsavedChangesApi, + ...stateApi, ...timeRangeManager.api, - serializeState, }); const count$ = new BehaviorSubject(0); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index bd14d7cb205b3..ced4d74f861e4 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -50,7 +50,7 @@ pageLoadAssetSize: elasticAssistant: 301540 elasticAssistantSharedState: 4881 elasticConsole: 4451 - embeddable: 16634 + embeddable: 19659 embeddableAlertsTable: 6524 enterpriseSearch: 37565 entityStore: 9718 diff --git a/src/platform/packages/shared/presentation/presentation_publishing/index.ts b/src/platform/packages/shared/presentation/presentation_publishing/index.ts index be2ffd667601f..96ca82d2b32c5 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/index.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/index.ts @@ -13,6 +13,7 @@ export { type ComparatorFunction, type StateComparators, type WithAllKeys, + type StateManager, runComparator, areComparatorsEqual, diffComparators, @@ -207,9 +208,7 @@ export { type HasSerializedChildState, } from './interfaces/containers/child_state'; -export { childrenUnsavedChanges$ } from './interfaces/containers/unsaved_changes/children_unsaved_changes'; - -export { initializeUnsavedChanges } from './interfaces/containers/unsaved_changes/initialize_unsaved_changes'; +export { childrenUnsavedChanges$ } from './interfaces/containers/container_state/children_unsaved_changes'; export { apiCanDuplicatePanels, 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/container_state/children_unsaved_changes.test.ts similarity index 100% rename from src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.test.ts rename to src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/container_state/children_unsaved_changes.test.ts diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/container_state/children_unsaved_changes.ts similarity index 100% rename from src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/children_unsaved_changes.ts rename to src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/container_state/children_unsaved_changes.ts diff --git a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/index.ts b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/index.ts index 415cc2b8efcf1..c5dd6c316ac6e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/index.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/index.ts @@ -10,4 +10,4 @@ export { shouldLogStateDiff, logStateDiff } from './state_diff_logger'; export { areComparatorsEqual, diffComparators, runComparator } from './state_comparators'; export { initializeStateManager } from './state_manager'; -export type { ComparatorFunction, StateComparators, WithAllKeys } from './types'; +export type { ComparatorFunction, StateComparators, WithAllKeys, StateManager } from './types'; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/state_manager.ts b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/state_manager.ts index 9fac069655c11..15d00a3cab90f 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/state_manager.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/state_manager.ts @@ -10,7 +10,7 @@ import { camelCase } from 'lodash'; import { BehaviorSubject, map, merge } from 'rxjs'; import { runComparator } from './state_comparators'; -import type { StateComparators, StateManager, WithAllKeys } from './types'; +import type { StateManager, StateManagerInitializer, WithAllKeys } from './types'; type SubjectOf = BehaviorSubject[keyof StateType]>; @@ -29,9 +29,9 @@ type KeyToSubjectMap = { * @param comparators - Optional StateComparators. When provided, subject will only emit when value changes. */ export const initializeStateManager = ( - initialState: Partial, - defaultState: WithAllKeys, - comparators?: StateComparators + initialState: StateManagerInitializer['initialState'], + defaultState: StateManagerInitializer['defaultState'], + comparators?: StateManagerInitializer['comparators'] ): StateManager => { const allState = { ...defaultState, ...initialState }; const allSubjects: Array> = []; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/types.ts b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/types.ts index 45f8cda4fa7c9..445466518793b 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/state_manager/types.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/state_manager/types.ts @@ -13,6 +13,12 @@ import type { SnakeToCamelCase } from '../utils/types'; export type WithAllKeys = { [Key in keyof Required]: T[Key] }; +export interface StateManagerInitializer { + initialState: Partial; + defaultState: WithAllKeys; + comparators?: StateComparators; +} + export type ComparatorFunction = ( last: StateType[KeyType] | undefined, current: StateType[KeyType] | undefined, diff --git a/src/platform/plugins/private/image_embeddable/public/image_embeddable/get_image_embeddable_factory.tsx b/src/platform/plugins/private/image_embeddable/public/image_embeddable/get_image_embeddable_factory.tsx index a38782a52c584..095c8dd063857 100644 --- a/src/platform/plugins/private/image_embeddable/public/image_embeddable/get_image_embeddable_factory.tsx +++ b/src/platform/plugins/private/image_embeddable/public/image_embeddable/get_image_embeddable_factory.tsx @@ -13,11 +13,7 @@ import { BehaviorSubject, map, merge } from 'rxjs'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { openLazyFlyout } from '@kbn/presentation-util'; -import { - initializeUnsavedChanges, - initializeTitleManager, - titleComparators, -} from '@kbn/presentation-publishing'; +import { initializeTitleManager, titleComparators } from '@kbn/presentation-publishing'; import type { ImageEmbeddableState } from '../../server'; import { ImageEmbeddable as ImageEmbeddableComponent } from '../components/image_embeddable'; @@ -31,10 +27,11 @@ export const getImageEmbeddableFactory = () => { type: IMAGE_EMBEDDABLE_TYPE, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, - uuid, parentApi, + uuid, }) => { const titleManager = initializeTitleManager(initialState); @@ -44,18 +41,7 @@ export const getImageEmbeddableFactory = () => { const imageConfig$ = new BehaviorSubject(initialState.image_config); const dataLoading$ = new BehaviorSubject(true); - function serializeState() { - return { - ...titleManager.getLatestState(), - ...drilldownsManager.getLatestState(), - image_config: imageConfig$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, imageConfig$.pipe(map(() => undefined)), @@ -68,17 +54,22 @@ export const getImageEmbeddableFactory = () => { image_config: 'deepEquality', }; }, - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - drilldownsManager.reinitializeState(lastSaved ?? {}); - if (lastSaved) imageConfig$.next(lastSaved.image_config); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...drilldownsManager.getLatestState(), + image_config: imageConfig$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + drilldownsManager.reinitializeState(nextState ?? {}); + if (nextState) imageConfig$.next(nextState.image_config); }, }); const embeddable = finalizeApi({ ...titleManager.api, ...drilldownsManager.api, - ...unsavedChangesApi, + ...stateApi, dataLoading$, supportedTriggers: () => IMAGE_EMBEDDABLE_SUPPORTED_TRIGGERS, @@ -106,7 +97,6 @@ export const getImageEmbeddableFactory = () => { i18n.translate('imageEmbeddable.imageEmbeddableFactory.displayName.edit', { defaultMessage: 'image', }), - serializeState, }); return { api: embeddable, diff --git a/src/platform/plugins/private/links/public/embeddable/links_embeddable.test.tsx b/src/platform/plugins/private/links/public/embeddable/links_embeddable.test.tsx index c0519866ef3f4..fdb04cf71a434 100644 --- a/src/platform/plugins/private/links/public/embeddable/links_embeddable.test.tsx +++ b/src/platform/plugins/private/links/public/embeddable/links_embeddable.test.tsx @@ -17,6 +17,7 @@ import type { Link } from '../../server'; import type { LinksApi, ResolvedLink } from '../types'; import { linksClient } from '../content_management'; import { getMockLinksParentApi } from '../mocks'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const getLinks = (): Link[] => [ { @@ -116,6 +117,7 @@ async function buildLinksEmbeddable(state: LinksEmbeddableState) { type: LINKS_EMBEDDABLE_TYPE, } as LinksApi; }, + initializeStateApi: getMockinitializeStateApi(factory), parentApi, uuid, }); diff --git a/src/platform/plugins/private/links/public/embeddable/links_embeddable.tsx b/src/platform/plugins/private/links/public/embeddable/links_embeddable.tsx index 1c1b04ce060fc..c62797250b0be 100644 --- a/src/platform/plugins/private/links/public/embeddable/links_embeddable.tsx +++ b/src/platform/plugins/private/links/public/embeddable/links_embeddable.tsx @@ -22,7 +22,6 @@ import { useBatchedPublishingSubjects, titleComparators, apiIsPresentationContainer, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import { css } from '@emotion/react'; import { openLazyFlyout } from '@kbn/presentation-util'; @@ -49,7 +48,7 @@ export const LinksContext = createContext(null); export const getLinksEmbeddableFactory = () => { const linksEmbeddableFactory: EmbeddableFactory = { type: LINKS_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi, parentApi }) => { const savedObjectId = (initialState as LinksByReferenceState).savedObjectId; const intialLinksState = savedObjectId ? await loadFromLibrary(savedObjectId) @@ -86,13 +85,7 @@ export const getLinksEmbeddableFactory = () => { }; } - const serializeState = () => - isByReference ? serializeByReference(savedObjectId) : serializeByValue(); - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, layout$.pipe(map(() => undefined)), @@ -121,24 +114,25 @@ export const getLinksEmbeddableFactory = () => { savedObjectId: 'skip', }; }, - onReset: async (lastSaved) => { - titleManager.reinitializeState(lastSaved); + serializeState: () => + isByReference ? serializeByReference(savedObjectId) : serializeByValue(), + applySerializedState: async (nextState) => { + titleManager.reinitializeState(nextState); if (!savedObjectId) { - layout$.next((lastSaved as LinksByValueState)?.layout); - resolvedLinks$.next(await resolveLinks((lastSaved as LinksByValueState)?.links ?? [])); + layout$.next((nextState as LinksByValueState)?.layout); + resolvedLinks$.next(await resolveLinks((nextState as LinksByValueState)?.links ?? [])); } }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, blockingError$, defaultTitle$, defaultDescription$, isEditingEnabled: () => Boolean(blockingError$.value === undefined), getTypeDisplayName: () => DISPLAY_NAME, - serializeState, saveToLibrary: async (newTitle: string) => { defaultTitle$.next(newTitle); const { diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx index b34102d04661d..1794c6ed02749 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { coreServices, dataViewsService } from '../../../services/kibana_services'; import { getMockedFinalizeApi } from '../../mocks/control_mocks'; import { getOptionsListControlFactory } from './get_options_list_control_factory'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const render = (ui: React.ReactElement) => { return rtlRender(ui, { wrapper: EuiThemeProvider }); @@ -27,6 +28,8 @@ describe('Options List Control Api', () => { const factory = getOptionsListControlFactory(); const finalizeApi = getMockedFinalizeApi(uuid, factory); + const mockinitializeStateApi = getMockinitializeStateApi(factory); + const getDataView = async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); @@ -81,6 +84,7 @@ describe('Options List Control Api', () => { data_view_id: 'myDataViewId', field_name: 'myFieldName', }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -106,6 +110,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', selected_options: ['cool', 'test'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -138,6 +143,7 @@ describe('Options List Control Api', () => { data_view_id: 'myDataViewId', field_name: 'myFieldName', }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -154,6 +160,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', selected_options: ['cool', 'test'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -197,6 +204,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', exists_selected: true, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -227,6 +235,7 @@ describe('Options List Control Api', () => { exists_selected: true, exclude: true, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -272,6 +281,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', exists_selected: true, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -303,6 +313,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', exists_selected: true, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -334,6 +345,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', selected_options: ['woof', 'bark'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -370,6 +382,7 @@ describe('Options List Control Api', () => { field_name: 'myFieldName', selected_options: ['woof', 'bark'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, @@ -416,6 +429,7 @@ describe('Options List Control Api', () => { single_select: true, selected_options: ['woof'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: {}, 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 7db4ad980d934..df6982a30e5cf 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 @@ -27,7 +27,6 @@ import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { apiHasPinnedPanels, apiHasSections, - initializeUnsavedChanges, type PublishingSubject, } from '@kbn/presentation-publishing'; @@ -66,7 +65,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< > => { return { type: OPTIONS_LIST_CONTROL, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const state = initialState; const editorStateManager = initializeEditorStateManager(state); @@ -146,7 +145,6 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< loadMoreSubject, loadingSuggestions$, debouncedSearchString, - parentApi, uuid, }, requestSize$: temporaryStateManager.api.requestSize$, @@ -232,21 +230,7 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< } ); - function serializeState(): OptionsListDSLControlState { - return { - ...dataControlManager.getLatestState(), - ...selectionsManager.getLatestState(), - ...editorStateManager.getLatestState(), - - // serialize state that cannot be changed to keep it consistent - display_settings: state.display_settings, - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( dataControlManager.anyStateChange$, selectionsManager.anyStateChange$, @@ -267,10 +251,18 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< exclude: false, exists_selected: false, }, - onReset: (lastSaved) => { - dataControlManager.reinitializeState(lastSaved); - selectionsManager.reinitializeState(lastSaved); - editorStateManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...dataControlManager.getLatestState(), + ...selectionsManager.getLatestState(), + ...editorStateManager.getLatestState(), + + // serialize state that cannot be changed to keep it consistent + display_settings: state.display_settings, + }), + applySerializedState: (nextState) => { + dataControlManager.reinitializeState(nextState); + selectionsManager.reinitializeState(nextState); + editorStateManager.reinitializeState(nextState); }, }); @@ -287,12 +279,11 @@ export const getOptionsListControlFactory = (): EmbeddableFactory< .subscribe((error) => blockingError$.next(error)); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...dataControlManager.api, blockingError$, dataLoading$: temporaryStateManager.api.dataLoading$, getTypeDisplayName: OptionsListStrings.control.getDisplayName, - serializeState, clearSelections: () => clearSelections({ selectionsManager, temporaryStateManager }), hasSelections$: hasSelections$ as PublishingSubject, setSelectedOptions: selectionsManager.api.setSelectedOptions, diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx index cfda27f023c0c..f08b2a52f9929 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx @@ -21,6 +21,7 @@ import { getMockedFinalizeApi } from '../../mocks/control_mocks'; import { getRangesliderControlFactory } from './get_range_slider_control_factory'; import type { RangeSliderControlState } from '@kbn/controls-schemas'; import type { Filter, AggregateQuery, TimeRange } from '@kbn/es-query'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const DEFAULT_TOTAL_RESULTS = 20; const DEFAULT_MIN = 0; @@ -40,6 +41,8 @@ describe('RangeSliderControlApi', () => { const factory = getRangesliderControlFactory(); const finalizeApi = getMockedFinalizeApi(uuid, factory, parentApi); + const mockinitializeStateApi = getMockinitializeStateApi(factory); + let totalResults = DEFAULT_TOTAL_RESULTS; let min: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MIN; let max: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MAX; @@ -104,6 +107,7 @@ describe('RangeSliderControlApi', () => { data_view_id: 'myDataViewId', field_name: 'myFieldName', }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -120,6 +124,7 @@ describe('RangeSliderControlApi', () => { field_name: 'myFieldName', value: ['5', '10'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -160,6 +165,7 @@ describe('RangeSliderControlApi', () => { field_name: 'myFieldName', value: ['5', '10'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -184,6 +190,7 @@ describe('RangeSliderControlApi', () => { field_name: 'myFieldName', value: ['5', '10'], }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -204,6 +211,7 @@ describe('RangeSliderControlApi', () => { data_view_id: 'myDataViewId', field_name: 'myFieldName', }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -227,6 +235,7 @@ describe('RangeSliderControlApi', () => { data_view_id: 'myDataViewId', field_name: 'myFieldName', }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, @@ -244,6 +253,7 @@ describe('RangeSliderControlApi', () => { field_name: 'myFieldName', step: 1024, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi, diff --git a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.tsx index 4d13f2745f06c..37371f2b20d34 100644 --- a/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -15,7 +15,6 @@ import { apiHasSections, apiPublishesViewMode, fetch$, - initializeUnsavedChanges, useBatchedPublishingSubjects, } from '@kbn/presentation-publishing'; import { DEFAULT_RANGE_SLIDER_STATE, RANGE_SLIDER_CONTROL } from '@kbn/controls-constants'; @@ -42,7 +41,7 @@ export const getRangesliderControlFactory = (): EmbeddableFactory< > => { return { type: RANGE_SLIDER_CONTROL, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const state = initialState; const loadingMinMax$ = new BehaviorSubject(false); const loadingHasNoResults$ = new BehaviorSubject(false); @@ -68,18 +67,7 @@ export const getRangesliderControlFactory = (): EmbeddableFactory< dataControlManager.internalApi.onSelectionChange ); - function serializeState() { - return { - ...dataControlManager.getLatestState(), - ...editorStateManager.getLatestState(), - value: selections.value$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( dataControlManager.anyStateChange$, selections.value$, @@ -92,18 +80,22 @@ export const getRangesliderControlFactory = (): EmbeddableFactory< value: 'deepEquality', }; }, - onReset: (lastSaved) => { - dataControlManager.reinitializeState(lastSaved); - editorStateManager.reinitializeState(lastSaved); - selections.setValue(lastSaved?.value); + serializeState: () => ({ + ...dataControlManager.getLatestState(), + ...editorStateManager.getLatestState(), + value: selections.value$.getValue(), + }), + applySerializedState: (nextState) => { + dataControlManager.reinitializeState(nextState); + editorStateManager.reinitializeState(nextState); + selections.setValue(nextState?.value); }, }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...dataControlManager.api, dataLoading$, - serializeState, clearSelections: () => { selections.setValue(undefined); }, @@ -134,7 +126,7 @@ export const getRangesliderControlFactory = (): EmbeddableFactory< selections.setValue(undefined); }); - const controlFetch$ = fetch$({ uuid, parentApi }); + const controlFetch$ = fetch$({ parentApi }); const max$ = new BehaviorSubject(undefined); const min$ = new BehaviorSubject(undefined); const minMaxSubscription = minMax$({ diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx index 3bfdcbf2c3c63..2abc9decdc10a 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.test.tsx @@ -15,6 +15,7 @@ import { DEFAULT_ESQL_OPTIONS_LIST_STATE } from '@kbn/controls-constants'; import { getMockedFinalizeApi } from '../mocks/control_mocks'; import { getESQLControlFactory } from './get_esql_control_factory'; import { BehaviorSubject } from 'rxjs'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const mockGetESQLSingleColumnValues = jest.fn(() => ({ options: ['option1', 'option2'] })); const mockIsSuccess = jest.fn(() => true); @@ -44,6 +45,8 @@ describe('ESQLControlApi', () => { const factory = getESQLControlFactory(); const finalizeApi = getMockedFinalizeApi(uuid, factory, dashboardApi); + const mockinitializeStateApi = getMockinitializeStateApi(factory); + test('should publish ES|QL variable', async () => { const initialState: OptionsListESQLControlState = { ...DEFAULT_ESQL_OPTIONS_LIST_STATE, @@ -56,6 +59,7 @@ describe('ESQLControlApi', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -82,6 +86,7 @@ describe('ESQLControlApi', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -110,6 +115,7 @@ describe('ESQLControlApi', () => { await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -133,6 +139,7 @@ describe('ESQLControlApi', () => { await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -172,6 +179,7 @@ describe('ESQLControlApi', () => { const { Component, api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, diff --git a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx index 76c5a86a2d6d7..eb5eee6415ee3 100644 --- a/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/esql_control/get_esql_control_factory.tsx @@ -10,7 +10,6 @@ import { pick } from 'lodash'; import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; - import { ESQL_CONTROL } from '@kbn/controls-constants'; import type { OptionsListESQLControlState } from '@kbn/controls-schemas'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; @@ -20,12 +19,7 @@ import { type QueryESQLControl, type StaticESQLControl, } from '@kbn/esql-types'; -import { - apiHasPinnedPanels, - initializeUnsavedChanges, - type StateComparators, -} from '@kbn/presentation-publishing'; - +import { apiHasPinnedPanels, type StateComparators } from '@kbn/presentation-publishing'; import { uiActionsService } from '../../services/kibana_services'; import { defaultControlLabelComparators, initializeLabelManager } from '../control_labels'; import { OptionsListControl } from '../data_controls/options_list_control/components/options_list_control'; @@ -46,7 +40,7 @@ export const getESQLControlFactory = < > => { return { type: ESQL_CONTROL, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ uuid, parentApi, finalizeApi, initialState, initializeStateApi }) => { const state = initialState; const dataLoading$ = new BehaviorSubject(false); @@ -58,18 +52,7 @@ export const getESQLControlFactory = < selections.internalApi, 'variableName' ); - - function serializeState() { - return { - ...selections.getLatestState(), - ...labelManager.getLatestState(), - } as typeof initialState; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: selections.anyStateChange$, getComparators: () => { return { @@ -78,17 +61,22 @@ export const getESQLControlFactory = < display_settings: 'skip', } as StateComparators; }, - onReset: (lastSaved) => { + serializeState: () => + ({ + ...selections.getLatestState(), + ...labelManager.getLatestState(), + } as typeof initialState), + applySerializedState: (nextState) => { selections.reinitializeState({ available_options: [], - ...lastSaved, + ...nextState, } as ESQLOptionsListRuntimeState); - labelManager.reinitializeState(lastSaved); + labelManager.reinitializeState(nextState); }, }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...selections.api, ...labelManager.api, dataLoading$, @@ -133,8 +121,7 @@ export const getESQLControlFactory = < console.error('Error getting ESQL control trigger', e); } }, - serializeState, - }) as ESQLControlApi; + }); const componentApi: ESQLOptionsListComponentApi = { ...pick(api, ['dataLoading$', 'label$', 'type']), 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 86230ab93c7b5..fbfd5b119ac82 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 @@ -21,6 +21,7 @@ import { dataService } from '../../services/kibana_services'; import { getMockedFinalizeApi } from '../mocks/control_mocks'; import { getTimesliderControlFactory } from './get_timeslider_control_factory'; import type { TimeSliderControlApi } from './types'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const render = (ui: React.ReactElement) => { return rtlRender(ui, { wrapper: EuiThemeProvider }); @@ -41,6 +42,8 @@ describe('TimeSliderControlApi', () => { dashboardApi ); + const mockinitializeStateApi = getMockinitializeStateApi(factory); + dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { @@ -60,6 +63,7 @@ describe('TimeSliderControlApi', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState: DEFAULT_TIME_SLIDER_STATE, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -81,6 +85,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -103,6 +108,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -133,6 +139,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -161,6 +168,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -190,6 +198,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -218,6 +227,7 @@ describe('TimeSliderControlApi', () => { start_percentage_of_time_range: 0.25, end_percentage_of_time_range: 0.5, }, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, @@ -250,6 +260,7 @@ describe('TimeSliderControlApi', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState: controlState, + initializeStateApi: mockinitializeStateApi, finalizeApi, uuid, parentApi: dashboardApi, diff --git a/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx index 345cbbd1c36f8..d39eabb66c0cb 100644 --- a/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/platform/plugins/shared/controls/public/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -18,7 +18,6 @@ import { getViewModeSubject, useBatchedPublishingSubjects, apiPublishesSettings, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import { DEFAULT_TIME_SLIDER_STATE, TIME_SLIDER_CONTROL } from '@kbn/controls-constants'; @@ -53,7 +52,7 @@ export const getTimesliderControlFactory = (): EmbeddableFactory< > => { return { type: TIME_SLIDER_CONTROL, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const state = initialState; const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } = @@ -223,17 +222,7 @@ export const getTimesliderControlFactory = (): EmbeddableFactory< }) ); - function serializeState() { - return { - ...timeRangePercentage.getLatestState(), - is_anchored: isAnchored$.value, - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( timeRangePercentage.anyStateChange$, isAnchored$.pipe(map(() => undefined)) @@ -245,18 +234,21 @@ export const getTimesliderControlFactory = (): EmbeddableFactory< is_anchored: 'referenceEquality', }; }, - onReset: (lastSaved) => { - timeRangePercentage.reinitializeState(lastSaved); - setIsAnchored(lastSaved?.is_anchored ?? DEFAULT_TIME_SLIDER_STATE.is_anchored); + serializeState: () => ({ + ...timeRangePercentage.getLatestState(), + is_anchored: isAnchored$.value, + }), + applySerializedState: (nextState) => { + timeRangePercentage.reinitializeState(nextState); + setIsAnchored(nextState?.is_anchored ?? DEFAULT_TIME_SLIDER_STATE.is_anchored); }, }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, isPinnable: false, // Disable the user-facing unpin action; panel can still be pinned programatically when it's created label$: new BehaviorSubject(displayName), appliedTimeslice$: timeslice$, - serializeState, clearSelections: () => { setTimeslice(undefined); hasTimeSliceSelection$.next(false); diff --git a/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.test.tsx b/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.test.tsx index bc01e9cdc3afc..27fdc9d1e67e5 100644 --- a/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.test.tsx +++ b/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.test.tsx @@ -15,6 +15,7 @@ import { BehaviorSubject } from 'rxjs'; import type { ViewMode } from '@kbn/presentation-publishing'; import { markdownEmbeddableFactory } from './markdown_embeddable'; import type { MarkdownEditorApi } from './types'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; jest.mock('./markdown_client/markdown_client', () => { return { @@ -51,12 +52,15 @@ const renderEmbeddable = async ( const factory = markdownEmbeddableFactory; + const mockinitializeStateApi = getMockinitializeStateApi(factory); + const embeddable = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), initialState: { content: '[click here](https://example.com)', }, parentApi: parentApiStub, + initializeStateApi: mockinitializeStateApi, finalizeApi: (api) => ({ ...api, diff --git a/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.tsx b/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.tsx index 6a8af08ac17a8..1c1a312b2bb98 100644 --- a/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.tsx +++ b/src/platform/plugins/shared/dashboard_markdown/public/markdown_embeddable.tsx @@ -14,7 +14,6 @@ import { apiCanAddNewPanel, apiCanFocusPanel, apiIsPresentationContainer, - initializeUnsavedChanges, getViewModeSubject, initializeTitleManager, titleComparators, @@ -50,7 +49,7 @@ export const markdownEmbeddableFactory: EmbeddableFactory< MarkdownEditorApi > = { type: MARKDOWN_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi, parentApi }) => { const libraryId = (initialState as MarkdownByReferenceState).ref_id; const isByReference = libraryId !== undefined; const initialLibraryState = isByReference @@ -98,9 +97,6 @@ export const markdownEmbeddableFactory: EmbeddableFactory< }; }; - const serializeState = () => - isByReference ? serializeByReference(libraryId) : serializeByValue(); - const resetEditingState = () => { isEditing$.next(false); overrideHoverActions$.next(false); @@ -110,10 +106,7 @@ export const markdownEmbeddableFactory: EmbeddableFactory< } }; - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, content$.pipe(map(() => undefined)), @@ -127,14 +120,15 @@ export const markdownEmbeddableFactory: EmbeddableFactory< ref_id: 'skip', }; }, - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); + serializeState: () => (isByReference ? serializeByReference(libraryId) : serializeByValue()), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); // There are no unsaved changes to reset for // by reference 'content' or 'settings' since they are saved on apply. if (!isByReference) { - content$.next((initialState as MarkdownByValueState).content); + content$.next((nextState as MarkdownByValueState).content); settings$.next( - (initialState as MarkdownByValueState).settings ?? { + (nextState as MarkdownByValueState).settings ?? { open_links_in_new_tab: true, } ); @@ -143,11 +137,10 @@ export const markdownEmbeddableFactory: EmbeddableFactory< }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...titleManager.api, defaultTitle$, defaultDescription$, - serializeState, onEdit: async ({ isNewPanel = false } = {}) => { if (!apiCanAddNewPanel(parentApi)) throw new IncompatibleActionError(); isEditing$.next(true); 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 d25235747ded2..84ed7f9266d45 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 @@ -32,7 +32,10 @@ import type { SearchEmbeddableRuntimeState, } from './types'; import { SolutionType } from '../context_awareness'; -import { mockInitializeDrilldownsManager } from '@kbn/embeddable-plugin/public/mocks'; +import { + mockInitializeDrilldownsManager, + getMockinitializeStateApi, +} from '@kbn/embeddable-plugin/public/mocks'; import { renderWithI18n } from '@kbn/test-jest-helpers'; jest.mock('./utils/serialization_utils', () => ({})); @@ -163,6 +166,8 @@ describe('saved search embeddable', () => { phase$: new BehaviorSubject(undefined), }); + const mockinitializeStateApi = getMockinitializeStateApi(factory); + const waitOneTick = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); describe('search embeddable component', () => { @@ -171,6 +176,7 @@ describe('saved search embeddable', () => { runtimeState = getInitialRuntimeState({ searchMock: search }); const { Component, api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, + initializeStateApi: mockinitializeStateApi, initialState: { ref_id: 'id', overrides: {} }, finalizeApi: finalizeApiMock, uuid, @@ -207,6 +213,7 @@ describe('saved search embeddable', () => { const { Component, api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -248,6 +255,7 @@ describe('saved search embeddable', () => { const { Component } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { savedObjectId: 'id' }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -268,6 +276,7 @@ describe('saved search embeddable', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { savedObjectId: 'id' }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -289,6 +298,7 @@ describe('saved search embeddable', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -326,6 +336,7 @@ describe('saved search embeddable', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: byValueInitialState, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeEditableApiMock, uuid, parentApi: mockedEditableDashboardApi, @@ -359,6 +370,7 @@ describe('saved search embeddable', () => { const { api, Component } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { savedObjectId: 'id' }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeEditableApiMock, uuid, parentApi: mockedEditableDashboardApi, @@ -412,6 +424,7 @@ describe('saved search embeddable', () => { const { Component } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { savedObjectId: 'id' }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -447,6 +460,7 @@ describe('saved search embeddable', () => { await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -472,6 +486,7 @@ describe('saved search embeddable', () => { await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -499,6 +514,7 @@ describe('saved search embeddable', () => { const { api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, @@ -532,6 +548,7 @@ describe('saved search embeddable', () => { const { Component, api } = await factory.buildEmbeddable({ initializeDrilldownsManager: mockInitializeDrilldownsManager, initialState: { ref_id: 'id', overrides: {} }, + initializeStateApi: mockinitializeStateApi, finalizeApi: finalizeApiMock, uuid, parentApi: mockedDashboardApi, diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx index 73517a95e54ff..1a8a628a30699 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -23,7 +23,6 @@ import { timeRangeComparators, titleComparators, useBatchedPublishingSubjects, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { SearchResponseIncompleteWarning } from '@kbn/search-response-warnings/src/types'; @@ -69,6 +68,7 @@ export const getSearchEmbeddableFactory = ({ type: SEARCH_EMBEDDABLE_TYPE, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, parentApi, @@ -144,19 +144,15 @@ export const getSearchEmbeddableFactory = ({ const inlineEditingApi = initializeInlineEditingApi({ uuid, - parentApi, tabs, + parentApi, + dataLoading$, selectedTabId$, - searchEmbeddable, blockingError$, - dataLoading$, + searchEmbeddable, }); - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - defaultState, - serializeState: () => serialize(savedObjectId$.getValue()), + const stateApi = initializeStateApi({ anyStateChange$: merge( drilldownsManager.anyStateChange$, searchEmbeddable.anyStateChange$, @@ -183,13 +179,15 @@ export const getSearchEmbeddableFactory = ({ nonPersistedDisplayOptions: 'skip', }; }, - onReset: async (lastSaved) => { - drilldownsManager.reinitializeState(lastSaved ?? {}); - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) { + defaultState, + serializeState: () => serialize(savedObjectId$.getValue()), + applySerializedState: async (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) { const lastSavedRuntimeState = await deserializeState({ - serializedState: lastSaved, + serializedState: nextState, discoverServices, }); @@ -213,7 +211,7 @@ export const getSearchEmbeddableFactory = ({ }); const api: SearchEmbeddableApi = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...titleManager.api, ...searchEmbeddable.api, ...timeRangeManager.api, @@ -272,7 +270,6 @@ export const getSearchEmbeddableFactory = ({ }), getSerializedStateByValue: () => serialize(undefined), getSerializedStateByReference: (newId: string) => serialize(newId), - serializeState: () => serialize(savedObjectId$.getValue()), getInspectorAdapters: () => searchEmbeddable.stateManager.inspectorAdapters.getValue(), supportedTriggers: () => { return [ON_OPEN_PANEL_MENU]; diff --git a/src/platform/plugins/shared/embeddable/public/mocks.tsx b/src/platform/plugins/shared/embeddable/public/mocks.tsx index 6cb91d08c1fcd..38ca92172b4ec 100644 --- a/src/platform/plugins/shared/embeddable/public/mocks.tsx +++ b/src/platform/plugins/shared/embeddable/public/mocks.tsx @@ -20,7 +20,7 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { BehaviorSubject, of } from 'rxjs'; -import type { EmbeddableStateTransfer } from '.'; +import type { DefaultEmbeddableApi, EmbeddableFactory, EmbeddableStateTransfer } from '.'; import { setKibanaServices } from './kibana_services'; import { EmbeddablePublicPlugin } from './plugin'; import { registerReactEmbeddableFactory } from './react_embeddable_system'; @@ -146,3 +146,23 @@ export async function mockInitializeDrilldownsManager( ): Promise { return mockDrilldownsManager(); } + +export const getMockinitializeStateApi = < + SerializedState extends {} = {}, + ApiType extends DefaultEmbeddableApi = DefaultEmbeddableApi +>( + factory: EmbeddableFactory +): jest.Mocked< + Parameters< + EmbeddableFactory['buildEmbeddable'] + >[0]['initializeStateApi'] +> => { + const mockinitializeStateApi: jest.Mocked< + Parameters[0]['initializeStateApi'] + > = ({ serializeState, applySerializedState }) => ({ + serializeState, + applySerializedState, + hasUnsavedChanges$: of(false), + }); + return mockinitializeStateApi; +}; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/initialize_state_api.ts similarity index 60% rename from src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts rename to src/platform/plugins/shared/embeddable/public/react_embeddable_system/initialize_state_api.ts index c7f65d2f6da10..a60d013a4a777 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/containers/unsaved_changes/initialize_unsaved_changes.ts +++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/initialize_state_api.ts @@ -7,42 +7,45 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Observable } from 'rxjs'; -import type { MaybePromise } from '@kbn/utility-types'; +import { + apiHasLastSavedChildState, + areComparatorsEqual, + getTitle, + type HasParentApi, + type HasSerializableState, + type HasUniqueId, + type PresentationContainer, + type PublishesUnsavedChanges, + type StateComparators, + type StateManager, +} from '@kbn/presentation-publishing'; 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'; -import { apiHasLastSavedChildState } from '../last_saved_child_state'; -import type { PresentationContainer } from '../presentation_container'; + const UNSAVED_CHANGES_DEBOUNCE = 100; -export const initializeUnsavedChanges = ({ +export interface ContainerStateManagerInitializer + extends HasSerializableState { + defaultState?: Partial; + anyStateChange$: StateManager['anyStateChange$']; + getComparators: () => StateComparators; +} + +export const initializeStateApi = ({ uuid, - onReset, parentApi, - getComparators, defaultState, - serializeState, anyStateChange$, - checkRefEquality, -}: { - uuid: string; - parentApi: unknown; - anyStateChange$: Observable; - serializeState: () => StateType; - getComparators: () => StateComparators; - defaultState?: Partial; - onReset?: (lastSavedPanelState?: StateType) => MaybePromise; - checkRefEquality?: boolean; -}): PublishesUnsavedChanges & Pick, 'applySerializedState'> => { - const applySerializedState = async (state?: StateType) => { - await onReset?.(state); - }; - + serializeState, + getComparators, + applySerializedState, +}: HasSerializableState & + HasUniqueId & + HasParentApi & + ContainerStateManagerInitializer): PublishesUnsavedChanges & + HasSerializableState => { if (!apiHasLastSavedChildState(parentApi)) { return { + serializeState, applySerializedState, hasUnsavedChanges$: of(false), }; @@ -72,5 +75,9 @@ export const initializeUnsavedChanges = ({ }) ); - return { applySerializedState, hasUnsavedChanges$ }; + return { + serializeState, + hasUnsavedChanges$, + applySerializedState, + }; }; 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 580bb4a7d5c97..58da16719aeea 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 @@ -64,6 +64,7 @@ describe('embeddable renderer', () => { await waitFor(() => { expect(buildEmbeddableSpy).toHaveBeenCalledWith({ initializeDrilldownsManager: expect.any(Function), + initializeStateApi: expect.any(Function), initialState: { bork: 'blorp?' }, parentApi: expect.any(Object), uuid: expect.any(String), @@ -88,6 +89,7 @@ describe('embeddable renderer', () => { await waitFor(() => { expect(buildEmbeddableSpy).toHaveBeenCalledWith({ initializeDrilldownsManager: expect.any(Function), + initializeStateApi: expect.any(Function), initialState: { bork: 'blorp?' }, parentApi: expect.any(Object), uuid: '12345', @@ -108,6 +110,7 @@ describe('embeddable renderer', () => { await waitFor(() => { expect(buildEmbeddableSpy).toHaveBeenCalledWith({ initializeDrilldownsManager: expect.any(Function), + initializeStateApi: expect.any(Function), initialState: { bork: 'blorp?' }, parentApi, uuid: expect.any(String), diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index 10c85acdf00d5..b6991e8801694 100644 --- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -7,19 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; +import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; +import type { HasPanelCapabilities, HasSerializedChildState } from '@kbn/presentation-publishing'; +import { apiIsPresentationContainer } from '@kbn/presentation-publishing'; import React, { useImperativeHandle, useMemo, useRef } from 'react'; import { BehaviorSubject } from 'rxjs'; import { v4 as generateId } from 'uuid'; - -import type { HasPanelCapabilities, HasSerializedChildState } from '@kbn/presentation-publishing'; -import { apiIsPresentationContainer } from '@kbn/presentation-publishing'; -import type { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; -import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; - +import type { SerializedDrilldowns } from '../../server'; import { PhaseTracker } from './phase_tracker'; import { getReactEmbeddableFactory } from './react_embeddable_registry'; import type { DefaultEmbeddableApi, EmbeddableApiRegistration } from './types'; -import type { SerializedDrilldowns } from '../../server'; +import { initializeStateApi } from './initialize_state_api'; /** * Renders a component from the React Embeddable registry into a Presentation Panel. @@ -101,6 +100,7 @@ export const EmbeddableRenderer = < finalizeApi, uuid, parentApi, + initializeStateApi: (args) => initializeStateApi({ ...args, uuid, parentApi }), initializeDrilldownsManager: async ( embeddableUuid: string, state: SerializedDrilldowns diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/types.ts b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/types.ts index fa81d86b737cd..96166e2919287 100644 --- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/types.ts +++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/types.ts @@ -13,10 +13,12 @@ import type { HasSerializableState, HasType, PublishesPhaseEvents, + PublishesUnsavedChanges, } from '@kbn/presentation-publishing'; import type React from 'react'; import type { initializeDrilldownsManager } from '../drilldowns/drilldowns_manager'; import type { SerializedDrilldowns } from '../../server'; +import type { ContainerStateManagerInitializer } from './initialize_state_api'; /** * The default embeddable API that all Embeddables must implement. @@ -62,6 +64,13 @@ export interface BuildEmbeddableProps< */ parentApi: unknown | undefined; + /** + * Initializes and returns all APIs required for the parent to interact with the state of this API. + */ + initializeStateApi: ( + args: ContainerStateManagerInitializer + ) => PublishesUnsavedChanges & HasSerializableState; + /** * */ diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx index 3ff346bfcb80a..1ae5ed4bb2762 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx @@ -15,7 +15,7 @@ import type { ExpressionRendererParams } from '@kbn/expressions-plugin/public'; import { useExpressionRenderer } from '@kbn/expressions-plugin/public'; import { i18n } from '@kbn/i18n'; import { dispatchRenderComplete } from '@kbn/kibana-utils-plugin/public'; -import { apiPublishesSettings, initializeUnsavedChanges } from '@kbn/presentation-publishing'; +import { apiPublishesSettings } from '@kbn/presentation-publishing'; import { apiHasDisableTriggers, apiHasExecutionContext, @@ -62,6 +62,7 @@ export const visualizeEmbeddableFactory: EmbeddableFactory({ - uuid, - parentApi, - serializeState: () => { - return serializeVisualizeEmbeddable(savedObjectId$.getValue(), linkedToLibrary); - }, + const unsavedChangesApi = initializeStateApi({ anyStateChange$: merge( drilldownsManager.anyStateChange$, savedObjectId$, @@ -235,14 +231,17 @@ export const visualizeEmbeddableFactory: EmbeddableFactory { - drilldownsManager.reinitializeState(lastSaved ?? {}); - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - - if (!lastSaved) return; - const lastSavedRuntimeState = await deserializeState(lastSaved); - serializedVis$.next(lastSavedRuntimeState.serializedVis); + serializeState: () => { + return serializeVisualizeEmbeddable(savedObjectId$.getValue(), linkedToLibrary); + }, + applySerializedState: async (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + + if (!nextState) return; + const nextRuntimeState = await deserializeState(nextState); + serializedVis$.next(nextRuntimeState.serializedVis); }, }); 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 7343db8a86b1e..ad2910dce08f1 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 @@ -20,7 +20,6 @@ import { titleComparators, timeRangeComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import React, { useEffect } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { @@ -109,7 +108,7 @@ export const getFieldStatsChartEmbeddableFactory = ( FieldStatisticsTableEmbeddableApi > = { type: FIELD_STATS_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ uuid, initialState, parentApi, finalizeApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const [coreStart, pluginStart] = await getStartServices(); const { http, uiSettings, notifications, ...startServices } = coreStart; @@ -191,18 +190,7 @@ export const getFieldStatsChartEmbeddableFactory = ( const { toasts } = deps.notifications; - const serializeState = () => { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...serializeFieldStatsChartState(), - }; - }; - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, timeRangeManager.anyStateChange$, @@ -213,10 +201,15 @@ export const getFieldStatsChartEmbeddableFactory = ( ...fieldStatsControlsComparators, ...timeRangeComparators, }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - timeRangeManager.reinitializeState(lastSaved); - fieldStatsStateManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...serializeFieldStatsChartState(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + timeRangeManager.reinitializeState(nextState); + fieldStatsStateManager.reinitializeState(nextState); }, }); @@ -224,7 +217,7 @@ export const getFieldStatsChartEmbeddableFactory = ( ...timeRangeManager.api, ...titleManager.api, ...fieldStatsControlsApi, - ...unsavedChangesApi, + ...stateApi, // PublishesDataLoading dataLoading$, // PublishesBlockingError @@ -262,7 +255,6 @@ export const getFieldStatsChartEmbeddableFactory = ( }); }, dataViews$, - serializeState, }); const reload$ = fetch$(api).pipe( diff --git a/x-pack/platform/plugins/shared/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx b/x-pack/platform/plugins/shared/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx index c670f979b19a4..fbb214cd99d6f 100644 --- a/x-pack/platform/plugins/shared/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx @@ -21,7 +21,6 @@ import { titleComparators, timeRangeComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import fastIsEqual from 'fast-deep-equal'; import React, { useMemo } from 'react'; @@ -43,7 +42,7 @@ export const getChangePointChartEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: EMBEDDABLE_CHANGE_POINT_CHART_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const [coreStart, pluginStart] = await getStartServices(); const timeRangeManager = initializeTimeRangeManager(initialState); @@ -62,18 +61,7 @@ export const getChangePointChartEmbeddableFactory = ( const filtersApi = apiPublishesFilters(parentApi) ? parentApi : undefined; - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...changePointManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, timeRangeManager.anyStateChange$, @@ -86,10 +74,15 @@ export const getChangePointChartEmbeddableFactory = ( ...changePointComparators, }; }, - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) changePointManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...changePointManager.getLatestState(), + }), + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) changePointManager.reinitializeState(nextState); }, }); @@ -97,7 +90,7 @@ export const getChangePointChartEmbeddableFactory = ( ...timeRangeManager.api, ...titleManager.api, ...changePointManager.api, - ...unsavedChangesApi, + ...stateApi, getTypeDisplayName: () => i18n.translate('xpack.aiops.changePointDetection.typeDisplayName', { defaultMessage: 'change point charts', @@ -134,7 +127,6 @@ export const getChangePointChartEmbeddableFactory = ( dataLoading$, blockingError$, dataViews$, - serializeState, }); const ChangePointDetectionComponent = getChangePointDetectionComponent( diff --git a/x-pack/platform/plugins/shared/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx b/x-pack/platform/plugins/shared/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx index c85d75a56de03..082f669592229 100644 --- a/x-pack/platform/plugins/shared/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx @@ -22,7 +22,6 @@ import { titleComparators, timeRangeComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import fastIsEqual from 'fast-deep-equal'; import React, { useMemo } from 'react'; @@ -42,7 +41,7 @@ export const getLogRateAnalysisEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi, parentApi, uuid }) => { const [coreStart, pluginStart] = await getStartServices(); const runtimeState = initialState; const timeRangeManager = initializeTimeRangeManager(initialState); @@ -62,18 +61,7 @@ export const getLogRateAnalysisEmbeddableFactory = ( const filtersApi = apiPublishesFilters(parentApi) ? parentApi : undefined; - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...serializeLogRateAnalysisChartState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( timeRangeManager.anyStateChange$, titleManager.anyStateChange$, @@ -84,17 +72,22 @@ export const getLogRateAnalysisEmbeddableFactory = ( ...timeRangeComparators, ...titleComparators, }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - timeRangeManager.reinitializeState(lastSaved); - logRateAnalysisControlsApi.updateUserInput(lastSaved ?? {}); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...serializeLogRateAnalysisChartState(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + timeRangeManager.reinitializeState(nextState); + logRateAnalysisControlsApi.updateUserInput(nextState ?? {}); }, }); const api = finalizeApi({ ...timeRangeManager.api, ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, ...logRateAnalysisControlsApi, getTypeDisplayName: () => i18n.translate('xpack.aiops.logRateAnalysis.typeDisplayName', { @@ -134,7 +127,6 @@ export const getLogRateAnalysisEmbeddableFactory = ( dataLoading$, blockingError$, dataViews$, - serializeState, }); const LogRateAnalysisEmbeddableWrapper = getLogRateAnalysisEmbeddableWrapperComponent( diff --git a/x-pack/platform/plugins/shared/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx b/x-pack/platform/plugins/shared/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx index 04cb3be8b91ed..1b722ba651030 100644 --- a/x-pack/platform/plugins/shared/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx +++ b/x-pack/platform/plugins/shared/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx @@ -20,7 +20,6 @@ import { timeRangeComparators, titleComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import fastIsEqual from 'fast-deep-equal'; import React, { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; @@ -38,7 +37,7 @@ export const getPatternAnalysisEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: EMBEDDABLE_PATTERN_ANALYSIS_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { const [coreStart, pluginStart] = await getStartServices(); const runtimeState = initialState; const timeRangeManager = initializeTimeRangeManager(initialState); @@ -61,18 +60,7 @@ export const getPatternAnalysisEmbeddableFactory = ( const filtersApi = apiPublishesFilters(parentApi) ? parentApi : undefined; - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...serializePatternAnalysisChartState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( timeRangeManager.anyStateChange$, titleManager.anyStateChange$, @@ -87,11 +75,16 @@ export const getPatternAnalysisEmbeddableFactory = ( ...titleComparators, ...patternAnalysisControlsComparators, }), - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) { - patternAnalysisControlsApi.updateUserInput(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...serializePatternAnalysisChartState(), + }), + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) { + patternAnalysisControlsApi.updateUserInput(nextState); } }, }); @@ -99,7 +92,7 @@ export const getPatternAnalysisEmbeddableFactory = ( const api = finalizeApi({ ...timeRangeManager.api, ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, ...patternAnalysisControlsApi, getTypeDisplayName: () => i18n.translate('xpack.aiops.patternAnalysis.typeDisplayName', { @@ -131,7 +124,6 @@ export const getPatternAnalysisEmbeddableFactory = ( dataLoading$, blockingError$, dataViews$, - serializeState, }); const PatternAnalysisComponent = getPatternAnalysisComponent(coreStart, pluginStart); 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 bbb0cc085cd52..cecd3752135c2 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 @@ -17,6 +17,7 @@ import { getAlertsTableEmbeddableFactory } from './alerts_table_embeddable_facto import { PERSISTED_TABLE_CONFIG_KEY_PREFIX } from '../constants'; import type { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types'; import { getInternalRuleTypes } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types'; +import { getMockinitializeStateApi } from '@kbn/embeddable-plugin/public/mocks'; const core = coreMock.createStart(); const mockPresentationContainer = getMockPresentationContainer(); @@ -45,6 +46,7 @@ describe('getEmbeddableAlertsTableFactory', () => { ); const embeddableParams: Parameters[0] = { initializeDrilldownsManager: jest.fn(), + initializeStateApi: getMockinitializeStateApi(factory), initialState: { time_range: { from: '2025-01-01T00:00:00.000Z', 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 390ce1e1a2e70..65c90cefc874a 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 @@ -19,7 +19,6 @@ import { } from '@kbn/presentation-publishing'; import { QueryClientProvider } from '@kbn/react-query'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { openLazyFlyout } from '@kbn/presentation-util'; import { getRuleTypeIdsForSolution } from '@kbn/response-ops-alerts-filters-form/utils/solutions'; import { getInternalRuleTypesWithCache } from '../utils/get_internal_rule_types_with_cache'; @@ -39,7 +38,7 @@ export const getAlertsTableEmbeddableFactory = ( deps: EmbeddableAlertsTablePublicStartDependencies ): EmbeddableFactory => ({ type: EMBEDDABLE_ALERTS_TABLE_ID, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi, uuid }) => { const timeRangeManager = initializeTimeRangeManager(initialState); const titleManager = initializeTitleManager(initialState ?? {}); const queryLoading$ = new BehaviorSubject(true); @@ -51,31 +50,27 @@ export const getAlertsTableEmbeddableFactory = ( const initialTableConfig = initialState.tableConfig; const tableConfig$ = new BehaviorSubject(initialTableConfig); - const serializeState = (): EmbeddableAlertsTableSerializedState => ({ - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - tableConfig: tableConfig$.getValue(), - }); - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, + const stateApi = initializeStateApi({ anyStateChange$: merge( timeRangeManager.anyStateChange$, titleManager.anyStateChange$, tableConfig$ ).pipe(map(() => undefined)), - serializeState, getComparators: () => ({ ...titleComparators, ...timeRangeComparators, tableConfig: 'deepEquality', }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - timeRangeManager.reinitializeState(lastSaved); - if (lastSaved?.tableConfig) { - tableConfig$.next(lastSaved.tableConfig); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + tableConfig: tableConfig$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + timeRangeManager.reinitializeState(nextState); + if (nextState?.tableConfig) { + tableConfig$.next(nextState.tableConfig); } }, }); @@ -89,9 +84,8 @@ export const getAlertsTableEmbeddableFactory = ( const api = finalizeApi({ ...timeRangeManager.api, ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, dataLoading$: queryLoading$, - serializeState, isEditingEnabled: () => { // Users cannot edit panels based on a solution they cannot access. // The first condition ensures panels are editable even if the table configuration is diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx index 2a1797818ff81..2a2d90f97da45 100644 --- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx +++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/lens_embeddable.tsx @@ -8,7 +8,6 @@ import React from 'react'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { initializeTitleManager } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { merge } from 'rxjs'; import { LENS_EMBEDDABLE_TYPE, type LensRuntimeState } from '@kbn/lens-common'; import type { LensApi, LensSerializedAPIConfig } from '@kbn/lens-common-2'; @@ -57,6 +56,7 @@ export const createLensEmbeddableFactory = ( initializeDrilldownsManager, initialState, finalizeApi, + initializeStateApi, parentApi, uuid, }) => { @@ -139,15 +139,7 @@ export const createLensEmbeddableFactory = ( }; } - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState: () => { - if (internalApi.isEditingInProgress()) { - return initialState; - } - return integrationsConfig.api.serializeState(); - }, + const stateApi = initializeStateApi({ anyStateChange$: merge( actionsConfig.anyStateChange$, dashboardConfig.anyStateChange$, @@ -173,13 +165,19 @@ export const createLensEmbeddableFactory = ( } return comparators; }, - onReset: async (lastSaved) => { - actionsConfig.reinitializeState(lastSaved); - dashboardConfig.reinitializeState(lastSaved); - searchContextConfig.reinitializeState(lastSaved); - if (!lastSaved) return; - const lastSavedRuntimeState = await deserializeState(services, lastSaved); - stateConfig.reinitializeRuntimeState(lastSavedRuntimeState); + serializeState: () => { + if (internalApi.isEditingInProgress()) { + return initialState; + } + return integrationsConfig.api.serializeState(); + }, + applySerializedState: async (nextState) => { + actionsConfig.reinitializeState(nextState); + dashboardConfig.reinitializeState(nextState); + searchContextConfig.reinitializeState(nextState); + if (!nextState) return; + const nextRuntimeState = await deserializeState(services, nextState); + stateConfig.reinitializeRuntimeState(nextRuntimeState); }, }); @@ -192,7 +190,7 @@ export const createLensEmbeddableFactory = ( // dashboardConfig who owns the savedObjectId after the // stateConfig one who owns the inline editing { - ...unsavedChangesApi, + ...stateApi, ...editConfig.api, ...inspectorConfig.api, ...searchContextConfig.api, diff --git a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_cross_panel_actions.ts b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_cross_panel_actions.ts index 43c49a353bdd2..bd3b84a49fe76 100644 --- a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_cross_panel_actions.ts +++ b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_cross_panel_actions.ts @@ -226,9 +226,9 @@ export function initializeCrossPanelActions({ anyStateChange$: merge(isMovementSynchronized$, isFilterByMapExtent$).pipe( map(() => undefined) ), - reinitializeState: (lastSaved: MapEmbeddableState) => { - setIsMovementSynchronized(lastSaved.isMovementSynchronized); - setIsFilterByMapExtent(lastSaved.filterByMapExtent); + reinitializeState: (nextState?: MapEmbeddableState) => { + setIsMovementSynchronized(nextState?.isMovementSynchronized); + setIsFilterByMapExtent(nextState?.filterByMapExtent); }, getLatestState: () => { return { diff --git a/x-pack/platform/plugins/shared/maps/public/react_embeddable/map_react_embeddable.tsx b/x-pack/platform/plugins/shared/maps/public/react_embeddable/map_react_embeddable.tsx index b0125bb99646b..1dbdd54a2e890 100644 --- a/x-pack/platform/plugins/shared/maps/public/react_embeddable/map_react_embeddable.tsx +++ b/x-pack/platform/plugins/shared/maps/public/react_embeddable/map_react_embeddable.tsx @@ -17,7 +17,6 @@ import { titleComparators, useBatchedPublishingSubjects, apiPublishesSettings, - initializeUnsavedChanges, } from '@kbn/presentation-publishing'; import { BehaviorSubject, merge } from 'rxjs'; import { @@ -57,6 +56,7 @@ export const mapEmbeddableFactory: EmbeddableFactory type: MAP_SAVED_OBJECT_TYPE, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, parentApi, @@ -117,15 +117,7 @@ export const mapEmbeddableFactory: EmbeddableFactory return getByValueState(getLatestState(), savedMap.getAttributes()); } - function serializeState() { - const savedObjectId = savedMap.getSavedObjectId(); - return savedObjectId ? serializeByReference(savedObjectId) : serializeByValue(); - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( drilldownsManager.anyStateChange$, crossPanelActions.anyStateChange$, @@ -145,13 +137,18 @@ export const mapEmbeddableFactory: EmbeddableFactory savedObjectId: 'skip', }; }, - onReset: async (lastSaved) => { - drilldownsManager.reinitializeState(lastSaved ?? {}); - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); + serializeState: () => { + const savedObjectId = savedMap.getSavedObjectId(); + return savedObjectId ? serializeByReference(savedObjectId) : serializeByValue(); + }, + applySerializedState: async (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + crossPanelActions.reinitializeState(nextState); + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); - if (lastSaved) { - await savedMap.reset(lastSaved); + if (nextState) { + await savedMap.reset(nextState); } }, }); @@ -159,7 +156,7 @@ export const mapEmbeddableFactory: EmbeddableFactory api = finalizeApi({ defaultTitle$, defaultDescription$, - ...unsavedChangesApi, + ...stateApi, ...timeRangeManager.api, ...drilldownsManager.api, ...titleManager.api, @@ -183,7 +180,6 @@ export const mapEmbeddableFactory: EmbeddableFactory ), ...initializeDataViews(savedMap.getStore()), ...projectRoutingManager.api, - serializeState, supportedTriggers: () => { return [ON_OPEN_PANEL_MENU, ON_APPLY_FILTER, ON_CLICK_VALUE]; }, diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx index 8bdbe993fd3f8..18f1c68fe4661 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx @@ -20,7 +20,6 @@ import { initializeTimeRangeManager, initializeTitleManager, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { distinctUntilChanged } from 'rxjs'; import fastIsEqual from 'fast-deep-equal'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; @@ -47,7 +46,7 @@ export const getAnomalyChartsReactEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { if (!apiHasExecutionContext(parentApi)) { throw new Error('Parent API does not have execution context'); } @@ -71,18 +70,7 @@ export const getAnomalyChartsReactEmbeddableFactory = ( parentApi ); - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...chartsManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, timeRangeManager.anyStateChange$, @@ -95,10 +83,15 @@ export const getAnomalyChartsReactEmbeddableFactory = ( ...anomalyChartsComparators, }; }, - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) chartsManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...chartsManager.getLatestState(), + }), + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) chartsManager.reinitializeState(nextState); }, }); @@ -136,7 +129,7 @@ export const getAnomalyChartsReactEmbeddableFactory = ( ...timeRangeManager.api, ...chartsManager.api, ...chartsManager.dataLoadingApi, - ...unsavedChangesApi, + ...stateApi, dataViews$: buildDataViewPublishingApi( { anomalyDetectorService: mlServices.anomalyDetectorService, @@ -145,7 +138,6 @@ export const getAnomalyChartsReactEmbeddableFactory = ( { jobIds: chartsManager.api.jobIds$ }, subscriptions ), - serializeState, }); const appliedTimeRange$: Observable = fetch$(api).pipe( diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx index 03f2c5b1142c5..9faafa701d471 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx @@ -88,6 +88,7 @@ describe('getAnomalySwimLaneEmbeddableFactory', () => { }; const { api, Component } = await factory.buildEmbeddable({ initializeDrilldownsManager: jest.fn(), + initializeStateApi: jest.fn(), initialState: { swimlaneType: 'viewBy', jobIds: ['my-job'], diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx index 72d3e1f4594b7..740906af1b560 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx @@ -29,7 +29,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import useUnmount from 'react-use/lib/useUnmount'; import { BehaviorSubject, distinctUntilChanged, map, merge, Subscription } from 'rxjs'; import fastIsEqual from 'fast-deep-equal'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { dispatchRenderComplete, dispatchRenderStart } from '@kbn/kibana-utils-plugin/public'; import { SWIM_LANE_SELECTION_TRIGGER } from '@kbn/ui-actions-plugin/common/trigger_ids'; import type { AnomalySwimlaneEmbeddableServices } from '..'; @@ -88,7 +87,7 @@ export const getAnomalySwimLaneEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initializeStateApi, initialState, finalizeApi, parentApi, uuid }) => { if (!apiHasExecutionContext(parentApi)) { throw new Error('Parent API does not have execution context'); } @@ -120,18 +119,7 @@ export const getAnomalySwimLaneEmbeddableFactory = ( // Helpers for swim lane data fetching const chartWidth$ = new BehaviorSubject(undefined); - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...swimlaneManager.getLatestState(), - } as AnomalySwimLaneEmbeddableState; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, timeRangeManager.anyStateChange$, @@ -148,10 +136,16 @@ export const getAnomalySwimLaneEmbeddableFactory = ( filters: 'skip', }; }, - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) swimlaneManager.reinitializeState(lastSaved); + serializeState: () => + ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...swimlaneManager.getLatestState(), + } as AnomalySwimLaneEmbeddableState), + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) swimlaneManager.reinitializeState(nextState); }, }); @@ -187,7 +181,7 @@ export const getAnomalySwimLaneEmbeddableFactory = ( ...titleManager.api, ...timeRangeManager.api, ...swimlaneManager.api, - ...unsavedChangesApi, + ...stateApi, query$, filters$, interval, @@ -201,7 +195,6 @@ export const getAnomalySwimLaneEmbeddableFactory = ( subscriptions ), dataLoading$, - serializeState, }); const { swimLaneData$, onDestroy } = initializeSwimLaneDataFetcher( api, diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.tsx index fe648e5a58445..c1f0c2238e0a2 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/single_metric_viewer/single_metric_viewer_embeddable_factory.tsx @@ -22,7 +22,6 @@ import { useStateFromPublishingSubject, } from '@kbn/presentation-publishing'; import { BehaviorSubject, Subscription, merge } from 'rxjs'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE } from '..'; import type { MlPluginStart, MlStartDependencies } from '../../plugin'; import type { SingleMetricViewerEmbeddableApi } from '../types'; @@ -46,7 +45,7 @@ export const getSingleMetricViewerEmbeddableFactory = ( SingleMetricViewerEmbeddableApi > = { type: ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi, uuid, parentApi }) => { const services = await getServices(getStartServices, usageCollection); const subscriptions = new Subscription(); const titleManager = initializeTitleManager(initialState); @@ -60,18 +59,7 @@ export const getSingleMetricViewerEmbeddableFactory = ( const dataLoading$ = new BehaviorSubject(true); const blockingError$ = new BehaviorSubject(undefined); - function serializeState() { - return { - ...titleManager.getLatestState(), - ...timeRangeManager.getLatestState(), - ...singleMetricManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, timeRangeManager.anyStateChange$, @@ -88,10 +76,15 @@ export const getSingleMetricViewerEmbeddableFactory = ( refreshConfig: 'skip', }; }, - onReset: (lastSaved) => { - timeRangeManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - if (lastSaved) singleMetricManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...timeRangeManager.getLatestState(), + ...singleMetricManager.getLatestState(), + }), + applySerializedState: (nextState) => { + timeRangeManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + if (nextState) singleMetricManager.reinitializeState(nextState); }, }); @@ -132,10 +125,9 @@ export const getSingleMetricViewerEmbeddableFactory = ( ...titleManager.api, ...timeRangeManager.api, ...singleMetricManager.api, - ...unsavedChangesApi, + ...stateApi, dataLoading$, blockingError$, - serializeState, }); const { singleMetricViewerData$, onDestroy } = initializeSingleMetricViewerDataFetcher( 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 fb81ecb71793e..296b7fde929ff 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 @@ -12,7 +12,6 @@ import { titleComparators, useBatchedPublishingSubjects, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { BehaviorSubject, map, merge } from 'rxjs'; import type { EmbeddableApmAlertingVizProps } from '../types'; import type { EmbeddableDeps } from '../../types'; @@ -26,7 +25,7 @@ export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: Emb DefaultEmbeddableApi > = { type: APM_ALERTING_FAILED_TRANSACTIONS_CHART_EMBEDDABLE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const state = initialState; const titleManager = initializeTitleManager(state); const serviceName$ = new BehaviorSubject(state.serviceName); @@ -40,26 +39,7 @@ export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: Emb const kuery$ = new BehaviorSubject(state.kuery); const filters$ = new BehaviorSubject(state.filters); - function serializeState(): EmbeddableApmAlertingVizProps { - return { - ...titleManager.getLatestState(), - serviceName: serviceName$.getValue(), - transactionType: transactionType$.getValue(), - transactionName: transactionName$.getValue(), - environment: environment$.getValue(), - rangeFrom: rangeFrom$.getValue(), - rangeTo: rangeTo$.getValue(), - rule: rule$.getValue(), - alert: alert$.getValue(), - kuery: kuery$.getValue(), - filters: filters$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - parentApi, - uuid, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, serviceName$, @@ -86,25 +66,37 @@ export const getApmAlertingFailedTransactionsChartEmbeddableFactory = (deps: Emb kuery: 'referenceEquality', filters: 'referenceEquality', }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - serviceName$.next(lastSaved?.serviceName ?? ''); - transactionType$.next(lastSaved?.transactionType); - transactionName$.next(lastSaved?.transactionName); - environment$.next(lastSaved?.environment); - rangeFrom$.next(lastSaved?.rangeFrom); - rangeTo$.next(lastSaved?.rangeTo); - rule$.next(lastSaved?.rule as EmbeddableApmAlertingVizProps['rule']); - alert$.next(lastSaved?.alert as EmbeddableApmAlertingVizProps['alert']); - kuery$.next(lastSaved?.kuery); - filters$.next(lastSaved?.filters); + serializeState: () => ({ + ...titleManager.getLatestState(), + serviceName: serviceName$.getValue(), + transactionType: transactionType$.getValue(), + transactionName: transactionName$.getValue(), + environment: environment$.getValue(), + rangeFrom: rangeFrom$.getValue(), + rangeTo: rangeTo$.getValue(), + rule: rule$.getValue(), + alert: alert$.getValue(), + kuery: kuery$.getValue(), + filters: filters$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + serviceName$.next(nextState?.serviceName ?? ''); + transactionType$.next(nextState?.transactionType); + transactionName$.next(nextState?.transactionName); + environment$.next(nextState?.environment); + rangeFrom$.next(nextState?.rangeFrom); + rangeTo$.next(nextState?.rangeTo); + rule$.next(nextState?.rule as EmbeddableApmAlertingVizProps['rule']); + alert$.next(nextState?.alert as EmbeddableApmAlertingVizProps['alert']); + kuery$.next(nextState?.kuery); + filters$.next(nextState?.filters); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, - serializeState, + ...stateApi, }); return { 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 f1a4381ad2567..4365a1889a736 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 @@ -12,7 +12,6 @@ import { titleComparators, useBatchedPublishingSubjects, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { BehaviorSubject, map, merge } from 'rxjs'; import type { EmbeddableApmAlertingLatencyVizProps } from '../types'; import type { EmbeddableDeps } from '../../types'; @@ -26,9 +25,11 @@ export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps DefaultEmbeddableApi > = { type: APM_ALERTING_LATENCY_CHART_EMBEDDABLE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const state = initialState; const titleManager = initializeTitleManager(state); + + // TODO, these behaviourSubjects should be replaced with a state manager. const serviceName$ = new BehaviorSubject(state.serviceName); const transactionType$ = new BehaviorSubject(state.transactionType); const transactionName$ = new BehaviorSubject(state.transactionName); @@ -43,27 +44,7 @@ export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps const kuery$ = new BehaviorSubject(state.kuery); const filters$ = new BehaviorSubject(state.filters); - function serializeState(): EmbeddableApmAlertingLatencyVizProps { - return { - ...titleManager.getLatestState(), - serviceName: serviceName$.getValue(), - transactionType: transactionType$.getValue(), - transactionName: transactionName$.getValue(), - environment: environment$.getValue(), - latencyThresholdInMicroseconds: latencyThresholdInMicroseconds$.getValue(), - rangeFrom: rangeFrom$.getValue(), - rangeTo: rangeTo$.getValue(), - rule: rule$.getValue(), - alert: alert$.getValue(), - kuery: kuery$.getValue(), - filters: filters$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - parentApi, - uuid, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, serviceName$, @@ -92,26 +73,39 @@ export const getApmAlertingLatencyChartEmbeddableFactory = (deps: EmbeddableDeps kuery: 'referenceEquality', filters: 'referenceEquality', }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - serviceName$.next(lastSaved?.serviceName ?? ''); - transactionType$.next(lastSaved?.transactionType); - transactionName$.next(lastSaved?.transactionName); - environment$.next(lastSaved?.environment); - latencyThresholdInMicroseconds$.next(lastSaved?.latencyThresholdInMicroseconds); - rangeFrom$.next(lastSaved?.rangeFrom); - rangeTo$.next(lastSaved?.rangeTo); - rule$.next(lastSaved?.rule as EmbeddableApmAlertingLatencyVizProps['rule']); - alert$.next(lastSaved?.alert as EmbeddableApmAlertingLatencyVizProps['alert']); - kuery$.next(lastSaved?.kuery); - filters$.next(lastSaved?.filters); + serializeState: () => ({ + ...titleManager.getLatestState(), + serviceName: serviceName$.getValue(), + transactionType: transactionType$.getValue(), + transactionName: transactionName$.getValue(), + environment: environment$.getValue(), + latencyThresholdInMicroseconds: latencyThresholdInMicroseconds$.getValue(), + rangeFrom: rangeFrom$.getValue(), + rangeTo: rangeTo$.getValue(), + rule: rule$.getValue(), + alert: alert$.getValue(), + kuery: kuery$.getValue(), + filters: filters$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + serviceName$.next(nextState?.serviceName ?? ''); + transactionType$.next(nextState?.transactionType); + transactionName$.next(nextState?.transactionName); + environment$.next(nextState?.environment); + latencyThresholdInMicroseconds$.next(nextState?.latencyThresholdInMicroseconds); + rangeFrom$.next(nextState?.rangeFrom); + rangeTo$.next(nextState?.rangeTo); + kuery$.next(nextState?.kuery); + filters$.next(nextState?.filters); + if (nextState?.rule) rule$.next(nextState.rule); + if (nextState?.alert) alert$.next(nextState.alert); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, - serializeState, + ...stateApi, }); return { 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 7abdf0eb97934..71ec08432c239 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 @@ -10,7 +10,6 @@ import { titleComparators, useBatchedPublishingSubjects, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import React from 'react'; import { BehaviorSubject, map, merge } from 'rxjs'; import { ApmEmbeddableContext } from '../../embeddable_context'; @@ -25,7 +24,7 @@ export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableD DefaultEmbeddableApi > = { type: APM_ALERTING_THROUGHPUT_CHART_EMBEDDABLE, - buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const state = initialState; const titleManager = initializeTitleManager(state); const serviceName$ = new BehaviorSubject(state.serviceName); @@ -39,26 +38,7 @@ export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableD const kuery$ = new BehaviorSubject(state.kuery); const filters$ = new BehaviorSubject(state.filters); - function serializeState(): EmbeddableApmAlertingVizProps { - return { - ...titleManager.getLatestState(), - serviceName: serviceName$.getValue(), - transactionType: transactionType$.getValue(), - transactionName: transactionName$.getValue(), - environment: environment$.getValue(), - rangeFrom: rangeFrom$.getValue(), - rangeTo: rangeTo$.getValue(), - rule: rule$.getValue(), - alert: alert$.getValue(), - kuery: kuery$.getValue(), - filters: filters$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - parentApi, - uuid, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, serviceName$, @@ -85,25 +65,37 @@ export const getApmAlertingThroughputChartEmbeddableFactory = (deps: EmbeddableD kuery: 'referenceEquality', filters: 'referenceEquality', }), - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - serviceName$.next(lastSaved?.serviceName ?? ''); - transactionType$.next(lastSaved?.transactionType); - transactionName$.next(lastSaved?.transactionName); - environment$.next(lastSaved?.environment); - rangeFrom$.next(lastSaved?.rangeFrom); - rangeTo$.next(lastSaved?.rangeTo); - rule$.next(lastSaved?.rule as EmbeddableApmAlertingVizProps['rule']); - alert$.next(lastSaved?.alert as EmbeddableApmAlertingVizProps['alert']); - kuery$.next(lastSaved?.kuery); - filters$.next(lastSaved?.filters); + serializeState: () => ({ + ...titleManager.getLatestState(), + serviceName: serviceName$.getValue(), + transactionType: transactionType$.getValue(), + transactionName: transactionName$.getValue(), + environment: environment$.getValue(), + rangeFrom: rangeFrom$.getValue(), + rangeTo: rangeTo$.getValue(), + rule: rule$.getValue(), + alert: alert$.getValue(), + kuery: kuery$.getValue(), + filters: filters$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + serviceName$.next(nextState?.serviceName ?? ''); + transactionType$.next(nextState?.transactionType); + transactionName$.next(nextState?.transactionName); + environment$.next(nextState?.environment); + rangeFrom$.next(nextState?.rangeFrom); + rangeTo$.next(nextState?.rangeTo); + rule$.next(nextState?.rule as EmbeddableApmAlertingVizProps['rule']); + alert$.next(nextState?.alert as EmbeddableApmAlertingVizProps['alert']); + kuery$.next(nextState?.kuery); + filters$.next(nextState?.filters); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, - serializeState, + ...stateApi, }); return { 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 b91226b6ef48b..bfd9169555808 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 @@ -22,7 +22,6 @@ import { import { QueryClient, QueryClientProvider } from '@kbn/react-query'; import React, { useEffect } from 'react'; import { BehaviorSubject, Subject, merge } from 'rxjs'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { PluginContext } from '../../../context/plugin_context'; import type { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; import { @@ -54,10 +53,11 @@ export function getAlertsEmbeddableFactory({ type: SLO_ALERTS_EMBEDDABLE_ID, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, - uuid, parentApi, + uuid, }) => { const deps = { ...coreStart, ...pluginsStart }; const drilldownsManager = await initializeDrilldownsManager(uuid, initialState); @@ -83,21 +83,10 @@ export function getAlertsEmbeddableFactory({ const defaultTitle$ = new BehaviorSubject(getAlertsPanelTitle()); const reload$ = new Subject(); - function serializeState(): SloAlertsEmbeddableState { - return { - ...titleManager.getLatestState(), - ...drilldownsManager.getLatestState(), - ...sloAlertsStateManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( - drilldownsManager.anyStateChange$, titleManager.anyStateChange$, + drilldownsManager.anyStateChange$, sloAlertsStateManager.anyStateChange$ ), getComparators: () => ({ @@ -105,16 +94,20 @@ export function getAlertsEmbeddableFactory({ ...drilldownsManager.comparators, slos: 'referenceEquality', }), - onReset: (lastSaved) => { - drilldownsManager.reinitializeState(lastSaved ?? {}); - titleManager.reinitializeState(lastSaved); - sloAlertsStateManager.reinitializeState(lastSaved); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...drilldownsManager.getLatestState(), + ...sloAlertsStateManager.getLatestState(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + sloAlertsStateManager.reinitializeState(nextState); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, ...drilldownsManager.api, defaultTitle$, supportedTriggers: () => SLO_ALERTS_SUPPORTED_TRIGGERS, @@ -126,7 +119,6 @@ export function getAlertsEmbeddableFactory({ onEdit: async () => { onEdit(); }, - serializeState, getSloAlertsConfig: () => ({ slos: sloAlertsStateManager.api.slos$.getValue(), }), 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 e4f12b64aa401..0a726f3d611dd 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 @@ -7,7 +7,6 @@ import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { fetch$, initializeStateManager, @@ -42,10 +41,10 @@ export const getBurnRateEmbeddableFactory = ({ const factory: EmbeddableFactory = { type: SLO_BURN_RATE_EMBEDDABLE_ID, buildEmbeddable: async ({ - initialState, - finalizeApi, uuid, - parentApi, + finalizeApi, + initialState, + initializeStateApi, initializeDrilldownsManager, }) => { const deps = { ...coreStart, ...pluginsStart }; @@ -59,23 +58,12 @@ export const getBurnRateEmbeddableFactory = ({ const drilldownsManager = await initializeDrilldownsManager(uuid, initialState); const reload$ = new Subject(); - function serializeState(): BurnRateEmbeddableState { - return { - ...titleManager.getLatestState(), - ...sloBurnRateManager.getLatestState(), - ...drilldownsManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, sloBurnRateManager.anyStateChange$, drilldownsManager.anyStateChange$ ), - serializeState, getComparators: () => ({ ...titleComparators, ...drilldownsManager.comparators, @@ -83,19 +71,23 @@ export const getBurnRateEmbeddableFactory = ({ slo_instance_id: 'referenceEquality', duration: 'referenceEquality', }), - onReset: (lastSaved) => { - sloBurnRateManager.reinitializeState(lastSaved); - titleManager.reinitializeState(lastSaved); - drilldownsManager.reinitializeState(lastSaved ?? {}); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...sloBurnRateManager.getLatestState(), + ...drilldownsManager.getLatestState(), + }), + applySerializedState: (nextState) => { + sloBurnRateManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); + drilldownsManager.reinitializeState(nextState ?? {}); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, ...drilldownsManager.api, defaultTitle$, - serializeState, }); const fetchSubscription = fetch$(api) 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 48b74e090eafe..b11bbbe10ebdf 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 @@ -8,7 +8,6 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { fetch$, initializeStateManager, @@ -48,10 +47,10 @@ export const getErrorBudgetEmbeddableFactory = ({ type: SLO_ERROR_BUDGET_ID, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, uuid, - parentApi, }) => { const deps = { ...coreStart, ...pluginsStart }; const drilldownsManager = await initializeDrilldownsManager(uuid, initialState); @@ -63,18 +62,7 @@ export const getErrorBudgetEmbeddableFactory = ({ }); const reload$ = new Subject(); - function serializeState(): ErrorBudgetEmbeddableState { - return { - ...titleManager.getLatestState(), - ...drilldownsManager.getLatestState(), - ...sloErrorBudgetManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( drilldownsManager.anyStateChange$, titleManager.anyStateChange$, @@ -86,20 +74,24 @@ export const getErrorBudgetEmbeddableFactory = ({ slo_id: 'referenceEquality', slo_instance_id: 'referenceEquality', }), - onReset: (lastState) => { - drilldownsManager.reinitializeState(lastState ?? {}); - sloErrorBudgetManager.reinitializeState(lastState); - titleManager.reinitializeState(lastState); + serializeState: () => ({ + ...titleManager.getLatestState(), + ...drilldownsManager.getLatestState(), + ...sloErrorBudgetManager.getLatestState(), + }), + applySerializedState: (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + sloErrorBudgetManager.reinitializeState(nextState); + titleManager.reinitializeState(nextState); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, ...drilldownsManager.api, defaultTitle$, supportedTriggers: () => SLO_ERROR_BUDGET_SUPPORTED_TRIGGERS, - serializeState, }); const fetchSubscription = fetch$(api) diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx index 16ae5e8e40a29..5d918681e7905 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_embeddable_factory.tsx @@ -22,7 +22,6 @@ import { QueryClient, QueryClientProvider } from '@kbn/react-query'; import { ALL_VALUE } from '@kbn/slo-schema'; import React, { useEffect, useMemo } from 'react'; import { BehaviorSubject, Subject, map, merge } from 'rxjs'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { rewriteFiltersForSloSummary } from '../../../../common/rewrite_slo_filters'; import { PluginContext } from '../../../context/plugin_context'; import type { SLOPublicPluginsStart, SLORepositoryClient } from '../../../types'; @@ -56,10 +55,10 @@ export const getOverviewEmbeddableFactory = ({ type: SLO_OVERVIEW_EMBEDDABLE_ID, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, uuid, - parentApi, }) => { const deps = { ...coreStart, ...pluginsStart }; const state = initialState; @@ -89,35 +88,7 @@ export const getOverviewEmbeddableFactory = ({ const defaultTitle$ = new BehaviorSubject(getOverviewPanelTitle()); const reload$ = new Subject(); - function serializeState(): OverviewEmbeddableState { - const commonState = { - ...titleManager.getLatestState(), - ...drilldownsManager.getLatestState(), - }; - - if (overviewMode$.getValue() === 'single') { - return { - ...commonState, - overview_mode: 'single', - ...singleSloManager.getLatestState(), - }; - } - - if (overviewMode$.getValue() === 'groups') { - return { - ...commonState, - overview_mode: 'groups', - ...groupSloManager.getLatestState(), - }; - } - - throw new Error('overview_mode not provided'); - } - - const unsavedChangesApi = initializeUnsavedChanges({ - uuid, - parentApi, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( drilldownsManager.anyStateChange$, titleManager.anyStateChange$, @@ -134,17 +105,41 @@ export const getOverviewEmbeddableFactory = ({ ...titleComparators, ...drilldownsManager.comparators, }), - onReset: (lastSaved) => { - drilldownsManager.reinitializeState(lastSaved ?? {}); - titleManager.reinitializeState(lastSaved); - singleSloManager.reinitializeState(lastSaved as SingleOverviewCustomState); - groupSloManager.reinitializeState(lastSaved as GroupOverviewCustomState); - setOverviewMode(lastSaved?.overview_mode); + serializeState: () => { + const commonState = { + ...titleManager.getLatestState(), + ...drilldownsManager.getLatestState(), + }; + + if (overviewMode$.getValue() === 'single') { + return { + ...commonState, + overview_mode: 'single', + ...singleSloManager.getLatestState(), + }; + } + + if (overviewMode$.getValue() === 'groups') { + return { + ...commonState, + overview_mode: 'groups', + ...groupSloManager.getLatestState(), + }; + } + + throw new Error('overview_mode not provided'); + }, + applySerializedState: (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + titleManager.reinitializeState(nextState); + singleSloManager.reinitializeState(nextState as SingleOverviewCustomState); + groupSloManager.reinitializeState(nextState as GroupOverviewCustomState); + setOverviewMode(nextState?.overview_mode); }, }); const api = finalizeApi({ - ...unsavedChangesApi, + ...stateApi, ...titleManager.api, ...drilldownsManager.api, defaultTitle$, @@ -169,7 +164,6 @@ export const getOverviewEmbeddableFactory = ({ return Promise.reject(); } }, - serializeState, getSloGroupOverviewConfig: (): GroupOverviewCustomState => { return { ...groupSloManager.getLatestState(), diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx index 68944139b44df..7a03a0c660f61 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx @@ -20,7 +20,6 @@ import { fetch$, titleComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { BehaviorSubject, Subject, map, merge } from 'rxjs'; import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; import { StatusGridComponent } from './monitors_grid_component'; @@ -61,7 +60,7 @@ export const getMonitorsEmbeddableFactory = ( ) => { const factory: EmbeddableFactory = { type: SYNTHETICS_MONITORS_EMBEDDABLE, - buildEmbeddable: async ({ initialState, finalizeApi, parentApi, uuid }) => { + buildEmbeddable: async ({ initialState, finalizeApi, initializeStateApi }) => { const [coreStart, pluginStart] = await getStartServices(); const titleManager = initializeTitleManager(initialState); @@ -74,18 +73,7 @@ export const getMonitorsEmbeddableFactory = ( }); const view$ = new BehaviorSubject(initialState.view); - function serializeState() { - return { - ...titleManager.getLatestState(), - filters: filters$.getValue(), - view: view$.getValue(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - parentApi, - uuid, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge(titleManager.anyStateChange$, filters$, view$).pipe( map(() => undefined) ), @@ -97,23 +85,27 @@ export const getMonitorsEmbeddableFactory = ( defaultState: { filters: DEFAULT_FILTERS, }, - onReset: (lastSaved) => { - titleManager.reinitializeState(lastSaved); - filters$.next(lastSaved?.filters ?? DEFAULT_FILTERS); - if (lastSaved) view$.next(lastSaved?.view); + serializeState: () => ({ + ...titleManager.getLatestState(), + filters: filters$.getValue(), + view: view$.getValue(), + }), + applySerializedState: (nextState) => { + titleManager.reinitializeState(nextState); + filters$.next(nextState?.filters ?? DEFAULT_FILTERS); + if (nextState) view$.next(nextState?.view); }, }); const api = finalizeApi({ ...titleManager.api, - ...unsavedChangesApi, + ...stateApi, defaultTitle$, getTypeDisplayName: () => i18n.translate('xpack.synthetics.editSloOverviewEmbeddableTitle.typeDisplayName', { defaultMessage: 'filters', }), isEditingEnabled: () => true, - serializeState, onEdit: async () => { try { const result = await openMonitorConfiguration({ diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx index 33b1a935f6532..6ebbc9e554422 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx @@ -25,7 +25,6 @@ import { fetch$, titleComparators, } from '@kbn/presentation-publishing'; -import { initializeUnsavedChanges } from '@kbn/presentation-publishing'; import { BehaviorSubject, Subject, map, merge } from 'rxjs'; import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; import type { ClientPluginsStart } from '../../../plugin'; @@ -64,9 +63,9 @@ export const getStatsOverviewEmbeddableFactory = ( type: SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, buildEmbeddable: async ({ initializeDrilldownsManager, + initializeStateApi, initialState, finalizeApi, - parentApi, uuid, }) => { const [coreStart, pluginStart] = await getStartServices(); @@ -83,18 +82,7 @@ export const getStatsOverviewEmbeddableFactory = ( const drilldownsManager = await initializeDrilldownsManager(uuid, initialState); - function serializeState(): OverviewStatsEmbeddableState { - return { - ...titleManager.getLatestState(), - filters: filters$.getValue(), - ...drilldownsManager.getLatestState(), - }; - } - - const unsavedChangesApi = initializeUnsavedChanges({ - parentApi, - uuid, - serializeState, + const stateApi = initializeStateApi({ anyStateChange$: merge( titleManager.anyStateChange$, filters$, @@ -108,17 +96,22 @@ export const getStatsOverviewEmbeddableFactory = ( defaultState: { filters: DEFAULT_FILTERS, }, - onReset: (lastSaved) => { - drilldownsManager.reinitializeState(lastSaved ?? {}); - titleManager.reinitializeState(lastSaved); - filters$.next(lastSaved?.filters ?? DEFAULT_FILTERS); + serializeState: () => ({ + ...titleManager.getLatestState(), + filters: filters$.getValue(), + ...drilldownsManager.getLatestState(), + }), + applySerializedState: (nextState) => { + drilldownsManager.reinitializeState(nextState ?? {}); + titleManager.reinitializeState(nextState); + filters$.next(nextState?.filters ?? DEFAULT_FILTERS); }, }); const api = finalizeApi({ ...titleManager.api, ...drilldownsManager.api, - ...unsavedChangesApi, + ...stateApi, supportedTriggers: () => SYNTHETICS_STATS_SUPPORTED_TRIGGERS, defaultTitle$, getTypeDisplayName: () => @@ -145,7 +138,6 @@ export const getStatsOverviewEmbeddableFactory = ( return Promise.reject(); } }, - serializeState, }); const fetchSubscription = fetch$(api)