Files
floci-ui/packages/frontend/src/pages/DiagnosticsPage.tsx
T
Zakaria 8b166d86d4
CI / checks (pull_request) Failing after 12m12s
feat(frontend): collapse responding services on the diagnostics page
Failing console services stay visible; responding ones are behind a toggle.
2026-10-23 12:00:10 -04:00

230 lines
12 KiB
TypeScript

import {useMemo, useState} from 'react'
import {Activity, RefreshCw} from 'lucide-react'
import {Link, Navigate, useParams} from 'react-router-dom'
import {useCloudDiagnosticsQuery} from '@/api/queries/cloudQueries'
import {formatDateTime, formatRelativeTime} from '@/lib/format'
import type {CloudProvider, CloudServiceStatus, EmulatedServiceStatus, RuntimeDiagnostics} from '@/types/cloud'
type ServiceFilter = 'all' | 'not-running' | 'with-page' | 'without-page'
const SERVICE_FILTERS: Array<{id: ServiceFilter; label: string}> = [
{id: 'all', label: 'All'},
{id: 'not-running', label: 'Not running'},
{id: 'with-page', label: 'Has console page'},
{id: 'without-page', label: 'No console page'},
]
export function DiagnosticsPage() {
const params = useParams()
const cloud = normalizeCloud(params.cloud)
const query = useCloudDiagnosticsQuery(cloud ?? 'aws')
if (!cloud) return <Navigate to="/diagnostics/aws" replace/>
const report = query.data
return (
<>
<div className="page-header cloud-explorer-header">
<div className="page-title">
<Activity size={20}/>
<div>
<h1>Diagnostics</h1>
<p className="muted">{report ? `Checked ${formatRelativeTime(report.checkedAt) ?? 'just now'}` : 'Runtime, services and console health'}</p>
</div>
</div>
<button className="button" type="button" disabled={query.isFetching} onClick={() => void query.refetch()}>
<RefreshCw size={14}/>{query.isFetching ? 'Checking' : 'Re-check'}
</button>
</div>
<div className="content diagnostics">
{query.isLoading && <p className="muted padded">Running diagnostics</p>}
{query.error && <p className="error-text">Could not load diagnostics: {query.error.message}</p>}
{report && <DiagnosticsReport report={report}/>}
</div>
</>
)
}
function DiagnosticsReport({report}: {report: RuntimeDiagnostics}) {
const emulated = report.emulatedServices
const notRunning = emulated?.filter((service) => service.state !== 'running').length ?? 0
const covered = emulated?.filter((service) => service.consolePages.length > 0).length ?? 0
const reachable = report.runtime.status === 'reachable'
return (
<>
<section className="console-summary" aria-label="Runtime summary">
<div className={`summary-tile ${reachable ? 'ready' : 'unavailable'}`}>
<span>Runtime</span>
<strong>{reachable ? 'Reachable' : 'Unavailable'}</strong>
<small>{report.runtime.endpoint ?? 'No endpoint'}{report.runtime.latencyMs !== null ? ` · ${report.runtime.latencyMs} ms` : ''}</small>
</div>
<div className="summary-tile">
<span>Version</span>
<strong>{report.runtime.version ?? 'Unknown'}</strong>
<small>{report.runtime.edition ? `${report.runtime.edition} edition` : 'Not reported by the runtime'}</small>
</div>
<div className={`summary-tile ${notRunning > 0 ? 'pending' : ''}`}>
<span>Emulated services</span>
<strong>{emulated ? emulated.length : '—'}</strong>
<small>{emulated ? (notRunning > 0 ? `${notRunning} not running` : 'All running') : 'Not reported by the runtime'}</small>
</div>
<div className="summary-tile">
<span>Console coverage</span>
<strong>{emulated ? `${covered} / ${emulated.length}` : `${report.consoleServices.length}`}</strong>
<small>{emulated ? 'Emulated services with a console page' : 'Console services'}</small>
</div>
</section>
{report.runtime.error && <p className="error-text">{report.runtime.error}</p>}
<ConsoleServicesTable services={report.consoleServices}/>
{emulated && <EmulatedServices services={emulated}/>}
<ContainersTable report={report}/>
<ConsoleProcess report={report}/>
</>
)
}
function ConsoleServicesTable({services}: {services: CloudServiceStatus[]}) {
const [showResponding, setShowResponding] = useState(false)
const failing = services.filter((service) => service.runtime !== 'reachable')
const responding = services.filter((service) => service.runtime === 'reachable')
const ordered = showResponding ? [...failing, ...responding] : failing
return (
<section className="table-panel" aria-labelledby="diagnostics-console-services">
<header className="diagnostics-section-header">
<h2 id="diagnostics-console-services">Console services</h2>
<div className="diagnostics-filters">
<span className="muted">{failing.length ? `${failing.length} of ${services.length} failing` : `All ${services.length} responding`}</span>
{responding.length > 0 && (
<button className="button compact" type="button" aria-expanded={showResponding} onClick={() => setShowResponding((value) => !value)}>
{showResponding ? 'Hide responding' : `Show ${responding.length} responding`}
</button>
)}
</div>
</header>
{services.length === 0 && <p className="muted padded">No console services are registered for this cloud.</p>}
{ordered.length > 0 && <table className="table">
<thead><tr><th>Service</th><th>Status</th><th>Latency</th><th>Detail</th></tr></thead>
<tbody>{ordered.map((service) => (
<tr key={service.service}>
<td><code>{service.service}</code></td>
<td><span className={`status ${statusClass(service.runtime)}`}>{statusLabel(service.runtime)}</span></td>
<td>{service.latencyMs === null ? '—' : `${service.latencyMs} ms`}</td>
<td>{service.error ? <span className="error-text compact-text">{service.error}{service.errorCode ? ` (${service.errorCode})` : ''}</span> : <span className="muted">—</span>}</td>
</tr>
))}</tbody>
</table>}
</section>
)
}
function EmulatedServices({services}: {services: EmulatedServiceStatus[]}) {
const [filter, setFilter] = useState<ServiceFilter>('all')
const [search, setSearch] = useState('')
const visible = useMemo(() => {
const term = search.trim().toLowerCase()
return services.filter((service) => {
if (term && !service.name.includes(term) && !service.consolePages.some((page) => page.displayName.toLowerCase().includes(term))) return false
if (filter === 'not-running') return service.state !== 'running'
if (filter === 'with-page') return service.consolePages.length > 0
if (filter === 'without-page') return service.consolePages.length === 0
return true
})
}, [services, filter, search])
return (
<section className="table-panel" aria-labelledby="diagnostics-emulated-services">
<header className="diagnostics-section-header">
<h2 id="diagnostics-emulated-services">Emulated services</h2>
<div className="diagnostics-filters">
<div className="diagnostics-filter-group" role="group" aria-label="Filter emulated services">
{SERVICE_FILTERS.map((option) => (
<button key={option.id} className={`button compact ${filter === option.id ? 'primary' : ''}`} type="button" aria-pressed={filter === option.id} onClick={() => setFilter(option.id)}>{option.label}</button>
))}
</div>
<input className="input" type="search" value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Filter services" aria-label="Filter emulated services by name"/>
</div>
</header>
{visible.length === 0
? <p className="muted padded">No emulated services match this filter.</p>
: <ul className="diagnostics-service-grid">{visible.map((service) => (
<li key={service.name} className="diagnostics-service">
<span className={`dot ${service.state === 'running' ? 'healthy' : 'unavailable'}`} aria-hidden="true"/>
<code>{service.name}</code>
{service.state !== 'running' && <span className="muted">{service.state}</span>}
{service.consolePages.map((page) => <Link key={page.service} className="diagnostics-page-link" to={page.route}>{page.displayName}</Link>)}
</li>
))}</ul>}
</section>
)
}
function ContainersTable({report}: {report: RuntimeDiagnostics}) {
const containers = report.containers
return (
<section className="table-panel" aria-labelledby="diagnostics-containers">
<header className="diagnostics-section-header">
<h2 id="diagnostics-containers">Runtime containers</h2>
<span className="muted">Containers started for EC2 instances</span>
</header>
{containers === null && <p className="muted padded">This runtime does not publish container diagnostics.</p>}
{containers?.length === 0 && <p className="muted padded">No EC2 instance containers are running.</p>}
{containers && containers.length > 0 && <table className="table">
<thead><tr><th>Instance</th><th>Container</th><th>SSH</th></tr></thead>
<tbody>{containers.map((container) => (
<tr key={container.resourceId}>
<td><Link to={`/cloud-explorer/${report.cloud}/compute?resource=${encodeURIComponent(container.resourceId)}`}><code>{container.resourceId}</code></Link></td>
<td>{container.status ?? '—'}</td>
<td>{container.sshPort ? <code>{container.sshHost ?? 'localhost'}:{container.sshPort}</code> : '—'}</td>
</tr>
))}</tbody>
</table>}
</section>
)
}
function ConsoleProcess({report}: {report: RuntimeDiagnostics}) {
return (
<section className="table-panel" aria-labelledby="diagnostics-console">
<header className="diagnostics-section-header">
<h2 id="diagnostics-console">Console</h2>
</header>
<dl className="diagnostics-facts">
<div><dt>Uptime</dt><dd>{formatUptime(report.console.uptimeSeconds)}</dd></div>
<div><dt>Started</dt><dd>{formatDateTime(report.console.startedAt) ?? report.console.startedAt}</dd></div>
<div><dt>Sign-in</dt><dd>{report.console.authMode === 'oidc' ? 'OIDC single sign-on' : 'Local administrator'}</dd></div>
<div><dt>Active sessions</dt><dd>{report.console.activeSessions}</dd></div>
</dl>
</section>
)
}
function formatUptime(totalSeconds: number): string {
const days = Math.floor(totalSeconds / 86_400)
const hours = Math.floor((totalSeconds % 86_400) / 3_600)
const minutes = Math.floor((totalSeconds % 3_600) / 60)
if (days > 0) return `${days} d ${hours} h`
if (hours > 0) return `${hours} h ${minutes} min`
if (minutes > 0) return `${minutes} min`
return `${totalSeconds} s`
}
function statusClass(runtime: CloudServiceStatus['runtime']): string {
if (runtime === 'reachable') return 'healthy'
if (runtime === 'coming_soon') return 'degraded'
return 'unavailable'
}
function statusLabel(runtime: CloudServiceStatus['runtime']): string {
if (runtime === 'reachable') return 'Reachable'
if (runtime === 'coming_soon') return 'Not registered'
return 'Unavailable'
}
function normalizeCloud(value?: string): CloudProvider | null {
return value === 'aws' || value === 'azure' || value === 'gcp' ? value : null
}