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.
This commit is contained in:
Fusion
2026-05-23 07:54:52 +02:00
parent 953ea05756
commit d1be2e4951
2 changed files with 94 additions and 3 deletions
+38 -3
View File
@@ -38,6 +38,7 @@ export const SessionsPage = () => {
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [createStatus, setCreateStatus] = useState<CreateStatus>("idle"); const [createStatus, setCreateStatus] = useState<CreateStatus>("idle");
const [createError, setCreateError] = useState<string | null>(null); const [createError, setCreateError] = useState<string | null>(null);
const [createProgress, setCreateProgress] = useState("");
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount"); const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
const [branch, setBranch] = useState("main"); const [branch, setBranch] = useState("main");
@@ -60,6 +61,8 @@ export const SessionsPage = () => {
}>>({}); }>>({});
const [recreatingId, setRecreatingId] = useState<string | null>(null); const [recreatingId, setRecreatingId] = useState<string | null>(null);
const [expandedProbeId, setExpandedProbeId] = useState<string | null>(null); const [expandedProbeId, setExpandedProbeId] = useState<string | null>(null);
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
const [loadingAction, setLoadingAction] = useState<string>("");
const loadSessions = useCallback(async () => { const loadSessions = useCallback(async () => {
setStatus("loading"); setStatus("loading");
@@ -206,6 +209,7 @@ export const SessionsPage = () => {
} }
setCreateStatus("creating"); setCreateStatus("creating");
setCreateProgress("Creating instance...");
try { try {
const instance = await createInstance( const instance = await createInstance(
selectedProject, selectedProject,
@@ -216,11 +220,13 @@ export const SessionsPage = () => {
cloneMode === "clone" ? branch : undefined cloneMode === "clone" ? branch : undefined
); );
setCreateProgress("Starting container...");
// Auto-start the instance // Auto-start the instance
await startInstance(selectedProject, selectedRepo, instance.id); await startInstance(selectedProject, selectedRepo, instance.id);
await updateUserConfig({ last_session_id: instance.id }); await updateUserConfig({ last_session_id: instance.id });
setCreateStatus("idle"); setCreateStatus("idle");
setCreateProgress("");
setSelectedProject(""); setSelectedProject("");
setSelectedRepo(""); setSelectedRepo("");
setSelectedToolType(""); setSelectedToolType("");
@@ -230,6 +236,7 @@ export const SessionsPage = () => {
await loadSessions(); await loadSessions();
} catch (error) { } catch (error) {
setCreateStatus("error"); setCreateStatus("error");
setCreateProgress("");
const axiosError = error as { response?: { data?: { detail?: string } } }; const axiosError = error as { response?: { data?: { detail?: string } } };
const message = axiosError.response?.data?.detail; const message = axiosError.response?.data?.detail;
setCreateError( setCreateError(
@@ -239,16 +246,23 @@ export const SessionsPage = () => {
}; };
const handleStop = async (sessionId: string, projectId: string, repoId: string) => { const handleStop = async (sessionId: string, projectId: string, repoId: string) => {
setLoadingSessionId(sessionId);
setLoadingAction("Stopping...");
try { try {
await stopInstance(projectId, repoId, sessionId); await stopInstance(projectId, repoId, sessionId);
setStopConfirmId(null); setStopConfirmId(null);
await loadSessions(); await loadSessions();
} catch { } catch {
setStopConfirmId(null); setStopConfirmId(null);
} finally {
setLoadingSessionId(null);
setLoadingAction("");
} }
}; };
const handleDelete = async (sessionId: string, projectId: string, repoId: string, force = false) => { const handleDelete = async (sessionId: string, projectId: string, repoId: string, force = false) => {
setLoadingSessionId(sessionId);
setLoadingAction("Deleting...");
try { try {
await deleteInstance(projectId, repoId, sessionId, force); await deleteInstance(projectId, repoId, sessionId, force);
setDeleteConfirmId(null); setDeleteConfirmId(null);
@@ -268,11 +282,15 @@ export const SessionsPage = () => {
} }
} }
setDeleteConfirmId(null); setDeleteConfirmId(null);
} finally {
setLoadingSessionId(null);
setLoadingAction("");
} }
}; };
const handleRecreateTunnel = async (session: Session) => { const handleRecreateTunnel = async (session: Session) => {
setRecreatingId(session.id); setLoadingSessionId(session.id);
setLoadingAction("Recreating tunnel...");
try { try {
await recreateInstanceTunnel( await recreateInstanceTunnel(
session.project_id, session.project_id,
@@ -284,7 +302,8 @@ export const SessionsPage = () => {
} catch { } catch {
// ignore // ignore
} finally { } finally {
setRecreatingId(null); setLoadingSessionId(null);
setLoadingAction("");
} }
}; };
@@ -379,7 +398,23 @@ export const SessionsPage = () => {
{activeSessions.length === 0 ? ( {activeSessions.length === 0 ? (
<p className="muted">No active sessions</p> <p className="muted">No active sessions</p>
) : ( ) : (
<div className="sessions-grid"> <div className={`sessions-grid ${createStatus === "creating" || loadingSessionId ? "dimmed" : ""}`}>
{createStatus === "creating" && (
<div className="loading-overlay">
<div className="loading-content">
<Icon name="loading" size="lg" />
<p>{createProgress || "Creating session..."}</p>
</div>
</div>
)}
{loadingSessionId && (
<div className="loading-overlay">
<div className="loading-content">
<Icon name="loading" size="lg" />
<p>{loadingAction || "Processing..."}</p>
</div>
</div>
)}
{activeSessions.map((session) => ( {activeSessions.map((session) => (
<div className="card session-card" key={session.id}> <div className="card session-card" key={session.id}>
<div className="session-info"> <div className="session-info">
+56
View File
@@ -2717,6 +2717,62 @@ a.nav-item,
margin-bottom: var(--space-6); 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 { .recent-sessions-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;