Add dashboard shell with sidebar nav and version header.

Replace the health-check stub UI with a left menu and content panel so section navigation and frontend/backend versions are visible in the shell.
This commit is contained in:
2026-07-16 20:57:47 +02:00
parent 38b4e8fd43
commit d2b92401f1
7 changed files with 351 additions and 70 deletions
+107 -32
View File
@@ -1,43 +1,118 @@
import { useState } from 'react'
import { fetchHealth } from './api/client'
import { useEffect, useState } from 'react'
import { fetchStatus } from './api/client'
import './App.css'
type SectionId = 'overview' | 'vms' | 'containers' | 'configuration'
const NAV_ITEMS: ReadonlyArray<{ id: SectionId; label: string }> = [
{ id: 'overview', label: 'Overview' },
{ id: 'vms', label: 'VMs' },
{ id: 'containers', label: 'Containers' },
]
const SECTION_TITLES: Record<SectionId, string> = {
overview: 'Overview',
vms: 'VMs',
containers: 'Containers',
configuration: 'Configuration',
}
function CogIcon() {
return (
<svg
className="cog-icon"
viewBox="0 0 24 24"
width="18"
height="18"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96a7.2 7.2 0 0 0-1.62-.94l-.36-2.54A.48.48 0 0 0 13.9 2h-3.8a.48.48 0 0 0-.48.42l-.36 2.54c-.59.24-1.13.55-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.48a.49.49 0 0 0 .12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.86 14.52a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.3.59.22l2.39-.96c.49.39 1.03.7 1.62.94l.36 2.54c.05.24.24.42.48.42h3.8c.24 0 .43-.18.48-.42l.36-2.54c.59-.24 1.13-.55 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.49.49 0 0 0-.12-.61l-2.03-1.58ZM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7Z"
/>
</svg>
)
}
function App() {
const [healthStatus, setHealthStatus] = useState<string | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [activeSection, setActiveSection] = useState<SectionId>('overview')
const [backendVersion, setBackendVersion] = useState('…')
async function handleCheckHealth() {
setIsLoading(true)
setErrorMessage(null)
setHealthStatus(null)
useEffect(() => {
let isCancelled = false
try {
const health = await fetchHealth()
setHealthStatus(health.status)
} catch (error) {
const message =
error instanceof Error ? error.message : 'unknown health check error'
setErrorMessage(message)
} finally {
setIsLoading(false)
async function loadBackendVersion() {
try {
const status = await fetchStatus()
if (!isCancelled) {
setBackendVersion(status.version)
}
} catch {
if (!isCancelled) {
setBackendVersion('unavailable')
}
}
}
}
void loadBackendVersion()
return () => {
isCancelled = true
}
}, [])
return (
<main className="app">
<h1>ClusterCanvas</h1>
<p>Dashboard for Proxmox VMs and LXC containers.</p>
<button type="button" onClick={handleCheckHealth} disabled={isLoading}>
{isLoading ? 'Checking…' : 'Check service health'}
</button>
{healthStatus ? (
<p role="status">Service health: {healthStatus}</p>
) : null}
{errorMessage ? (
<p role="alert">Health check failed: {errorMessage}</p>
) : null}
</main>
<div className="app-shell">
<aside className="sidebar" aria-label="Main menu">
<div className="sidebar-header">
<h1 className="brand">Cluster Canvas</h1>
<p className="version-line">Frontend Version {__FRONTEND_VERSION__}</p>
<p className="version-line">Backend Version {backendVersion}</p>
</div>
<nav className="sidebar-nav" aria-label="Sections">
{NAV_ITEMS.map((item) => (
<button
key={item.id}
type="button"
className={
activeSection === item.id
? 'nav-item nav-item-active'
: 'nav-item'
}
aria-current={activeSection === item.id ? 'page' : undefined}
onClick={() => setActiveSection(item.id)}
>
{item.label}
</button>
))}
</nav>
<div className="sidebar-footer">
<button
type="button"
className={
activeSection === 'configuration'
? 'nav-item nav-item-active config-item'
: 'nav-item config-item'
}
aria-current={
activeSection === 'configuration' ? 'page' : undefined
}
onClick={() => setActiveSection('configuration')}
>
<CogIcon />
Configuration
</button>
</div>
</aside>
<main className="content" aria-label={SECTION_TITLES[activeSection]}>
<h2>{SECTION_TITLES[activeSection]}</h2>
<p>Coming soon</p>
</main>
</div>
)
}