diff --git a/src/documentdb/utils/connectionStringHelpers.ts b/src/documentdb/utils/connectionStringHelpers.ts index f3a29249..eade39ee 100644 --- a/src/documentdb/utils/connectionStringHelpers.ts +++ b/src/documentdb/utils/connectionStringHelpers.ts @@ -19,8 +19,13 @@ import { DocumentDBConnectionString } from './DocumentDBConnectionString'; * @returns The text with credentials replaced by `` */ export const redactCredentialsFromConnectionString = (text: string): string => { - // Matches the credentials portion (everything before the last '@' that follows the scheme) + // Matches the credentials portion (user:password before the '@' delimiter) // in mongodb:// or mongodb+srv:// URIs. + // + // Note: Per RFC 3986, the '@' character in usernames or passwords MUST be + // percent-encoded as '%40'. This regex relies on that assumption — it stops + // at the first literal '@' after the scheme, which is the credential delimiter + // in any spec-compliant URI. Unencoded '@' in passwords is malformed input. return text.replace(/(mongodb(?:\+srv)?:\/\/)[^\s@]*@/gi, '$1@'); }; diff --git a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx index ac3fe7eb..8485ff0c 100644 --- a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx +++ b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx @@ -109,10 +109,22 @@ export const QueryInsightsMain = (): JSX.Element => { // AbortController ref for cancelling in-flight Stage 3 AI requests const stage3AbortControllerRef = useRef(null); + // Timer ref for the delayed tips/error card shown during Stage 3 loading + const stage3TipsTimerRef = useRef | null>(null); + // Feedback dialog state const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false); const [feedbackSentiment, setFeedbackSentiment] = useState<'positive' | 'negative'>('positive'); + useEffect(() => { + return () => { + if (stage3TipsTimerRef.current !== null) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + }; + }, []); + /** * Display error message to user for the given stage * Only displays once per error state to avoid duplicate toasts @@ -452,13 +464,19 @@ export const QueryInsightsMain = (): JSX.Element => { // Transition to Stage 3 loading (this will reset UI flags) transitionToStage(3, 'loading'); + // Clear any pending tips/error card timer from a previous request + if (stage3TipsTimerRef.current) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + // Check if Stage 2 has query execution errors const hasExecutionError = queryInsightsState.stage2Data?.concerns && queryInsightsState.stage2Data.concerns.some((concern) => concern.includes('Query Execution Failed')); // Show appropriate card after 1 second delay - const timer = setTimeout(() => { + stage3TipsTimerRef.current = setTimeout(() => { if (hasExecutionError) { setShowErrorCard(true); } else { @@ -550,11 +568,15 @@ export const QueryInsightsMain = (): JSX.Element => { ...prev, stage3Promise: promise, })); - - return () => clearTimeout(timer); }; const handleCancelAI = () => { + // Clear any pending tips/error card timer to prevent stale UI after cancel + if (stage3TipsTimerRef.current) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + // Abort the in-flight tRPC request so the server can stop work early if (stage3AbortControllerRef.current) { stage3AbortControllerRef.current.abort();