blob: 2de0997f18ba57be18ae04af3950aaa0e4a3df13 (
plain) (
tree)
|
|
import React, { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
interface Daemon {
id: string
ownerId: string
connectionId: string
hostname: string | null
machineId: string | null
maxConcurrentTasks: number
currentTaskCount: number
status: string
lastHeartbeatAt: string
connectedAt: string
disconnectedAt: string | null
}
interface DaemonDirectory {
path: string
label: string
directoryType: string
hostname: string | null
exists?: boolean
}
type Tab = 'overview' | 'directories'
function statusIndicator(status: string): { color: string; label: string } {
switch (status.toLowerCase()) {
case 'connected':
return { color: 'green', label: 'Connected' }
case 'disconnected':
return { color: 'red', label: 'Disconnected' }
case 'unhealthy':
return { color: 'yellow', label: 'Unhealthy' }
default:
return { color: 'gray', label: status }
}
}
export function DaemonDetail() {
const { id } = useParams<{ id: string }>()
const [daemon, setDaemon] = useState<Daemon | null>(null)
const [directories, setDirectories] = useState<DaemonDirectory[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<Tab>('overview')
useEffect(() => {
async function fetchDaemon() {
if (!id) return
try {
setLoading(true)
const [daemonRes, dirRes] = await Promise.all([
fetch(`/api/v1/mesh/daemons/${id}`),
fetch('/api/v1/mesh/daemons/directories'),
])
if (!daemonRes.ok) {
throw new Error(`Failed to fetch daemon: ${daemonRes.statusText}`)
}
const daemonData = await daemonRes.json()
setDaemon(daemonData)
if (dirRes.ok) {
const dirData = await dirRes.json()
setDirectories(dirData.directories || [])
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}
fetchDaemon()
const interval = setInterval(async () => {
if (!id) return
try {
const response = await fetch(`/api/v1/mesh/daemons/${id}`)
if (response.ok) {
const daemonData = await response.json()
setDaemon(daemonData)
}
} catch {
// Silently ignore refresh errors
}
}, 10000)
return () => clearInterval(interval)
}, [id])
if (loading) {
return (
<div className="daemon-detail-container">
<div className="loading">Loading daemon...</div>
</div>
)
}
if (error) {
return (
<div className="daemon-detail-container">
<div className="error">Error: {error}</div>
<Link to="/daemons" className="back-link">
Back to Daemons
</Link>
</div>
)
}
if (!daemon) {
return (
<div className="daemon-detail-container">
<div className="not-found">Daemon not found</div>
<Link to="/daemons" className="back-link">
Back to Daemons
</Link>
</div>
)
}
const status = statusIndicator(daemon.status)
const filteredDirectories = directories.filter(
(dir) => dir.hostname === daemon.hostname
)
return (
<div className="daemon-detail-container">
<div className="daemon-detail-header">
<Link to="/daemons" className="back-link">
Back to Daemons
</Link>
<h1 className="daemon-title">{daemon.hostname || 'Unknown Host'}</h1>
<div className="daemon-meta">
<span>
<span
className="status-dot"
style={{ backgroundColor: status.color }}
/>
{status.label}
</span>
<span>
Tasks: {daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
</span>
</div>
</div>
<div className="daemon-tabs">
<button
className={`tab-button ${activeTab === 'overview' ? 'active' : ''}`}
onClick={() => setActiveTab('overview')}
>
Overview
</button>
<button
className={`tab-button ${activeTab === 'directories' ? 'active' : ''}`}
onClick={() => setActiveTab('directories')}
>
Directories ({filteredDirectories.length})
</button>
</div>
<div className="daemon-tab-content">
{activeTab === 'overview' && (
<div className="tab-panel">
<h2>Daemon Overview</h2>
<dl className="overview-list">
<dt>ID</dt>
<dd>{daemon.id}</dd>
<dt>Hostname</dt>
<dd>{daemon.hostname || 'N/A'}</dd>
<dt>Machine ID</dt>
<dd>{daemon.machineId || 'N/A'}</dd>
<dt>Status</dt>
<dd>
<span
className="status-dot"
style={{ backgroundColor: status.color }}
/>
{status.label}
</dd>
<dt>Connection ID</dt>
<dd>{daemon.connectionId}</dd>
<dt>Max Concurrent Tasks</dt>
<dd>{daemon.maxConcurrentTasks}</dd>
<dt>Current Task Count</dt>
<dd>{daemon.currentTaskCount}</dd>
<dt>Connected At</dt>
<dd>{new Date(daemon.connectedAt).toLocaleString()}</dd>
<dt>Last Heartbeat At</dt>
<dd>{new Date(daemon.lastHeartbeatAt).toLocaleString()}</dd>
<dt>Disconnected At</dt>
<dd>
{daemon.disconnectedAt
? new Date(daemon.disconnectedAt).toLocaleString()
: 'N/A'}
</dd>
</dl>
</div>
)}
{activeTab === 'directories' && (
<div className="tab-panel">
<h2>Directories</h2>
{filteredDirectories.length === 0 ? (
<p>No directories found for this daemon</p>
) : (
<ul className="directory-list">
{filteredDirectories.map((dir, index) => (
<li key={`${dir.path}-${index}`} className="directory-item">
<h3>{dir.label}</h3>
<div className="daemon-meta">
<span>Path: {dir.path}</span>
<span>Type: {dir.directoryType}</span>
</div>
</li>
))}
</ul>
)}
</div>
)}
</div>
</div>
)
}
|