From 74ddf6d2f62c11283bd67ca5e3a12f6d20efcb67 Mon Sep 17 00:00:00 2001 From: setoh2000 Date: Fri, 3 Apr 2026 00:03:49 +0900 Subject: [PATCH] feat(SceneQueryRunner): cancel in-flight queries when panel scrolls out of view --- .../scenes/src/querying/SceneQueryRunner.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/scenes/src/querying/SceneQueryRunner.ts b/packages/scenes/src/querying/SceneQueryRunner.ts index 82c4ba8dc..fae71293d 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.ts @@ -126,6 +126,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen private _isInView = true; private _bypassIsInView = false; private _queryNotExecutedWhenOutOfView = false; + private _isQueryInFlight = false; public getResultsStream() { return this._results; @@ -274,6 +275,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen * Check if value changed is an adhoc filter or group by variable that did not exist when we issued the last query */ private onAnyVariableChanged(variable: SceneVariable) { + // If this variable was already detected as a dependency, onVariableUpdatesCompleted will handle value changes if ( this._drilldownDependenciesManager.adHocFiltersVar === variable || this._drilldownDependenciesManager.groupByVar === variable || @@ -339,6 +341,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen this._querySub.unsubscribe(); this._querySub = undefined; } + this._isQueryInFlight = false; if (this._dataLayersSub) { this._dataLayersSub.unsubscribe(); @@ -414,6 +417,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen public cancelQuery() { this._querySub?.unsubscribe(); + this._isQueryInFlight = false; if (this._dataLayersSub) { this._dataLayersSub.unsubscribe(); @@ -437,6 +441,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen } this._queryNotExecutedWhenOutOfView = false; + this._isQueryInFlight = true; // If data layers subscription doesn't exist, create one if (!this._dataLayersSub) { @@ -467,6 +472,13 @@ export class SceneQueryRunner extends SceneObjectBase implemen const datasource = this.state.datasource ?? findFirstDatasource(queries); const ds = await getDataSource(datasource, this._scopedVars); + // Re-check viewport after async getDataSource: the panel may have scrolled out of view during the await. + if (this.isQueryModeAuto() && !this._isInView && !this._bypassIsInView) { + this._isQueryInFlight = false; + this._queryNotExecutedWhenOutOfView = true; + return; + } + this._drilldownDependenciesManager.findAndSubscribeToDrilldowns(ds.uid, this); const runRequest = getRunRequest(); @@ -502,6 +514,14 @@ export class SceneQueryRunner extends SceneObjectBase implemen ); this._querySub = stream.subscribe(this.onDataReceived); + + // Re-check viewport after subscribe: the panel may have scrolled out of view while we were setting up the stream. + if (this.isQueryModeAuto() && !this._isInView && !this._bypassIsInView) { + this._querySub.unsubscribe(); + this._querySub = undefined; + this._isQueryInFlight = false; + this._queryNotExecutedWhenOutOfView = true; + } } catch (err) { console.error('PanelQueryRunner Error', err); @@ -656,6 +676,10 @@ export class SceneQueryRunner extends SceneObjectBase implemen hasFetchedData = true; } + if (preProcessedData.state !== LoadingState.Loading) { + this._isQueryInFlight = false; + } + this.setState({ data: dataWithLayersApplied, _hasFetchedData: hasFetchedData }); this._results.next({ origin: this, data: dataWithLayersApplied }); }; @@ -718,6 +742,15 @@ export class SceneQueryRunner extends SceneObjectBase implemen if (isInView && this._queryNotExecutedWhenOutOfView) { this.runQueries(); } + + // Cancel running query when panel leaves the viewport + if (!isInView && this._isQueryInFlight) { + writeSceneLog('SceneQueryRunner', 'Cancelling query for out-of-view panel', this.state.key); + this._querySub?.unsubscribe(); + this._querySub = undefined; + this._isQueryInFlight = false; + this._queryNotExecutedWhenOutOfView = true; + } } public bypassIsInViewChanged(bypassIsInView: boolean): void {