refactor: centralize types and extract seed data (Task 1.1)
- Create types/ directory with canonical domain type definitions - session.ts, tool-instance.ts, tool-type.ts, git-repository.ts - config-folder.ts, tool-config.ts, project.ts, user.ts, api-response.ts - Move inline types from api modules to types/ with backward-compatible re-exports - Update all consumers (pages, components, state) to import from types/ - Extract seed_builtin_tool_types from main.py to seeds/builtin_tool_types.py - Ensure Session, ToolInstance, ToolType, GitRepository defined exactly once Quality gates: tsc (pass), eslint (pass), Python syntax (pass) Refs: repo-restructure Task 1.1
This commit is contained in:
@@ -4,356 +4,385 @@ import { Icon } from "./icon";
|
||||
import type { ToolInstance } from "../types/tool-instance";
|
||||
import type { ToolType } from "../types/tool-type";
|
||||
import {
|
||||
checkInstanceHealth,
|
||||
createInstance,
|
||||
deleteInstance,
|
||||
listInstances,
|
||||
recreateInstanceTunnel,
|
||||
restartInstance,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
checkInstanceHealth,
|
||||
createInstance,
|
||||
deleteInstance,
|
||||
listInstances,
|
||||
recreateInstanceTunnel,
|
||||
restartInstance,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
} from "../api/sessions";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
interface InstanceListProps {
|
||||
projectId: string;
|
||||
repoId: string;
|
||||
toolTypes: ToolType[];
|
||||
projectId: string;
|
||||
repoId: string;
|
||||
toolTypes: ToolType[];
|
||||
}
|
||||
|
||||
export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [instances, setInstances] = useState<ToolInstance[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [selectedToolType, setSelectedToolType] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Stop confirmation
|
||||
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
|
||||
|
||||
// Health check state
|
||||
const [healthStatus, setHealthStatus] = useState<Record<string, { healthy: boolean; lastCheck: number }>>({});
|
||||
export const InstanceList = ({
|
||||
projectId,
|
||||
repoId,
|
||||
toolTypes,
|
||||
}: InstanceListProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [instances, setInstances] = useState<ToolInstance[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [selectedToolType, setSelectedToolType] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadInstances = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await listInstances(projectId, repoId);
|
||||
setInstances(data);
|
||||
} catch {
|
||||
setError("Failed to load instances");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, repoId]);
|
||||
// Stop confirmation
|
||||
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void loadInstances();
|
||||
}, [loadInstances]);
|
||||
// Health check state
|
||||
const [healthStatus, setHealthStatus] = useState<
|
||||
Record<string, { healthy: boolean; lastCheck: number }>
|
||||
>({});
|
||||
|
||||
// Health check polling
|
||||
useEffect(() => {
|
||||
const runningInstances = instances.filter(i => i.status === "running" && i.url?.startsWith("http"));
|
||||
if (runningInstances.length === 0) return;
|
||||
const loadInstances = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await listInstances(projectId, repoId);
|
||||
setInstances(data);
|
||||
} catch {
|
||||
setError("Failed to load instances");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, repoId]);
|
||||
|
||||
const checkHealth = async () => {
|
||||
for (const instance of runningInstances) {
|
||||
try {
|
||||
const health = await checkInstanceHealth(projectId, repoId, instance.id);
|
||||
setHealthStatus(prev => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: health.healthy, lastCheck: Date.now() }
|
||||
}));
|
||||
} catch {
|
||||
setHealthStatus(prev => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: false, lastCheck: Date.now() }
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
void loadInstances();
|
||||
}, [loadInstances]);
|
||||
|
||||
// Check immediately
|
||||
void checkHealth();
|
||||
|
||||
// Then every 30 seconds
|
||||
const interval = setInterval(() => void checkHealth(), 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [instances, projectId, repoId]);
|
||||
// Health check polling
|
||||
useEffect(() => {
|
||||
const runningInstances = instances.filter(
|
||||
(i) => i.status === "running" && i.url?.startsWith("http"),
|
||||
);
|
||||
if (runningInstances.length === 0) return;
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!selectedToolType) return;
|
||||
setError(null);
|
||||
try {
|
||||
await createInstance(projectId, repoId, selectedToolType, displayName || undefined);
|
||||
setShowCreate(false);
|
||||
setSelectedToolType("");
|
||||
setDisplayName("");
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to create instance");
|
||||
}
|
||||
};
|
||||
const checkHealth = async () => {
|
||||
for (const instance of runningInstances) {
|
||||
try {
|
||||
const health = await checkInstanceHealth(
|
||||
projectId,
|
||||
repoId,
|
||||
instance.id,
|
||||
);
|
||||
setHealthStatus((prev) => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: health.healthy, lastCheck: Date.now() },
|
||||
}));
|
||||
} catch {
|
||||
setHealthStatus((prev) => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: false, lastCheck: Date.now() },
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStart = async (instanceId: string) => {
|
||||
try {
|
||||
await startInstance(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to start instance");
|
||||
}
|
||||
};
|
||||
// Check immediately
|
||||
void checkHealth();
|
||||
|
||||
const handleStop = async (instanceId: string) => {
|
||||
try {
|
||||
await stopInstance(projectId, repoId, instanceId);
|
||||
setStopConfirmId(null);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to stop instance");
|
||||
}
|
||||
};
|
||||
// Then every 30 seconds
|
||||
const interval = setInterval(() => void checkHealth(), 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [instances, projectId, repoId]);
|
||||
|
||||
const handleRestart = async (instanceId: string) => {
|
||||
try {
|
||||
await restartInstance(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to restart instance");
|
||||
}
|
||||
};
|
||||
const handleCreate = async () => {
|
||||
if (!selectedToolType) return;
|
||||
setError(null);
|
||||
try {
|
||||
await createInstance(
|
||||
projectId,
|
||||
repoId,
|
||||
selectedToolType,
|
||||
displayName || undefined,
|
||||
);
|
||||
setShowCreate(false);
|
||||
setSelectedToolType("");
|
||||
setDisplayName("");
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to create instance");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (instanceId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this instance?")) return;
|
||||
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");
|
||||
}
|
||||
};
|
||||
const handleStart = async (instanceId: string) => {
|
||||
try {
|
||||
await startInstance(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to start instance");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecreateTunnel = async (instanceId: string) => {
|
||||
try {
|
||||
await recreateInstanceTunnel(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to recreate tunnel");
|
||||
}
|
||||
};
|
||||
const handleStop = async (instanceId: string) => {
|
||||
try {
|
||||
await stopInstance(projectId, repoId, instanceId);
|
||||
setStopConfirmId(null);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to stop instance");
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "var(--success)";
|
||||
case "error":
|
||||
return "var(--danger)";
|
||||
case "pending":
|
||||
case "building":
|
||||
return "var(--warning)";
|
||||
default:
|
||||
return "var(--muted)";
|
||||
}
|
||||
};
|
||||
const handleRestart = async (instanceId: string) => {
|
||||
try {
|
||||
await restartInstance(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to restart instance");
|
||||
}
|
||||
};
|
||||
|
||||
const isTunnelUnhealthy = (instance: ToolInstance) => {
|
||||
if (instance.status !== "running") return false;
|
||||
if (!instance.url?.startsWith("http")) return false;
|
||||
const health = healthStatus[instance.id];
|
||||
if (!health) return false;
|
||||
return !health.healthy;
|
||||
};
|
||||
const handleDelete = async (instanceId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this instance?")) return;
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="instance-list">
|
||||
<div className="instance-list-header">
|
||||
<h3>Tool Instances</h3>
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => setShowCreate(true)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="add" size="sm" />
|
||||
Launch Tool
|
||||
</button>
|
||||
</div>
|
||||
const handleRecreateTunnel = async (instanceId: string) => {
|
||||
try {
|
||||
await recreateInstanceTunnel(projectId, repoId, instanceId);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to recreate tunnel");
|
||||
}
|
||||
};
|
||||
|
||||
{error && (
|
||||
<div className="error-message">{error}</div>
|
||||
)}
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "var(--success)";
|
||||
case "error":
|
||||
return "var(--danger)";
|
||||
case "pending":
|
||||
case "building":
|
||||
return "var(--warning)";
|
||||
default:
|
||||
return "var(--muted)";
|
||||
}
|
||||
};
|
||||
|
||||
{loading ? (
|
||||
<p className="muted">Loading instances...</p>
|
||||
) : instances.length === 0 ? (
|
||||
<p className="muted">No instances yet. Launch a tool to get started.</p>
|
||||
) : (
|
||||
<div className="instance-grid">
|
||||
{instances.map((instance) => (
|
||||
<div key={instance.id} className="instance-card">
|
||||
<div className="instance-info">
|
||||
<div className="instance-name">{instance.display_name || instance.tool_type_name || "Unnamed Instance"}</div>
|
||||
<div className="instance-meta">
|
||||
<span
|
||||
className="status-dot"
|
||||
style={{ backgroundColor: getStatusColor(instance.status) }}
|
||||
/>
|
||||
{instance.status}
|
||||
{isTunnelUnhealthy(instance) && (
|
||||
<span className="error-badge" title="Tunnel unreachable">
|
||||
<Icon name="warning" size="sm" />
|
||||
tunnel error
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="instance-actions">
|
||||
{instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && (
|
||||
<>
|
||||
<a
|
||||
href={instance.url.startsWith("http") ? instance.url : `${API_BASE_URL}${instance.url}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button small"
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
Open
|
||||
</a>
|
||||
{isTunnelUnhealthy(instance) && (
|
||||
<button
|
||||
className="secondary-button small warning"
|
||||
onClick={() => void handleRecreateTunnel(instance.id)}
|
||||
type="button"
|
||||
title="Recreate tunnel"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Fix Tunnel
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => navigate(`/instances/${instance.id}/terminal`)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="terminal" size="sm" />
|
||||
Terminal
|
||||
</button>
|
||||
)}
|
||||
{instance.status !== "running" && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => void handleStart(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
{instance.status === "running" && (
|
||||
<>
|
||||
{stopConfirmId === instance.id ? (
|
||||
<div className="inline-confirm">
|
||||
<span>Stop?</span>
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
onClick={() => void handleStop(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => setStopConfirmId(null)}
|
||||
type="button"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => setStopConfirmId(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="stop" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => void handleRestart(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
onClick={() => void handleDelete(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
const isTunnelUnhealthy = (instance: ToolInstance) => {
|
||||
if (instance.status !== "running") return false;
|
||||
if (!instance.url?.startsWith("http")) return false;
|
||||
const health = healthStatus[instance.id];
|
||||
if (!health) return false;
|
||||
return !health.healthy;
|
||||
};
|
||||
|
||||
{showCreate && (
|
||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||
<div className="dialog">
|
||||
<h2>Launch Tool</h2>
|
||||
<div className="stack">
|
||||
<label className="form-field">
|
||||
Tool Type
|
||||
<select
|
||||
value={selectedToolType}
|
||||
onChange={(e) => setSelectedToolType(e.target.value)}
|
||||
>
|
||||
<option value="">Select a tool...</option>
|
||||
{toolTypes.map((tool) => (
|
||||
<option key={tool.id} value={tool.id}>
|
||||
{tool.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
Display Name (optional)
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="My Development Environment"
|
||||
/>
|
||||
</label>
|
||||
<div className="dialog-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => setShowCreate(false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!selectedToolType}
|
||||
type="button"
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="instance-list">
|
||||
<div className="instance-list-header">
|
||||
<h3>Tool Instances</h3>
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => setShowCreate(true)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="add" size="sm" />
|
||||
Launch Tool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<p className="muted">Loading instances...</p>
|
||||
) : instances.length === 0 ? (
|
||||
<p className="muted">No instances yet. Launch a tool to get started.</p>
|
||||
) : (
|
||||
<div className="instance-grid">
|
||||
{instances.map((instance) => (
|
||||
<div key={instance.id} className="instance-card">
|
||||
<div className="instance-info">
|
||||
<div className="instance-name">
|
||||
{instance.display_name ||
|
||||
instance.tool_type_name ||
|
||||
"Unnamed Instance"}
|
||||
</div>
|
||||
<div className="instance-meta">
|
||||
<span
|
||||
className="status-dot"
|
||||
style={{ backgroundColor: getStatusColor(instance.status) }}
|
||||
/>
|
||||
{instance.status}
|
||||
{isTunnelUnhealthy(instance) && (
|
||||
<span className="error-badge" title="Tunnel unreachable">
|
||||
<Icon name="warning" size="sm" />
|
||||
tunnel error
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="instance-actions">
|
||||
{instance.status === "running" &&
|
||||
instance.url &&
|
||||
instance.tool_type_interfaces.includes("web") && (
|
||||
<>
|
||||
<a
|
||||
href={
|
||||
instance.url.startsWith("http")
|
||||
? instance.url
|
||||
: `${API_BASE_URL}${instance.url}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button small"
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
Open
|
||||
</a>
|
||||
{isTunnelUnhealthy(instance) && (
|
||||
<button
|
||||
className="secondary-button small warning"
|
||||
onClick={() => void handleRecreateTunnel(instance.id)}
|
||||
type="button"
|
||||
title="Recreate tunnel"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Fix Tunnel
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{instance.status === "running" &&
|
||||
instance.tool_type_interfaces.includes("terminal") && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() =>
|
||||
navigate(`/instances/${instance.id}/terminal`)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="terminal" size="sm" />
|
||||
Terminal
|
||||
</button>
|
||||
)}
|
||||
{instance.status !== "running" && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => void handleStart(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
{instance.status === "running" && (
|
||||
<>
|
||||
{stopConfirmId === instance.id ? (
|
||||
<div className="inline-confirm">
|
||||
<span>Stop?</span>
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
onClick={() => void handleStop(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => setStopConfirmId(null)}
|
||||
type="button"
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => setStopConfirmId(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="stop" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => void handleRestart(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
onClick={() => void handleDelete(instance.id)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||
<div className="dialog">
|
||||
<h2>Launch Tool</h2>
|
||||
<div className="stack">
|
||||
<label className="form-field">
|
||||
Tool Type
|
||||
<select
|
||||
value={selectedToolType}
|
||||
onChange={(e) => setSelectedToolType(e.target.value)}
|
||||
>
|
||||
<option value="">Select a tool...</option>
|
||||
{toolTypes.map((tool) => (
|
||||
<option key={tool.id} value={tool.id}>
|
||||
{tool.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
Display Name (optional)
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="My Development Environment"
|
||||
/>
|
||||
</label>
|
||||
<div className="dialog-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => setShowCreate(false)}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!selectedToolType}
|
||||
type="button"
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user