From e34f85ab1aac016a0537bd029b76c71ea88729bd Mon Sep 17 00:00:00 2001 From: Maqbool Ahmed Date: Mon, 13 Jul 2026 13:49:20 +0500 Subject: [PATCH] fix(ui): show spinner on Run Scan button while request is in flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #199. App.tsx set setScanStatus('running') and setView('scanning') simultaneously before await startScan() resolved, so the view switched to the scanning screen before any spinner was visible. IdleView had no spinner at all on its submit button. Changes ------- App.tsx Added isStarting state (true only while startScan() is in flight). setView('scanning') and setScanStatus('running') now fire after the request resolves, not before. isStarting is passed to both Sidebar and IdleView. Sidebar.tsx Accepts isStarting prop (optional, defaults false). Button is disabled and shows Loader2 spinner when isStarting OR isRunning. No new imports needed — Loader2 was already present. IdleView.tsx Accepts isStarting prop (optional, defaults false). Added Loader2 import. Submit button is disabled and shows Loader2 spinner while isStarting is true. --- frontend/src/components/App.tsx | 34 ++++++++++++---------- frontend/src/components/Sidebar.tsx | 11 +++---- frontend/src/components/views/IdleView.tsx | 11 +++---- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 9d7cde8..8ff3917 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -9,9 +9,9 @@ import { ResultsSkeleton } from './ResultsSkeleton'; import { ToolPanels } from './tools/ToolPanels'; import { ScanComparison } from './views/ScanComparison'; import { WatchlistView } from './views/WatchlistView'; -import { MODULE_MAP } from './Sidebar'; import { startScan, getWsUrl, getScan, getUsage } from '@/lib/api'; import { detectScanType, normalizeScanTarget } from '@/lib/scan-target'; +import { MODULE_MAP } from './Sidebar'; import type { ScanType, ScanStatus, ToolMode, ScanResults as ScanResultsType, ScanMeta, LiveModuleStatus, UsageData } from '@/lib/types'; type View = 'idle' | 'tool' | 'scanning' | 'results' | 'compare' | 'watchlist'; @@ -47,7 +47,9 @@ export function App() { const [totalModules, setTotalModules] = useState(0); const [compareIds, setCompareIds] = useState<[string, string] | null>(null); const [usage, setUsage] = useState(null); + const [isStarting, setIsStarting] = useState(false); const wsRef = useRef(null); + const urlScanStarted = useRef(false); const usageLimitedRef = useRef(true); const handleHome = useCallback(() => { @@ -228,29 +230,23 @@ export function App() { setProgressLog([]); setModuleStatuses({}); setTotalModules(modules.length); - setScanStatus('running'); - setView('scanning'); setScanMeta(null); + setIsStarting(true); try { const { scan_id } = await startScan(target, type, modules); + setIsStarting(false); + setScanStatus('running'); + setView('scanning'); setScanId(scan_id); connectWs(scan_id); handleUsageRefresh(); } catch (e: unknown) { + setIsStarting(false); setScanStatus('failed'); setProgressLog([`Failed to start scan: ${e instanceof Error ? e.message : 'Unknown error'}`]); } }, [connectWs, handleUsageRefresh]); - useEffect(() => { - handleUsageRefresh(); - }, [handleUsageRefresh]); - - useEffect(() => { - return () => { wsRef.current?.close(); }; - }, []); - - const urlScanStarted = useRef(false); useEffect(() => { if (urlScanStarted.current) return; try { @@ -265,14 +261,22 @@ export function App() { } catch {} }, [handleScan]); + useEffect(() => { + handleUsageRefresh(); + }, [handleUsageRefresh]); + + useEffect(() => { + return () => { wsRef.current?.close(); }; + }, []); + return (
setSidebarOpen(v => !v)} />
{sidebarOpen &&
setSidebarOpen(false)} className="fixed inset-0 bg-black/50 z-40 md:hidden" />} - setSidebarOpen(false)} /> + setSidebarOpen(false)} />
- {view === 'idle' && } + {view === 'idle' && } {view === 'tool' && toolMode && ( setView('idle')} /> )} @@ -293,4 +297,4 @@ export function App() {
); -} +} \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b131b2b..8396193 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -30,6 +30,7 @@ interface Props { onLoadScan?: (scanId: string) => void; onCompare?: (a: string, b: string) => void; isRunning: boolean; + isStarting?: boolean; isOpen: boolean; onClose: () => void; } @@ -55,7 +56,7 @@ function useRecentScans() { return { recents, add, clear }; } -export function Sidebar({ onScan, onLoadScan, onCompare, isRunning, isOpen, onClose }: Props) { +export function Sidebar({ onScan, onLoadScan, onCompare, isRunning, isStarting = false, isOpen, onClose }: Props) { const { t } = useTranslations(); const [target, setTarget] = useState(''); const [scanType, setScanType] = useState('domain'); @@ -211,11 +212,11 @@ export function Sidebar({ onScan, onLoadScan, onCompare, isRunning, isOpen, onCl @@ -367,4 +368,4 @@ export function Sidebar({ onScan, onLoadScan, onCompare, isRunning, isOpen, onCl
); -} +} \ No newline at end of file diff --git a/frontend/src/components/views/IdleView.tsx b/frontend/src/components/views/IdleView.tsx index 0ba5399..452ff8d 100644 --- a/frontend/src/components/views/IdleView.tsx +++ b/frontend/src/components/views/IdleView.tsx @@ -1,6 +1,6 @@ 'use client'; import { useEffect, useState } from 'react'; -import { FileText, Mail, Bitcoin, QrCode, ChevronRight, Globe, User, Phone, Shield, Database, Zap, Eye, Activity, Network, Hash, Binary, KeyRound, Search } from 'lucide-react'; +import { FileText, Mail, Bitcoin, QrCode, ChevronRight, Globe, User, Phone, Shield, Database, Zap, Eye, Activity, Network, Hash, Binary, KeyRound, Search, Loader2 } from 'lucide-react'; import { useTranslations } from '@/lib/i18n'; import { getPrismDemoMode } from '@/lib/prism-config'; import { detectScanType, normalizeScanTarget } from '@/lib/scan-target'; @@ -29,9 +29,10 @@ type CapsKey = typeof CAPS_KEYS[number]; interface Props { onTool: (mode: ToolMode) => void; onScan: (target: string, type: ScanType, modules: string[]) => void; + isStarting?: boolean; } -export function IdleView({ onTool, onScan }: Props) { +export function IdleView({ onTool, onScan, isStarting = false }: Props) { const { t } = useTranslations(); const demoMode = getPrismDemoMode(); const [targetIdx, setTargetIdx] = useState(0); @@ -110,11 +111,11 @@ export function IdleView({ onTool, onScan }: Props) { @@ -194,4 +195,4 @@ export function IdleView({ onTool, onScan }: Props) { )} ); -} +} \ No newline at end of file