6f18dd24a5
- Add OIDC authentication with PKCE flow - Create dashboard shell with sidebar and header - Implement project management UI (list, create, detail) - Add tool spawn page with tool/project selection - Create tool instance detail page with status and controls - Set up React Router with route guards - Add Zustand auth store and API client with types
223 lines
7.2 KiB
TypeScript
223 lines
7.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
|
import { api } from '../api/client'
|
|
import type { ToolInstance, ToolDefinition } from '../types/api'
|
|
|
|
export default function ToolInstanceDetailPage() {
|
|
const { projectId, instanceId } = useParams<{ projectId: string; instanceId: string }>()
|
|
const navigate = useNavigate()
|
|
const [instance, setInstance] = useState<ToolInstance | null>(null)
|
|
const [toolDef, setToolDef] = useState<ToolDefinition | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [actionLoading, setActionLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!projectId || !instanceId) return
|
|
loadInstance()
|
|
}, [projectId, instanceId])
|
|
|
|
const loadInstance = async () => {
|
|
if (!projectId || !instanceId) return
|
|
try {
|
|
setLoading(true)
|
|
const data = await api.getToolInstance(projectId, instanceId)
|
|
setInstance(data)
|
|
const registry = await api.getToolRegistry()
|
|
const tool = registry.find((t) => t.id === data.tool_definition_id)
|
|
setToolDef(tool || null)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load instance')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleStop = async () => {
|
|
if (!projectId || !instanceId) return
|
|
setActionLoading(true)
|
|
try {
|
|
const data = await api.stopToolInstance(projectId, instanceId)
|
|
setInstance(data)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to stop instance')
|
|
} finally {
|
|
setActionLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleStart = async () => {
|
|
if (!projectId || !instanceId) return
|
|
setActionLoading(true)
|
|
try {
|
|
const data = await api.startToolInstance(projectId, instanceId)
|
|
setInstance(data)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to start instance')
|
|
} finally {
|
|
setActionLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (!projectId || !instanceId) return
|
|
if (!confirm('Are you sure you want to delete this instance?')) return
|
|
setActionLoading(true)
|
|
try {
|
|
await api.deleteToolInstance(projectId, instanceId)
|
|
navigate(`/projects/${projectId}`)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to delete instance')
|
|
} finally {
|
|
setActionLoading(false)
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'running':
|
|
return 'text-green-400'
|
|
case 'creating':
|
|
return 'text-yellow-400'
|
|
case 'stopped':
|
|
return 'text-gray-400'
|
|
case 'error':
|
|
return 'text-red-400'
|
|
default:
|
|
return 'text-gray-400'
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="animate-pulse">Loading instance...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !instance) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="text-red-400 mb-4">{error || 'Instance not found'}</div>
|
|
<Link
|
|
to={`/projects/${projectId}`}
|
|
className="text-accent hover:underline"
|
|
>
|
|
← Back to project
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-6">
|
|
<Link
|
|
to={`/projects/${projectId}`}
|
|
className="text-accent hover:underline mb-4 inline-block"
|
|
>
|
|
← Back to project
|
|
</Link>
|
|
<h1 className="text-3xl font-bold mt-2">{instance.name}</h1>
|
|
<div className="flex items-center gap-4 mt-2">
|
|
<span className={`font-medium ${getStatusColor(instance.status)}`}>
|
|
{instance.status}
|
|
</span>
|
|
{toolDef && (
|
|
<span className="text-gray-400">{toolDef.name}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-lg mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="bg-surface border border-border rounded-lg p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Instance Details</h2>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<span className="text-gray-400">Status: </span>
|
|
<span className={getStatusColor(instance.status)}>{instance.status}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-400">Tool: </span>
|
|
<span>{toolDef?.name || instance.tool_definition_id}</span>
|
|
</div>
|
|
{instance.subdomain && (
|
|
<div>
|
|
<span className="text-gray-400">Subdomain: </span>
|
|
<a
|
|
href={`https://${instance.subdomain}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-accent hover:underline"
|
|
>
|
|
{instance.subdomain}
|
|
</a>
|
|
</div>
|
|
)}
|
|
{instance.container_id && (
|
|
<div>
|
|
<span className="text-gray-400">Container: </span>
|
|
<span className="font-mono text-sm">{instance.container_id.slice(0, 12)}</span>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="text-gray-400">Created: </span>
|
|
<span>{new Date(instance.created_at).toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-surface border border-border rounded-lg p-6">
|
|
<h2 className="text-lg font-semibold mb-4">Actions</h2>
|
|
<div className="space-y-3">
|
|
{instance.status === 'running' && instance.subdomain && (
|
|
<a
|
|
href={`https://${instance.subdomain}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="block w-full bg-accent text-white text-center py-2 px-4 rounded-lg hover:bg-accent/90 transition-colors"
|
|
>
|
|
Open Tool
|
|
</a>
|
|
)}
|
|
|
|
{instance.status === 'running' ? (
|
|
<button
|
|
onClick={handleStop}
|
|
disabled={actionLoading}
|
|
className="w-full bg-yellow-600 text-white py-2 px-4 rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{actionLoading ? 'Stopping...' : 'Stop Instance'}
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={handleStart}
|
|
disabled={actionLoading}
|
|
className="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{actionLoading ? 'Starting...' : 'Start Instance'}
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={actionLoading}
|
|
className="w-full bg-red-600 text-white py-2 px-4 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{actionLoading ? 'Deleting...' : 'Delete Instance'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|