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
This commit is contained in:
@@ -43,6 +43,9 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
const [profileSelectInstanceId, setProfileSelectInstanceId] = useState<string | null>(null);
|
const [profileSelectInstanceId, setProfileSelectInstanceId] = useState<string | null>(null);
|
||||||
const [selectedProfileForAction, setSelectedProfileForAction] = useState("");
|
const [selectedProfileForAction, setSelectedProfileForAction] = useState("");
|
||||||
|
|
||||||
|
// Per-instance busy state for actions
|
||||||
|
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadInstances = useCallback(async () => {
|
const loadInstances = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -104,6 +107,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
const handleStart = async (instanceId: string, configProfileId?: string) => {
|
const handleStart = async (instanceId: string, configProfileId?: string) => {
|
||||||
|
setBusyInstanceId(instanceId);
|
||||||
try {
|
try {
|
||||||
await startInstance(projectId, repoId, instanceId, configProfileId);
|
await startInstance(projectId, repoId, instanceId, configProfileId);
|
||||||
setProfileSelectInstanceId(null);
|
setProfileSelectInstanceId(null);
|
||||||
@@ -111,20 +115,26 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
await loadInstances();
|
await loadInstances();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to start instance");
|
setError("Failed to start instance");
|
||||||
|
} finally {
|
||||||
|
setBusyInstanceId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStop = async (instanceId: string) => {
|
const handleStop = async (instanceId: string) => {
|
||||||
|
setBusyInstanceId(instanceId);
|
||||||
try {
|
try {
|
||||||
await stopInstance(projectId, repoId, instanceId);
|
await stopInstance(projectId, repoId, instanceId);
|
||||||
setStopConfirmId(null);
|
setStopConfirmId(null);
|
||||||
await loadInstances();
|
await loadInstances();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to stop instance");
|
setError("Failed to stop instance");
|
||||||
|
} finally {
|
||||||
|
setBusyInstanceId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestart = async (instanceId: string, configProfileId?: string) => {
|
const handleRestart = async (instanceId: string, configProfileId?: string) => {
|
||||||
|
setBusyInstanceId(instanceId);
|
||||||
try {
|
try {
|
||||||
await restartInstance(projectId, repoId, instanceId, configProfileId);
|
await restartInstance(projectId, repoId, instanceId, configProfileId);
|
||||||
setProfileSelectInstanceId(null);
|
setProfileSelectInstanceId(null);
|
||||||
@@ -132,26 +142,34 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
await loadInstances();
|
await loadInstances();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to restart instance");
|
setError("Failed to restart instance");
|
||||||
|
} finally {
|
||||||
|
setBusyInstanceId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (instanceId: string) => {
|
const handleDelete = async (instanceId: string) => {
|
||||||
if (!confirm("Are you sure you want to delete this instance?")) return;
|
if (!confirm("Are you sure you want to delete this instance?")) return;
|
||||||
|
setBusyInstanceId(instanceId);
|
||||||
try {
|
try {
|
||||||
await deleteInstance(projectId, repoId, instanceId);
|
await deleteInstance(projectId, repoId, instanceId);
|
||||||
// Update state immediately instead of reloading
|
// Update state immediately instead of reloading
|
||||||
setInstances(prev => prev.filter(i => i.id !== instanceId));
|
setInstances(prev => prev.filter(i => i.id !== instanceId));
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to delete instance");
|
setError("Failed to delete instance");
|
||||||
|
} finally {
|
||||||
|
setBusyInstanceId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRecreateTunnel = async (instanceId: string) => {
|
const handleRecreateTunnel = async (instanceId: string) => {
|
||||||
|
setBusyInstanceId(instanceId);
|
||||||
try {
|
try {
|
||||||
await recreateInstanceTunnel(projectId, repoId, instanceId);
|
await recreateInstanceTunnel(projectId, repoId, instanceId);
|
||||||
await loadInstances();
|
await loadInstances();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to recreate tunnel");
|
setError("Failed to recreate tunnel");
|
||||||
|
} finally {
|
||||||
|
setBusyInstanceId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +220,13 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
) : (
|
) : (
|
||||||
<div className="instance-grid">
|
<div className="instance-grid">
|
||||||
{instances.map((instance) => (
|
{instances.map((instance) => (
|
||||||
<div key={instance.id} className="instance-card">
|
<div key={instance.id} className={`instance-card ${busyInstanceId === instance.id ? "busy" : ""}`}>
|
||||||
|
{busyInstanceId === instance.id && (
|
||||||
|
<div className="instance-busy-overlay">
|
||||||
|
<Icon name="loading" size="md" />
|
||||||
|
<span>Working...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="instance-info">
|
<div className="instance-info">
|
||||||
<div className="instance-name">{instance.display_name}</div>
|
<div className="instance-name">{instance.display_name}</div>
|
||||||
<div className="instance-meta">
|
<div className="instance-meta">
|
||||||
@@ -244,6 +268,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
onClick={() => void handleRecreateTunnel(instance.id)}
|
onClick={() => void handleRecreateTunnel(instance.id)}
|
||||||
type="button"
|
type="button"
|
||||||
title="Recreate tunnel"
|
title="Recreate tunnel"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="refresh" size="sm" />
|
<Icon name="refresh" size="sm" />
|
||||||
Fix Tunnel
|
Fix Tunnel
|
||||||
@@ -256,6 +281,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="secondary-button small"
|
className="secondary-button small"
|
||||||
onClick={() => navigate(`/instances/${instance.id}/terminal`)}
|
onClick={() => navigate(`/instances/${instance.id}/terminal`)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="terminal" size="sm" />
|
<Icon name="terminal" size="sm" />
|
||||||
Terminal
|
Terminal
|
||||||
@@ -280,6 +306,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="primary-button small"
|
className="primary-button small"
|
||||||
onClick={() => void handleStart(instance.id, selectedProfileForAction || undefined)}
|
onClick={() => void handleStart(instance.id, selectedProfileForAction || undefined)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="play" size="sm" />
|
<Icon name="play" size="sm" />
|
||||||
Start
|
Start
|
||||||
@@ -291,6 +318,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
setSelectedProfileForAction("");
|
setSelectedProfileForAction("");
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -307,6 +335,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="play" size="sm" />
|
<Icon name="play" size="sm" />
|
||||||
Start
|
Start
|
||||||
@@ -323,6 +352,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="ghost-button small danger-text"
|
className="ghost-button small danger-text"
|
||||||
onClick={() => void handleStop(instance.id)}
|
onClick={() => void handleStop(instance.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
Yes
|
Yes
|
||||||
</button>
|
</button>
|
||||||
@@ -330,6 +360,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="ghost-button small"
|
className="ghost-button small"
|
||||||
onClick={() => setStopConfirmId(null)}
|
onClick={() => setStopConfirmId(null)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
@@ -339,6 +370,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="ghost-button small"
|
className="ghost-button small"
|
||||||
onClick={() => setStopConfirmId(instance.id)}
|
onClick={() => setStopConfirmId(instance.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="stop" size="sm" />
|
<Icon name="stop" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -360,6 +392,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="primary-button small"
|
className="primary-button small"
|
||||||
onClick={() => void handleRestart(instance.id, selectedProfileForAction || undefined)}
|
onClick={() => void handleRestart(instance.id, selectedProfileForAction || undefined)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="refresh" size="sm" />
|
<Icon name="refresh" size="sm" />
|
||||||
Restart
|
Restart
|
||||||
@@ -371,6 +404,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
setSelectedProfileForAction("");
|
setSelectedProfileForAction("");
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -387,6 +421,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="refresh" size="sm" />
|
<Icon name="refresh" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -397,6 +432,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
|||||||
className="ghost-button small danger-text"
|
className="ghost-button small danger-text"
|
||||||
onClick={() => void handleDelete(instance.id)}
|
onClick={() => void handleDelete(instance.id)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={busyInstanceId === instance.id}
|
||||||
>
|
>
|
||||||
<Icon name="delete" size="sm" />
|
<Icon name="delete" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2449,6 +2449,7 @@ a.nav-item,
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-info {
|
.instance-info {
|
||||||
@@ -2481,6 +2482,24 @@ a.nav-item,
|
|||||||
align-items: center;
|
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
|
Terminal Styles
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
Reference in New Issue
Block a user