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 [createStatus, setCreateStatus] = useState<CreateStatus>("idle");
const [createError, setCreateError] = useState<string | null>(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<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 () => {
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 ? (
<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) => (
<div className="card session-card" key={session.id}>
<div className="session-info">
+56
View File
@@ -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;