From d1be2e4951877b721c00381b5dbd6bf0cbb2b807 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sat, 23 May 2026 07:54:52 +0200 Subject: [PATCH] feat: add loading indicators for long-running operations Add loading overlay to sessions list during create, stop, delete, and recreate tunnel operations. Show progress messages like 'Creating instance...' and 'Starting container...' during creation. Dim the sessions grid while operations are in progress to prevent user confusion and accidental duplicate actions. --- apps/web/src/pages/sessions.tsx | 41 ++++++++++++++++++++++-- apps/web/src/styles.css | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx index 0d7b9c2..9a4573b 100644 --- a/apps/web/src/pages/sessions.tsx +++ b/apps/web/src/pages/sessions.tsx @@ -38,6 +38,7 @@ export const SessionsPage = () => { const [displayName, setDisplayName] = useState(""); const [createStatus, setCreateStatus] = useState("idle"); const [createError, setCreateError] = useState(null); + const [createProgress, setCreateProgress] = useState(""); const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount"); const [branch, setBranch] = useState("main"); @@ -60,6 +61,8 @@ export const SessionsPage = () => { }>>({}); const [recreatingId, setRecreatingId] = useState(null); const [expandedProbeId, setExpandedProbeId] = useState(null); + const [loadingSessionId, setLoadingSessionId] = useState(null); + const [loadingAction, setLoadingAction] = useState(""); const loadSessions = useCallback(async () => { setStatus("loading"); @@ -206,6 +209,7 @@ export const SessionsPage = () => { } setCreateStatus("creating"); + setCreateProgress("Creating instance..."); try { const instance = await createInstance( selectedProject, @@ -216,11 +220,13 @@ export const SessionsPage = () => { cloneMode === "clone" ? branch : undefined ); + setCreateProgress("Starting container..."); // Auto-start the instance await startInstance(selectedProject, selectedRepo, instance.id); await updateUserConfig({ last_session_id: instance.id }); setCreateStatus("idle"); + setCreateProgress(""); setSelectedProject(""); setSelectedRepo(""); setSelectedToolType(""); @@ -230,6 +236,7 @@ export const SessionsPage = () => { await loadSessions(); } catch (error) { setCreateStatus("error"); + setCreateProgress(""); const axiosError = error as { response?: { data?: { detail?: string } } }; const message = axiosError.response?.data?.detail; setCreateError( @@ -239,16 +246,23 @@ export const SessionsPage = () => { }; const handleStop = async (sessionId: string, projectId: string, repoId: string) => { + setLoadingSessionId(sessionId); + setLoadingAction("Stopping..."); try { await stopInstance(projectId, repoId, sessionId); setStopConfirmId(null); await loadSessions(); } catch { setStopConfirmId(null); + } finally { + setLoadingSessionId(null); + setLoadingAction(""); } }; const handleDelete = async (sessionId: string, projectId: string, repoId: string, force = false) => { + setLoadingSessionId(sessionId); + setLoadingAction("Deleting..."); try { await deleteInstance(projectId, repoId, sessionId, force); setDeleteConfirmId(null); @@ -268,11 +282,15 @@ export const SessionsPage = () => { } } setDeleteConfirmId(null); + } finally { + setLoadingSessionId(null); + setLoadingAction(""); } }; const handleRecreateTunnel = async (session: Session) => { - setRecreatingId(session.id); + setLoadingSessionId(session.id); + setLoadingAction("Recreating tunnel..."); try { await recreateInstanceTunnel( session.project_id, @@ -284,7 +302,8 @@ export const SessionsPage = () => { } catch { // ignore } finally { - setRecreatingId(null); + setLoadingSessionId(null); + setLoadingAction(""); } }; @@ -379,7 +398,23 @@ export const SessionsPage = () => { {activeSessions.length === 0 ? (

No active sessions

) : ( -
+
+ {createStatus === "creating" && ( +
+
+ +

{createProgress || "Creating session..."}

+
+
+ )} + {loadingSessionId && ( +
+
+ +

{loadingAction || "Processing..."}

+
+
+ )} {activeSessions.map((session) => (
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index d7221cf..6131118 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2717,6 +2717,62 @@ a.nav-item, margin-bottom: var(--space-6); } +/* Loading overlay for sessions */ +.sessions-grid { + position: relative; +} + +.sessions-grid.dimmed { + opacity: 0.5; + pointer-events: none; +} + +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + background: rgba(255, 254, 249, 0.7); + border-radius: var(--space-2); +} + +.loading-content { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + padding: var(--space-6); + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--space-2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.loading-content .icon { + animation: spin 1s linear infinite; + color: var(--brand); +} + +.loading-content p { + margin: 0; + font-size: var(--font-size-base); + color: var(--muted); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + .recent-sessions-list { display: flex; flex-direction: column;