summaryrefslogtreecommitdiff
path: root/frontend/src/components/DaemonDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/DaemonDetail.tsx')
-rw-r--r--frontend/src/components/DaemonDetail.tsx229
1 files changed, 229 insertions, 0 deletions
diff --git a/frontend/src/components/DaemonDetail.tsx b/frontend/src/components/DaemonDetail.tsx
new file mode 100644
index 0000000..2de0997
--- /dev/null
+++ b/frontend/src/components/DaemonDetail.tsx
@@ -0,0 +1,229 @@
+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>
+ )
+}