|
|
|
@@ -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>
|
|
|
|
|