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;