Files
Control-Center/frontend/src/pages/SessionsPage.tsx
Joshua 8b8cb8210c
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m11s
Dev Build / build-test (push) Successful in 2m18s
CUB-121: build React pages with real API integration
- HubPage: agent summary stats, cards, status badges, progress bars, refresh
- LogsPage: activity feed from tasks, status filter, loading skeleton
- ProjectsPage: project cards with status badges and agent counts
- SessionsPage: responsive table/card view with model/token info
- SettingsPage: dark mode toggle, gateway URL, refresh interval persist
- ThemeProvider with dark/light mode via CSS custom properties
- useLocalStorage hook for settings persistence
- Loading/error/empty states across all pages
- npm run build passes cleanly
2026-05-08 19:53:21 -04:00

178 lines
6.8 KiB
TypeScript

import { useQuery, useQueryClient } from '@tanstack/react-query'
import { listSessions } from '../services/api'
import { AlertCircle, Monitor, RefreshCw, Cpu, MessageSquare, Clock, Hash } from 'lucide-react'
export default function SessionsPage() {
const queryClient = useQueryClient()
const { data, isLoading, error } = useQuery({
queryKey: ['sessions'],
queryFn: listSessions,
})
const sessions = data?.data ?? []
if (isLoading) return <SessionsSkeleton />
if (error) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<AlertCircle size={48} className="text-danger" />
<p className="text-danger text-lg">Failed to load sessions</p>
<button
onClick={() => queryClient.invalidateQueries({ queryKey: ['sessions'] })}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
>
<RefreshCw size={16} /> Retry
</button>
</div>
)
}
return (
<div className="space-y-6">
<header>
<h1 className="text-2xl font-bold">Sessions</h1>
<p className="text-on-surface-variant">Active and recent agent sessions</p>
</header>
{sessions.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 gap-3 border border-dashed border-surface-light rounded-xl">
<Monitor size={40} className="text-on-surface-muted" />
<p className="text-on-surface-muted text-lg">No active sessions</p>
<p className="text-on-surface-muted text-sm">Sessions will appear when agents connect.</p>
</div>
) : (
<>
{/* Desktop: Table view */}
<div className="hidden lg:block overflow-x-auto rounded-xl border border-surface-light">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-light bg-surface-dark">
<th className="text-left p-4 font-medium text-on-surface-variant">Agent</th>
<th className="text-left p-4 font-medium text-on-surface-variant">Session Key</th>
<th className="text-left p-4 font-medium text-on-surface-variant">Channel</th>
<th className="text-left p-4 font-medium text-on-surface-variant">Model</th>
<th className="text-right p-4 font-medium text-on-surface-variant">Context Tokens</th>
<th className="text-right p-4 font-medium text-on-surface-variant">Started</th>
</tr>
</thead>
<tbody>
{sessions.map((s) => (
<tr
key={s.id}
className="border-b border-surface-light hover:bg-surface-dark/50 transition-colors"
>
<td className="p-4 font-medium">{s.agentId}</td>
<td className="p-4">
<code className="text-xs bg-surface-light px-2 py-1 rounded text-on-surface-variant">
{s.sessionKey}
</code>
</td>
<td className="p-4 text-on-surface-variant">{s.channel}</td>
<td className="p-4 text-on-surface-variant">{s.model}</td>
<td className="p-4 text-right tabular-nums text-on-surface-variant">
{s.contextTokens.toLocaleString()}
</td>
<td className="p-4 text-right text-on-surface-muted whitespace-nowrap">
{formatDateTime(s.startedAt)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: Card view */}
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-4">
{sessions.map((s) => (
<div
key={s.id}
className="p-4 rounded-xl border border-surface-light bg-surface-dark"
>
<div className="flex items-center gap-2 mb-3">
<Monitor size={16} className="text-primary" />
<span className="font-medium text-sm">{s.agentId}</span>
</div>
<div className="space-y-2 text-sm">
<Row icon={Hash} label="Session">{s.sessionKey}</Row>
<Row icon={MessageSquare} label="Channel">{s.channel}</Row>
<Row icon={Cpu} label="Model">{s.model}</Row>
<Row icon={Hash} label="Tokens">{s.contextTokens.toLocaleString()}</Row>
<Row icon={Clock} label="Started">{formatDateTime(s.startedAt)}</Row>
</div>
</div>
))}
</div>
</>
)}
</div>
)
}
function Row({ icon: Icon, label, children }: { icon: React.ElementType; label: string; children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<Icon size={14} className="text-on-surface-muted shrink-0" />
<span className="text-on-surface-muted text-xs w-14 shrink-0">{label}</span>
<span className="text-on-surface-variant truncate">{children}</span>
</div>
)
}
function formatDateTime(iso: string): string {
try {
const d = new Date(iso)
return d.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
})
} catch {
return iso
}
}
function SessionsSkeleton() {
return (
<div className="space-y-6 animate-pulse">
<div>
<div className="h-8 w-40 skeleton mb-2" />
<div className="h-4 w-56 skeleton" />
</div>
{/* Desktop skeleton */}
<div className="hidden lg:block rounded-xl border border-surface-light overflow-hidden">
<div className="bg-surface-dark p-4 border-b border-surface-light">
<div className="grid grid-cols-6 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-3 skeleton" />
))}
</div>
</div>
{[...Array(5)].map((_, i) => (
<div key={i} className="border-b border-surface-light p-4">
<div className="grid grid-cols-6 gap-4">
{[...Array(6)].map((_, j) => (
<div key={j} className="h-4 skeleton" />
))}
</div>
</div>
))}
</div>
{/* Mobile skeleton */}
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="p-4 rounded-xl border border-surface-light bg-surface-dark">
<div className="h-4 w-24 skeleton mb-3" />
{[...Array(5)].map((_, j) => (
<div key={j} className="h-4 w-full skeleton mb-2" />
))}
</div>
))}
</div>
</div>
)
}