Visual redesign: design system, refined ops console aesthetic

- Full component library (Button, Input, Label, Select, Modal, Table, Badge, Card, etc.)
- Tailwind design tokens: IBM Plex Sans + JetBrains Mono, teal accent, semantic status colors
- NavBar with logo, responsive hamburger menu, real logout
- All pages redesigned: Login, Dashboard (KPI cards), Machines, SyncPairs, JobHistory, JobDetail, SSHKeys, Settings
- Fixed: hover:bg-gray-750 dead class, window.location.href navigation bug
- Replaced alert()/confirm() with sonner toasts and accessible modals
- Added ErrorBoundary, skip link, accessible modal dialogs (Radix)
- Icons: lucide-react throughout, copy/download buttons
- 1.0.5 → 1.0.6
This commit is contained in:
2026-07-07 23:12:34 -04:00
parent 93c22e844e
commit 9cef7173cb
36 changed files with 4069 additions and 652 deletions
+147 -61
View File
@@ -1,18 +1,33 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Server, Activity, HardDrive, Clock, Plus } from 'lucide-react';
import { api, Machine, Job } from '../api/client';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Card, CardBody } from '@/components/ui/Card';
import { PageHeader } from '@/components/ui/PageHeader';
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/Table';
import { EmptyState } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton';
import { statusVariant, statusLabel } from '@/lib/status';
import { formatRelativeTime } from '@/lib/utils';
export default function Dashboard() {
const [machines, setMachines] = useState<Machine[]>([]);
const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
api<Machine[]>('/api/machines'),
api<Job[]>('/api/jobs?limit=5'),
]).then(([m, j]) => {
setMachines(m);
setJobs(j);
}).catch(() => {});
])
.then(([m, j]) => {
setMachines(m);
setJobs(j);
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const online = machines.filter(m => m.status.startsWith('online')).length;
@@ -20,67 +35,138 @@ export default function Dashboard() {
if (!j.started_at) return false;
return j.started_at.startsWith(new Date().toISOString().split('T')[0]);
}).length;
const runningJobs = jobs.filter(j =>
['running', 'queued', 'waking_up'].includes(j.status)
).length;
const kpis = [
{
label: 'Total Machines',
value: machines.length,
icon: Server,
className: 'text-sky-400',
bgClass: 'bg-sky-500/10',
},
{
label: 'Online',
value: online,
icon: Activity,
className: 'text-emerald-400',
bgClass: 'bg-emerald-500/10',
accent: online > 0,
},
{
label: 'Jobs Today',
value: todayJobs,
icon: Clock,
className: 'text-amber-400',
bgClass: 'bg-amber-500/10',
},
{
label: 'Running',
value: runningJobs,
icon: HardDrive,
className: 'text-accent',
bgClass: 'bg-accent/10',
},
];
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
<div className="grid grid-cols-3 gap-4 mb-8">
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-gray-400 text-sm">Machines</div>
<div className="text-3xl font-bold">{machines.length}</div>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-gray-400 text-sm">Online</div>
<div className="text-3xl font-bold text-green-500">{online}</div>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-gray-400 text-sm">Jobs Today</div>
<div className="text-3xl font-bold text-blue-500">{todayJobs}</div>
</div>
<div className="space-y-6">
<PageHeader
title="Dashboard"
description="Overview of your sync infrastructure"
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{loading
? Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardBody>
<Skeleton className="h-4 w-20 mb-3" />
<Skeleton className="h-8 w-12" />
</CardBody>
</Card>
))
: kpis.map(kpi => {
const Icon = kpi.icon;
return (
<Card key={kpi.label} className="transition-shadow hover:shadow-card-hover">
<CardBody>
<div className="flex items-start justify-between mb-3">
<span className="text-xs font-medium text-fg-muted uppercase tracking-wider">
{kpi.label}
</span>
<div className={`rounded-card p-1.5 ${kpi.bgClass}`}>
<Icon className={`h-3.5 w-3.5 ${kpi.className}`} />
</div>
</div>
<p className={`text-3xl font-bold ${kpi.className}`}>
{kpi.value}
</p>
</CardBody>
</Card>
);
})}
</div>
<div className="bg-gray-800 rounded-lg p-4">
<h2 className="text-lg font-semibold mb-3">Recent Jobs</h2>
{jobs.length === 0 ? <p className="text-gray-500">No jobs yet</p> : (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-400 border-b border-gray-700">
<th className="pb-2">ID</th>
<th className="pb-2">Sync Pair</th>
<th className="pb-2">Status</th>
<th className="pb-2">Started</th>
</tr>
</thead>
<tbody>
{jobs.map(j => (
<tr key={j.id} className="border-b border-gray-700/50">
<td className="py-2">{j.id}</td>
<td className="py-2">{j.sync_pair_id}</td>
<td className="py-2">
<StatusBadge status={j.status} />
</td>
<td className="py-2">{j.started_at ? new Date(j.started_at).toLocaleString() : '-'}</td>
</tr>
<Card>
<div className="p-5 pb-0 flex items-center justify-between">
<h2 className="text-base font-semibold text-fg">Recent Jobs</h2>
<Button variant="ghost" size="sm" asChild>
<Link to="/jobs">View all</Link>
</Button>
</div>
<div className="p-5">
{loading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</tbody>
</table>
)}
</div>
</div>
) : jobs.length === 0 ? (
<EmptyState
icon={<HardDrive className="h-5 w-5" />}
title="No jobs yet"
description="Sync pairs will appear here once jobs are executed"
/>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Sync Pair</TableHead>
<TableHead>Status</TableHead>
<TableHead>Started</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jobs.map(j => (
<TableRow key={j.id}>
<TableCell>
<Link
to={`/jobs/${j.id}`}
className="text-accent hover:text-accent-hover font-mono text-xs"
>
#{j.id}
</Link>
</TableCell>
<TableCell className="text-fg-muted">
Pair {j.sync_pair_id}
</TableCell>
<TableCell>
<Badge variant={statusVariant(j.status)} label={statusLabel(j.status)} />
</TableCell>
<TableCell className="text-fg-muted text-xs">
{j.started_at ? formatRelativeTime(j.started_at) : '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</Card>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
queued: 'bg-gray-600',
waking_up: 'bg-yellow-600',
running: 'bg-blue-600',
success: 'bg-green-600',
failed: 'bg-red-600',
cancelled: 'bg-gray-600',
};
return (
<span className={`${colors[status] || 'bg-gray-600'} text-white text-xs px-2 py-0.5 rounded`}>
{status}
</span>
);
}