From 4525467be315c47dd7708dc611e3bb30dcc44a37 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 16 Sep 2025 15:49:43 -0400 Subject: [PATCH 1/3] Gives the embeddable system the ability to pause and unpause fetching --- .../presentation_publishing/index.ts | 7 +- .../interfaces/fetch/fetch.test.ts | 52 ++++++++ .../interfaces/fetch/fetch.ts | 117 +++++++++++------- .../interfaces/fetch/fetch_context.ts | 71 +++++++++++ .../interfaces/fetch/publishes_pause_fetch.ts | 20 +++ .../interfaces/fetch/publishes_reload.ts | 2 +- .../fetch/publishes_unified_search.ts | 19 ++- .../public/dashboard_api/get_dashboard_api.ts | 2 +- .../get_search_embeddable_factory.tsx | 42 ++++--- .../public/actions/data_request_actions.ts | 12 +- .../public/actions/map_action_constants.ts | 1 + .../shared/maps/public/actions/map_actions.ts | 21 ++++ .../react_embeddable/initialize_fetch.ts | 22 +++- .../shared/maps/public/reducers/map/map.ts | 7 ++ .../shared/maps/public/reducers/map/types.ts | 1 + 15 files changed, 316 insertions(+), 80 deletions(-) create mode 100644 src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts create mode 100644 src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts diff --git a/src/platform/packages/shared/presentation/presentation_publishing/index.ts b/src/platform/packages/shared/presentation/presentation_publishing/index.ts index 1e8d4d779d4e2..e2db9263ad2aa 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/index.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/index.ts @@ -30,7 +30,12 @@ export { apiCanLockHoverActions, type CanLockHoverActions, } from './interfaces/can_lock_hover_actions'; -export { fetch$, useFetchContext, type FetchContext } from './interfaces/fetch/fetch'; +export { fetch$, useFetchContext } from './interfaces/fetch/fetch'; +export type { FetchContext } from './interfaces/fetch/fetch_context'; +export { + type PublishesPauseFetch, + apiPublishesPauseFetch, +} from './interfaces/fetch/publishes_pause_fetch'; export { initializeTimeRangeManager, timeRangeComparators, diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts index bb4682c78e62e..eef95845a7304 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts @@ -131,6 +131,58 @@ describe('onFetchContextChanged', () => { }); }); + describe('with isFetchPaused$', () => { + test('should skip emits while fetch is paused', async () => { + const isFetchPaused$ = new BehaviorSubject(true); + const api = { + parentApi, + isFetchPaused$, + }; + const subscription = fetch$(api).subscribe(onFetchMock); + + parentApi.filters$.next([]); + parentApi.query$.next({ language: 'kquery', query: 'hello' }); + parentApi.reload$.next(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(onFetchMock).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + test('should emit most recent context when fetch becomes un-paused', async () => { + const isFetchPaused$ = new BehaviorSubject(true); + const api = { + parentApi, + isFetchPaused$, + }; + const subscription = fetch$(api).subscribe(onFetchMock); + + parentApi.filters$.next([]); + parentApi.query$.next({ language: 'kquery', query: '' }); + parentApi.reload$.next(); + + isFetchPaused$.next(false); + + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(onFetchMock).toHaveBeenCalledTimes(1); + const fetchContext = onFetchMock.mock.calls[0][0]; + expect(fetchContext).toEqual({ + filters: [], + isReload: true, + query: { + language: 'kquery', + query: '', + }, + searchSessionId: undefined, + timeRange: undefined, + timeslice: undefined, + }); + + subscription.unsubscribe(); + }); + }); + describe('no searchSession$', () => { test('should emit once on reload', async () => { const subscription = fetch$({ parentApi }).pipe(skip(1)).subscribe(onFetchMock); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts index 4d2488cae41e6..8d760c37d008e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts @@ -7,49 +7,49 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Observable } from 'rxjs'; +import { useEffect, useMemo } from 'react'; import { BehaviorSubject, + Subject, combineLatest, + combineLatestWith, debounceTime, delay, + distinctUntilChanged, filter, map, merge, of, skip, startWith, - Subject, switchMap, takeUntil, tap, + type Observable, } from 'rxjs'; -import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; -import { useMemo, useEffect } from 'react'; -import type { PublishesTimeRange, PublishesUnifiedSearch } from './publishes_unified_search'; -import { apiPublishesTimeRange, apiPublishesUnifiedSearch } from './publishes_unified_search'; -import type { PublishesSearchSession } from './publishes_search_session'; -import { apiPublishesSearchSession } from './publishes_search_session'; -import type { HasParentApi } from '../has_parent_api'; -import { apiHasParentApi } from '../has_parent_api'; -import { apiPublishesReload } from './publishes_reload'; import { useStateFromPublishingSubject } from '../../publishing_subject'; +import { apiHasParentApi, type HasParentApi } from '../has_parent_api'; +import { + type FetchContext, + type ReloadTimeFetchContext, + isReloadTimeFetchContextEqual, +} from './fetch_context'; +import { apiPublishesPauseFetch } from './publishes_pause_fetch'; +import { apiPublishesReload } from './publishes_reload'; +import { apiPublishesSearchSession, type PublishesSearchSession } from './publishes_search_session'; +import { + apiPublishesTimeRange, + apiPublishesUnifiedSearch, + type PublishesTimeRange, + type PublishesUnifiedSearch, +} from './publishes_unified_search'; -export interface FetchContext { - isReload: boolean; - filters: Filter[] | undefined; - query: Query | AggregateQuery | undefined; - searchSessionId: string | undefined; - timeRange: TimeRange | undefined; - timeslice: [number, number] | undefined; -} - -function getFetchContext(api: unknown, isReload: boolean) { +function getReloadTimeFetchContext(api: unknown, reloadTimestamp?: number): ReloadTimeFetchContext { const typeApi = api as Partial< PublishesTimeRange & HasParentApi> >; return { - isReload, + reloadTimestamp, filters: typeApi?.parentApi?.filters$?.value, query: typeApi?.parentApi?.query$?.value, searchSessionId: typeApi?.parentApi?.searchSessionId$?.value, @@ -121,36 +121,69 @@ export function fetch$(api: unknown): Observable { const batchedObservables = getBatchedObservables(api); const immediateObservables = getImmediateObservables(api); - if (immediateObservables.length === 0) { - return merge(...batchedObservables).pipe( - startWith(getFetchContext(api, false)), - debounceTime(0), - map(() => getFetchContext(api, false)) + const fetchContext$ = (() => { + if (immediateObservables.length === 0) { + return merge(...batchedObservables).pipe( + startWith(getReloadTimeFetchContext(api)), + debounceTime(0), + map(() => getReloadTimeFetchContext(api)) + ); + } + const interrupt = new Subject(); + const batchedChanges$ = merge(...batchedObservables).pipe( + switchMap((value) => + of(value).pipe( + delay(0), + takeUntil(interrupt), + map(() => getReloadTimeFetchContext(api)) + ) + ) ); - } - const interrupt = new Subject(); - const batchedChanges$ = merge(...batchedObservables).pipe( - switchMap((value) => - of(value).pipe( - delay(0), - takeUntil(interrupt), - map(() => getFetchContext(api, false)) - ) + const immediateChange$ = merge(...immediateObservables).pipe( + tap(() => { + interrupt.next(); + }), + map(() => getReloadTimeFetchContext(api, Date.now())) + ); + return merge(immediateChange$, batchedChanges$).pipe(startWith(getReloadTimeFetchContext(api))); + })(); + + const parentPauseFetch = + apiHasParentApi(api) && apiPublishesPauseFetch(api.parentApi) + ? api.parentApi.isFetchPaused$ + : of(false); + const apiPauseFetch = apiPublishesPauseFetch(api) ? api.isFetchPaused$ : of(false); + const isFetchPaused$ = combineLatest([parentPauseFetch, apiPauseFetch]).pipe( + map( + ([parentRequestingPause, apiRequestingPause]) => parentRequestingPause || apiRequestingPause ) ); - const immediateChange$ = merge(...immediateObservables).pipe( - tap(() => interrupt.next()), - map(() => getFetchContext(api, true)) + return fetchContext$.pipe( + combineLatestWith(isFetchPaused$), + filter(([, isFetchPaused]) => !isFetchPaused), + map(([fetchContext]) => fetchContext), + distinctUntilChanged((prevContext, nextContext) => + isReloadTimeFetchContextEqual(prevContext, nextContext) + ), + map((reloadTimeFetchContext) => ({ + isReload: Boolean(reloadTimeFetchContext.reloadTimestamp), + filters: reloadTimeFetchContext.filters, + query: reloadTimeFetchContext.query, + timeRange: reloadTimeFetchContext.timeRange, + timeslice: reloadTimeFetchContext.timeslice, + searchSessionId: reloadTimeFetchContext.searchSessionId, + })) ); - - return merge(immediateChange$, batchedChanges$).pipe(startWith(getFetchContext(api, false))); } export const useFetchContext = (api: unknown): FetchContext => { const context$: BehaviorSubject = useMemo(() => { - return new BehaviorSubject(getFetchContext(api, false)); + return new BehaviorSubject({ + ...getReloadTimeFetchContext(api), + isReload: false, + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts new file mode 100644 index 0000000000000..d090c55d43f08 --- /dev/null +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { COMPARE_ALL_OPTIONS, onlyDisabledFiltersChanged } from '@kbn/es-query'; +import fastIsEqual from 'fast-deep-equal'; + +export interface FetchContext { + isReload: boolean; + filters: Filter[] | undefined; + query: Query | AggregateQuery | undefined; + searchSessionId: string | undefined; + timeRange: TimeRange | undefined; + timeslice: [number, number] | undefined; +} + +export interface ReloadTimeFetchContext extends Omit { + reloadTimestamp?: number; +} + +export function isReloadTimeFetchContextEqual( + currentContext: ReloadTimeFetchContext, + lastContext: ReloadTimeFetchContext +): boolean { + if (currentContext.searchSessionId !== lastContext.searchSessionId) return false; + + return ( + isReloadTimestampEqualForFetch(currentContext.reloadTimestamp, lastContext.reloadTimestamp) && + areFiltersEqualForFetch(currentContext.filters, lastContext.filters) && + isQueryEqualForFetch(currentContext.query, lastContext.query) && + isTimeRangeEqualForFetch(currentContext.timeRange, lastContext.timeRange) && + isTimeSliceEqualForFetch(currentContext.timeslice, lastContext.timeslice) + ); +} + +export const areFiltersEqualForFetch = (currentFilters?: Filter[], lastFilters?: Filter[]) => { + return onlyDisabledFiltersChanged(currentFilters, lastFilters, { + ...COMPARE_ALL_OPTIONS, + // do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results) + state: false, + }); +}; + +export const isReloadTimestampEqualForFetch = ( + currentReloadTimestamp?: number, + lastReloadTimestamp?: number +) => { + if (!currentReloadTimestamp) return true; // if current reload timestamp is not set, this is not a force refresh. + return currentReloadTimestamp === lastReloadTimestamp; +}; + +export const isQueryEqualForFetch = ( + currentQuery: Query | AggregateQuery | undefined, + lastQuery: Query | AggregateQuery | undefined +) => fastIsEqual(currentQuery, lastQuery); + +export const isTimeRangeEqualForFetch = ( + currentTimeRange: TimeRange | undefined, + lastTimeRange: TimeRange | undefined +) => fastIsEqual(currentTimeRange, lastTimeRange); + +export const isTimeSliceEqualForFetch = ( + currentTimeslice: [number, number] | undefined, + lastTimeslice: [number, number] | undefined +) => fastIsEqual(currentTimeslice, lastTimeslice); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts new file mode 100644 index 0000000000000..154ef639b2fbf --- /dev/null +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Observable } from 'rxjs'; + +export interface PublishesPauseFetch { + isFetchPaused$: Observable; +} + +export const apiPublishesPauseFetch = ( + unknownApi: null | unknown +): unknownApi is PublishesPauseFetch => { + return Boolean(unknownApi && (unknownApi as PublishesPauseFetch)?.isFetchPaused$ !== undefined); +}; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts index cfd8c11340028..56d0e1863be1e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts @@ -10,7 +10,7 @@ import type { Observable } from 'rxjs'; export interface PublishesReload { - reload$: Omit, 'next'>; + reload$: Observable; } export const apiPublishesReload = (unknownApi: null | unknown): unknownApi is PublishesReload => { diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts index 5f8c335cb7ffe..736bd5452947e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts @@ -8,11 +8,14 @@ */ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; -import { COMPARE_ALL_OPTIONS, onlyDisabledFiltersChanged } from '@kbn/es-query'; -import fastIsEqual from 'fast-deep-equal'; import { useEffect, useMemo } from 'react'; import { BehaviorSubject } from 'rxjs'; import type { PublishingSubject } from '../../publishing_subject'; +import { + areFiltersEqualForFetch, + isQueryEqualForFetch, + isTimeRangeEqualForFetch, +} from './fetch_context'; export interface PublishesTimeslice { timeslice$: PublishingSubject<[number, number] | undefined>; @@ -115,25 +118,19 @@ export function useSearchApi({ }, []); useEffect(() => { - if ( - !onlyDisabledFiltersChanged(searchApi.filters$.getValue(), filters, { - ...COMPARE_ALL_OPTIONS, - // do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results) - state: false, - }) - ) { + if (!areFiltersEqualForFetch(searchApi.filters$.getValue(), filters)) { searchApi.filters$.next(filters); } }, [filters, searchApi.filters$]); useEffect(() => { - if (!fastIsEqual(searchApi.query$.getValue(), query)) { + if (!isQueryEqualForFetch(searchApi.query$.getValue(), query)) { searchApi.query$.next(query); } }, [query, searchApi.query$]); useEffect(() => { - if (!fastIsEqual(searchApi.timeRange$.getValue(), timeRange)) { + if (!isTimeRangeEqualForFetch(searchApi.timeRange$.getValue(), timeRange)) { searchApi.timeRange$.next(timeRange); } }, [timeRange, searchApi.timeRange$]); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index beee0199da1e7..ee229f645a18b 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -9,7 +9,7 @@ import type { Reference } from '@kbn/content-management-utils'; import type { EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; -import { BehaviorSubject, debounceTime, merge } from 'rxjs'; +import { BehaviorSubject, concat, debounceTime, delay, merge, of } from 'rxjs'; import { v4 } from 'uuid'; import { DASHBOARD_APP_ID } from '../../common/constants'; import { getReferencesForControls, getReferencesForPanelId } from '../../common'; 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 8842753265345..527f1830fa356 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 @@ -105,26 +105,6 @@ export const getSearchEmbeddableFactory = ({ const searchEmbeddable = await initializeSearchEmbeddableApi(runtimeState, { discoverServices, }); - const unsubscribeFromFetch = initializeFetch({ - api: { - parentApi, - ...titleManager.api, - ...timeRangeManager.api, - defaultTitle$, - savedSearch$: searchEmbeddable.api.savedSearch$, - dataViews$: searchEmbeddable.api.dataViews$, - savedObjectId$, - dataLoading$, - blockingError$, - fetchContext$, - fetchWarnings$, - }, - discoverServices, - stateManager: searchEmbeddable.stateManager, - scopedProfilesManager, - setDataLoading: (dataLoading: boolean | undefined) => dataLoading$.next(dataLoading), - setBlockingError: (error: Error | undefined) => blockingError$.next(error), - }); const serialize = (savedObjectId?: string) => serializeState({ @@ -239,6 +219,28 @@ export const getSearchEmbeddableFactory = ({ }, }); + const unsubscribeFromFetch = initializeFetch({ + api: { + ...api, + parentApi, + ...titleManager.api, + ...timeRangeManager.api, + defaultTitle$, + savedSearch$: searchEmbeddable.api.savedSearch$, + dataViews$: searchEmbeddable.api.dataViews$, + savedObjectId$, + dataLoading$, + blockingError$, + fetchContext$, + fetchWarnings$, + }, + discoverServices, + stateManager: searchEmbeddable.stateManager, + scopedProfilesManager, + setDataLoading: (dataLoading: boolean | undefined) => dataLoading$.next(dataLoading), + setBlockingError: (error: Error | undefined) => blockingError$.next(error), + }); + return { api, Component: () => { diff --git a/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts b/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts index 23651348527fb..ff5fb190cc4fc 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts @@ -193,7 +193,11 @@ export function syncDataForLayerDueToDrawing(layer: ILayer) { true, false ); - if (!layer.isVisible() || !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom)) { + if ( + getState().map.__pauseSyncData || + !layer.isVisible() || + !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom) + ) { return; } await layer.syncData(dataRequestContext); @@ -209,7 +213,11 @@ export function syncDataForLayer(layer: ILayer, isForceRefresh: boolean) { false, isForceRefresh ); - if (!layer.isVisible() || !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom)) { + if ( + getState().map.__pauseSyncData || + !layer.isVisible() || + !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom) + ) { return; } await layer.syncData(dataRequestContext); diff --git a/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts b/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts index a1471209f73b7..7f290be925865 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts @@ -22,6 +22,7 @@ export const LAYER_DATA_LOAD_ERROR = 'LAYER_DATA_LOAD_ERROR'; export const UPDATE_SOURCE_DATA_REQUEST = 'UPDATE_SOURCE_DATA_REQUEST'; export const SET_JOINS = 'SET_JOINS'; export const SET_QUERY = 'SET_QUERY'; +export const SET_PAUSE_SYNC_DATA = 'SET_PAUSE_SYNC_DATA'; export const UPDATE_LAYER = 'UPDATE_LAYER'; export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP'; export const UPDATE_LAYER_STYLE = 'UPDATE_LAYER_STYLE'; diff --git a/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts b/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts index be38c1b450840..a0b4e20e22eb8 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts @@ -53,6 +53,7 @@ import { SET_OPEN_TOOLTIPS, SET_QUERY, TRACK_MAP_SETTINGS, + SET_PAUSE_SYNC_DATA, UPDATE_DRAW_STATE, UPDATE_MAP_SETTING, UPDATE_EDIT_STATE, @@ -79,6 +80,26 @@ import { expandToTileBoundaries, getTilesForExtent } from '../classes/util/geo_t import { getToasts } from '../kibana_services'; import { getDeletedFeatureIds } from '../selectors/ui_selectors'; +export function setPauseSyncData(pauseSyncData: boolean) { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { + dispatch({ + type: SET_PAUSE_SYNC_DATA, + pauseSyncData, + }); + + if (!pauseSyncData) { + if (getMapSettings(getState()).autoFitToDataBounds) { + dispatch(autoFitToBounds()); + } else { + dispatch(syncDataForAllLayers(false)); + } + } + }; +} + export function setMapInitError(errorMessage: string) { return { type: SET_MAP_INIT_ERROR, diff --git a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts index 202975cad319b..cb9ee48f5eb36 100644 --- a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts +++ b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { combineLatest, map, of } from 'rxjs'; import type { FetchContext } from '@kbn/presentation-publishing'; -import { fetch$ } from '@kbn/presentation-publishing'; +import { apiHasParentApi, apiPublishesPauseFetch, fetch$ } from '@kbn/presentation-publishing'; import type { Query } from '@kbn/es-query'; import type { MapExtent } from '../../common/descriptor_types'; import { getSearchService } from '../kibana_services'; import type { MapStore } from '../reducers/store'; import type { MapApi } from './types'; -import { setMapSettings, setQuery } from '../actions'; +import { setMapSettings, setQuery, setPauseSyncData } from '../actions'; function getIsRestore(searchSessionId?: string) { if (!searchSessionId) { @@ -77,7 +78,24 @@ export function initializeFetch({ }) ); }); + + const parentPauseFetch = + apiHasParentApi(api) && apiPublishesPauseFetch(api.parentApi) + ? api.parentApi.isFetchPaused$ + : of(false); + const apiPauseFetch = apiPublishesPauseFetch(api) ? api.isFetchPaused$ : of(false); + const isPausedSubscription = combineLatest([parentPauseFetch, apiPauseFetch]) + .pipe( + map(([parentPause, apiPause]) => { + return parentPause || apiPause; + }) + ) + .subscribe((isFetchPaused) => { + store.dispatch(setPauseSyncData(isFetchPaused)); + }); + return () => { fetchSubscription.unsubscribe(); + isPausedSubscription?.unsubscribe(); }; } diff --git a/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts b/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts index bb19a67275cf0..9f71b3093b841 100644 --- a/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts +++ b/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts @@ -47,6 +47,7 @@ import { UPDATE_MAP_SETTING, UPDATE_EDIT_STATE, SET_EXECUTION_CONTEXT, + SET_PAUSE_SYNC_DATA, } from '../../actions/map_action_constants'; import { getDefaultMapSettings } from './default_map_settings'; @@ -86,6 +87,7 @@ export const DEFAULT_MAP_STATE: MapState = { waitingForMapReadyLayerList: [], settings: getDefaultMapSettings(), __rollbackSettings: null, + __pauseSyncData: false, }; export function map(state: MapState = DEFAULT_MAP_STATE, action: Record) { @@ -228,6 +230,11 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: Record layer.id === action.selectedLayerId); return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null }; + case SET_PAUSE_SYNC_DATA: + return { + ...state, + __pauseSyncData: action.pauseSyncData, + }; case UPDATE_LAYER_ORDER: return { ...state, diff --git a/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts b/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts index 129c80fc79c65..942ef7baf4e3c 100644 --- a/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts +++ b/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts @@ -64,4 +64,5 @@ export type MapState = { waitingForMapReadyLayerList: LayerDescriptor[]; settings: MapSettings; __rollbackSettings: MapSettings | null; + __pauseSyncData: boolean; }; From 2a17542190ba2432d503305b3647cda1e209a23c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:22:31 +0000 Subject: [PATCH 2/3] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../shared/dashboard/public/dashboard_api/get_dashboard_api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index ee229f645a18b..beee0199da1e7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -9,7 +9,7 @@ import type { Reference } from '@kbn/content-management-utils'; import type { EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; -import { BehaviorSubject, concat, debounceTime, delay, merge, of } from 'rxjs'; +import { BehaviorSubject, debounceTime, merge } from 'rxjs'; import { v4 } from 'uuid'; import { DASHBOARD_APP_ID } from '../../common/constants'; import { getReferencesForControls, getReferencesForPanelId } from '../../common'; From 0fbc323dbf7bd96ea8d1a21696ef39cbbeb9abf5 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 1 Oct 2025 16:21:11 -0400 Subject: [PATCH 3/3] update jest test --- .../public/embeddable/get_search_embeddable_factory.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e7b7a3b74920..10810005fb2a4 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 @@ -285,7 +285,7 @@ describe('saved search embeddable', () => { expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); // trigger a refetch - dashboadFilters.next([]); + dashboadFilters.next([{ meta: {} }]); await waitOneTick(); expect(resolveDataSourceProfileSpy).toHaveBeenCalled(); });