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 [selectedProfileForAction, setSelectedProfileForAction] = useState("");
|
||||
|
||||
// Per-instance busy state for actions
|
||||
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(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
|
||||
) : (
|
||||
<div className="instance-grid">
|
||||
{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-name">{instance.display_name}</div>
|
||||
<div className="instance-meta">
|
||||
@@ -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}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
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}
|
||||
>
|
||||
<Icon name="terminal" size="sm" />
|
||||
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}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
@@ -291,6 +318,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
||||
setSelectedProfileForAction("");
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -307,6 +335,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
||||
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
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
|
||||
</button>
|
||||
@@ -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
|
||||
</button>
|
||||
@@ -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}
|
||||
>
|
||||
<Icon name="stop" size="sm" />
|
||||
</button>
|
||||
@@ -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}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Restart
|
||||
@@ -371,6 +404,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
||||
setSelectedProfileForAction("");
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -387,6 +421,7 @@ export const InstanceList = ({ projectId, repoId, projectName, repoName, toolTyp
|
||||
setSelectedProfileForAction(instance.selected_config_profile_id || "");
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
</button>
|
||||
@@ -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}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user