From 869efda2147504bae70dfdab4edfcea2d1b86d41 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Sun, 24 May 2026 19:20:21 +0000 Subject: [PATCH] fix: per-item loading animations in instance list - Replace full-screen loading with per-instance busy state - Add busy overlay with spinner to instance cards - Disable action buttons while instance is busy - Add CSS for visual dimming and overlay positioning Fixes add-config-profiles: instance UI polish --- apps/web/src/components/instance-list.tsx | 38 ++++++++++++++++++++++- apps/web/src/styles.css | 19 ++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/instance-list.tsx b/apps/web/src/components/instance-list.tsx index 2600829..bffddc8 100644 --- a/apps/web/src/components/instance-list.tsx +++ b/apps/web/src/components/instance-list.tsx @@ -43,6 +43,9 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp const [profileSelectInstanceId, setProfileSelectInstanceId] = useState(null); const [selectedProfileForAction, setSelectedProfileForAction] = useState(""); + // Per-instance busy state for actions + const [busyInstanceId, setBusyInstanceId] = useState(null); + const loadInstances = useCallback(async () => { setLoading(true); try { @@ -104,6 +107,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp }, [projectId]); const handleStart = async (instanceId: string, configProfileId?: string) => { + setBusyInstanceId(instanceId); try { await startInstance(projectId, repoId, instanceId, configProfileId); setProfileSelectInstanceId(null); @@ -111,20 +115,26 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp await loadInstances(); } catch { setError("Failed to start instance"); + } finally { + setBusyInstanceId(null); } }; const handleStop = async (instanceId: string) => { + setBusyInstanceId(instanceId); try { await stopInstance(projectId, repoId, instanceId); setStopConfirmId(null); await loadInstances(); } catch { setError("Failed to stop instance"); + } finally { + setBusyInstanceId(null); } }; const handleRestart = async (instanceId: string, configProfileId?: string) => { + setBusyInstanceId(instanceId); try { await restartInstance(projectId, repoId, instanceId, configProfileId); setProfileSelectInstanceId(null); @@ -132,26 +142,34 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp await loadInstances(); } catch { setError("Failed to restart instance"); + } finally { + setBusyInstanceId(null); } }; const handleDelete = async (instanceId: string) => { if (!confirm("Are you sure you want to delete this instance?")) return; + setBusyInstanceId(instanceId); try { await deleteInstance(projectId, repoId, instanceId); // Update state immediately instead of reloading setInstances(prev => prev.filter(i => i.id !== instanceId)); } catch { setError("Failed to delete instance"); + } finally { + setBusyInstanceId(null); } }; const handleRecreateTunnel = async (instanceId: string) => { + setBusyInstanceId(instanceId); try { await recreateInstanceTunnel(projectId, repoId, instanceId); await loadInstances(); } catch { setError("Failed to recreate tunnel"); + } finally { + setBusyInstanceId(null); } }; @@ -202,7 +220,13 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp ) : (
{instances.map((instance) => ( -
+
+ {busyInstanceId === instance.id && ( +
+ + Working... +
+ )}
{instance.display_name}
@@ -244,6 +268,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp onClick={() => void handleRecreateTunnel(instance.id)} type="button" title="Recreate tunnel" + disabled={busyInstanceId === instance.id} > Fix Tunnel @@ -256,6 +281,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="secondary-button small" onClick={() => navigate(`/instances/${instance.id}/terminal`)} type="button" + disabled={busyInstanceId === instance.id} > Terminal @@ -280,6 +306,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="primary-button small" onClick={() => void handleStart(instance.id, selectedProfileForAction || undefined)} type="button" + disabled={busyInstanceId === instance.id} > Start @@ -291,6 +318,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp setSelectedProfileForAction(""); }} type="button" + disabled={busyInstanceId === instance.id} > Cancel @@ -307,6 +335,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp setSelectedProfileForAction(instance.selected_config_profile_id || ""); }} type="button" + disabled={busyInstanceId === instance.id} > Start @@ -323,6 +352,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="ghost-button small danger-text" onClick={() => void handleStop(instance.id)} type="button" + disabled={busyInstanceId === instance.id} > Yes @@ -330,6 +360,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="ghost-button small" onClick={() => setStopConfirmId(null)} type="button" + disabled={busyInstanceId === instance.id} > No @@ -339,6 +370,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="ghost-button small" onClick={() => setStopConfirmId(instance.id)} type="button" + disabled={busyInstanceId === instance.id} > @@ -360,6 +392,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="primary-button small" onClick={() => void handleRestart(instance.id, selectedProfileForAction || undefined)} type="button" + disabled={busyInstanceId === instance.id} > Restart @@ -371,6 +404,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp setSelectedProfileForAction(""); }} type="button" + disabled={busyInstanceId === instance.id} > Cancel @@ -387,6 +421,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp setSelectedProfileForAction(instance.selected_config_profile_id || ""); }} type="button" + disabled={busyInstanceId === instance.id} > @@ -397,6 +432,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp className="ghost-button small danger-text" onClick={() => void handleDelete(instance.id)} type="button" + disabled={busyInstanceId === instance.id} > diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index bdf8cbc..0589f53 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -2449,6 +2449,7 @@ a.nav-item, background: var(--bg); border: 1px solid var(--border); border-radius: 10px; + position: relative; } .instance-info { @@ -2481,6 +2482,24 @@ a.nav-item, align-items: center; } +.instance-card.busy { + opacity: 0.7; +} + +.instance-busy-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + background: rgba(var(--bg-rgb, 255, 255, 255), 0.8); + border-radius: 10px; + z-index: 1; + font-size: var(--text-sm); + color: var(--muted); +} + /* ============================================ Terminal Styles ============================================ */