Template
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
import { useState, useMemo } from 'react'
|
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/Table'
|
|
import { Badge } from '../ui/Badge'
|
|
import { Kbd } from '../ui/Kbd'
|
|
import { EmptyState, EmptyStateIcon, EmptyStateTitle, EmptyStateDescription } from '../ui/EmptyState'
|
|
import { Icon } from '../Icon'
|
|
|
|
type Service = {
|
|
id: string
|
|
name: string
|
|
region: string
|
|
status: 'healthy' | 'degraded' | 'scaling'
|
|
latency: string
|
|
memory: string
|
|
instances: number
|
|
}
|
|
|
|
const INITIAL_SERVICES: Service[] = [
|
|
{ id: '1', name: 'api-gateway', region: 'us-east-1', status: 'healthy', latency: '12ms', memory: '128 MB', instances: 8 },
|
|
{ id: '2', name: 'vector-indexer', region: 'eu-central-1', status: 'healthy', latency: '34ms', memory: '890 MB', instances: 4 },
|
|
{ id: '3', name: 'model-inference', region: 'us-west-2', status: 'scaling', latency: '78ms', memory: '4.2 GB', instances: 16 },
|
|
{ id: '4', name: 'session-broker', region: 'us-east-1', status: 'healthy', latency: '8ms', memory: '64 MB', instances: 6 },
|
|
{ id: '5', name: 'analytics-worker', region: 'ap-northeast-1', status: 'degraded', latency: '142ms', memory: '1.8 GB', instances: 2 },
|
|
{ id: '6', name: 'audit-logger', region: 'eu-west-1', status: 'healthy', latency: '19ms', memory: '210 MB', instances: 3 },
|
|
]
|
|
|
|
export function ServiceTable() {
|
|
const [search, setSearch] = useState('')
|
|
const [filterStatus, setFilterStatus] = useState<string>('all')
|
|
|
|
const filteredServices = useMemo(() => {
|
|
return INITIAL_SERVICES.filter(svc => {
|
|
const matchesSearch = svc.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
svc.region.toLowerCase().includes(search.toLowerCase())
|
|
const matchesStatus = filterStatus === 'all' || svc.status === filterStatus
|
|
return matchesSearch && matchesStatus
|
|
})
|
|
}, [search, filterStatus])
|
|
|
|
return (
|
|
<div className="stack-md">
|
|
{/* Table Toolbar */}
|
|
<div className="flex-between" style={{ flexWrap: 'wrap', gap: 'var(--space-3)' }}>
|
|
<div className="icon-search-bar" style={{ maxWidth: 320 }}>
|
|
<Icon name="search" size={15} />
|
|
<input
|
|
type="text"
|
|
placeholder="Filter services or regions..."
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
className="icon-search-input"
|
|
/>
|
|
{search && (
|
|
<button className="icon-clear-btn" onClick={() => setSearch('')}>
|
|
<Icon name="close" size={13} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="row-xs">
|
|
{['all', 'healthy', 'scaling', 'degraded'].map(st => (
|
|
<button
|
|
key={st}
|
|
className={`button ${filterStatus === st ? 'secondary' : 'ghost'}`}
|
|
style={{
|
|
padding: 'var(--space-1) var(--space-3)',
|
|
fontSize: 'var(--text-xs)',
|
|
textTransform: 'capitalize',
|
|
}}
|
|
onClick={() => setFilterStatus(st)}
|
|
>
|
|
{st}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table Surface */}
|
|
{filteredServices.length > 0 ? (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Service Name</TableHead>
|
|
<TableHead>Region</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>P99 Latency</TableHead>
|
|
<TableHead>Memory</TableHead>
|
|
<TableHead>Nodes</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredServices.map(svc => (
|
|
<TableRow key={svc.id}>
|
|
<TableCell style={{ fontWeight: 600 }}>
|
|
<div className="row-xs" style={{ alignItems: 'center' }}>
|
|
<span
|
|
style={{
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: '50%',
|
|
background: svc.status === 'healthy' ? 'var(--chart-2)' : svc.status === 'scaling' ? 'var(--primary)' : 'var(--destructive)',
|
|
boxShadow: `0 0 8px ${svc.status === 'healthy' ? 'var(--chart-2)' : svc.status === 'scaling' ? 'var(--primary)' : 'var(--destructive)'}`,
|
|
}}
|
|
/>
|
|
{svc.name}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Kbd>{svc.region}</Kbd>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={svc.status === 'healthy' ? 'positive' : svc.status === 'scaling' ? 'primary' : 'destructive'}
|
|
>
|
|
{svc.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="mono" style={{ color: svc.status === 'degraded' ? 'var(--destructive)' : 'var(--primary)' }}>
|
|
{svc.latency}
|
|
</TableCell>
|
|
<TableCell className="mono" style={{ color: 'var(--muted-foreground)' }}>
|
|
{svc.memory}
|
|
</TableCell>
|
|
<TableCell className="mono" style={{ color: 'var(--foreground)' }}>
|
|
{svc.instances}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
) : (
|
|
<EmptyState>
|
|
<EmptyStateIcon>
|
|
<Icon name="search" size={24} />
|
|
</EmptyStateIcon>
|
|
<EmptyStateTitle>No matching services</EmptyStateTitle>
|
|
<EmptyStateDescription>
|
|
No active nodes or services match the current filter query "{search}".
|
|
</EmptyStateDescription>
|
|
</EmptyState>
|
|
)}
|
|
|
|
<div className="flex-between muted" style={{ fontSize: 'var(--text-3xs)', padding: '0 var(--space-2)' }}>
|
|
<span>Showing {filteredServices.length} of {INITIAL_SERVICES.length} deployed services</span>
|
|
<span className="mono">Synced live via cluster coordinator</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|