This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch what-if in repository https://gitbox.apache.org/repos/asf/superset.git
commit ab8144a5010f71df2d74a9019be23335518d67be Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Dec 18 14:32:18 2025 +0100 Abort signal for /interpret --- .../components/WhatIfDrawer/WhatIfAIInsights.tsx | 37 +++++++++++++-- .../dashboard/components/WhatIfDrawer/index.tsx | 53 ++++++++++++++++------ .../dashboard/components/WhatIfDrawer/whatIfApi.ts | 6 ++- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx index a845d771f3..9f4e1616ee 100644 --- a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx @@ -135,6 +135,17 @@ const WhatIfAIInsights = ({ const chartComparisons = useChartComparison(affectedChartIds); const allChartsLoaded = useAllChartsLoaded(affectedChartIds); + // AbortController for cancelling in-flight /interpret requests + const abortControllerRef = useRef<AbortController | null>(null); + + // Cleanup: cancel any pending requests on unmount + useEffect( + () => () => { + abortControllerRef.current?.abort(); + }, + [], + ); + // Track modification changes to reset status when user adjusts the slider const modificationsKey = getModificationsKey(modifications); const prevModificationsKeyRef = useRef<string>(modificationsKey); @@ -182,6 +193,8 @@ const WhatIfAIInsights = ({ console.log( '[WhatIfAIInsights] Modifications changed, resetting status to idle', ); + // Cancel any in-flight request when modifications change + abortControllerRef.current?.abort(); // eslint-disable-next-line react-hooks/set-state-in-effect -- Intentional: resetting state when modifications change setStatus('idle'); setResponse(null); @@ -194,18 +207,32 @@ const WhatIfAIInsights = ({ return; } + // Cancel any in-flight request before starting a new one + abortControllerRef.current?.abort(); + + // Create a new AbortController for this request + const abortController = new AbortController(); + abortControllerRef.current = abortController; + setStatus('loading'); setError(null); try { - const result = await fetchWhatIfInterpretation({ - modifications, - charts: chartComparisons, - dashboardName: dashboardTitle, - }); + const result = await fetchWhatIfInterpretation( + { + modifications, + charts: chartComparisons, + dashboardName: dashboardTitle, + }, + abortController.signal, + ); setResponse(result); setStatus('success'); } catch (err) { + // Don't update state if the request was aborted + if (err instanceof Error && err.name === 'AbortError') { + return; + } setError( err instanceof Error ? err.message diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx index a381f60f01..f3b0883d80 100644 --- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { t, logging } from '@superset-ui/core'; import { css, styled, Alert, useTheme } from '@apache-superset/core/ui'; @@ -210,6 +210,17 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { const [applyCounter, setApplyCounter] = useState(0); const [showAIReasoning, setShowAIReasoning] = useState(false); + // AbortController for cancelling in-flight /suggest_related requests + const suggestionsAbortControllerRef = useRef<AbortController | null>(null); + + // Cleanup: cancel any pending requests on unmount + useEffect( + () => () => { + suggestionsAbortControllerRef.current?.abort(); + }, + [], + ); + const slices = useSelector( (state: RootState) => state.sliceEntities.slices as { [id: number]: Slice }, ); @@ -251,6 +262,9 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { const handleApply = useCallback(async () => { if (!selectedColumn) return; + // Cancel any in-flight suggestions request + suggestionsAbortControllerRef.current?.abort(); + // Immediately clear previous results and increment counter to reset AI insights component setAppliedModifications([]); setAffectedChartIds([]); @@ -269,21 +283,28 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { // If cascading effects enabled, fetch AI suggestions if (enableCascadingEffects) { + // Create a new AbortController for this request + const abortController = new AbortController(); + suggestionsAbortControllerRef.current = abortController; + setIsLoadingSuggestions(true); try { - const suggestions = await fetchRelatedColumnSuggestions({ - selectedColumn, - userMultiplier: multiplier, - availableColumns: numericColumns.map(col => ({ - columnName: col.columnName, - description: col.description, - verboseName: col.verboseName, - datasourceId: col.datasourceId, - })), - dashboardName: dashboardInfo?.dash_edit_perm - ? dashboardInfo?.dashboard_title - : undefined, - }); + const suggestions = await fetchRelatedColumnSuggestions( + { + selectedColumn, + userMultiplier: multiplier, + availableColumns: numericColumns.map(col => ({ + columnName: col.columnName, + description: col.description, + verboseName: col.verboseName, + datasourceId: col.datasourceId, + })), + dashboardName: dashboardInfo?.dash_edit_perm + ? dashboardInfo?.dashboard_title + : undefined, + }, + abortController.signal, + ); // Add AI suggestions to modifications const aiModifications: ExtendedWhatIfModification[] = @@ -297,6 +318,10 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { allModifications = [...allModifications, ...aiModifications]; } catch (error) { + // Don't log or update state if the request was aborted + if (error instanceof Error && error.name === 'AbortError') { + return; + } logging.error('Failed to get AI suggestions:', error); // Continue with just user modification } diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts b/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts index 64f51601cb..cc1b247d93 100644 --- a/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SupersetClient } from '@superset-ui/core'; +import { SupersetClient, Signal } from '@superset-ui/core'; import { WhatIfInterpretRequest, WhatIfInterpretResponse, @@ -42,9 +42,11 @@ interface ApiResponse { export async function fetchWhatIfInterpretation( request: WhatIfInterpretRequest, + signal?: Signal, ): Promise<WhatIfInterpretResponse> { const response = await SupersetClient.post({ endpoint: '/api/v1/what_if/interpret', + signal, jsonPayload: { modifications: request.modifications.map(mod => ({ column: mod.column, @@ -102,9 +104,11 @@ interface ApiSuggestRelatedResponse { export async function fetchRelatedColumnSuggestions( request: WhatIfSuggestRelatedRequest, + signal?: Signal, ): Promise<WhatIfSuggestRelatedResponse> { const response = await SupersetClient.post({ endpoint: '/api/v1/what_if/suggest_related', + signal, jsonPayload: { selected_column: request.selectedColumn, user_multiplier: request.userMultiplier,
