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:
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user