220 lines
8.7 KiB
TypeScript
220 lines
8.7 KiB
TypeScript
'use client'
|
|||
|
|
|
||
|
|
import { useCallback, useEffect, useState } from 'react'
|
||
|
|
import { Panel } from '@/components/cluster/Panel'
|
||
|
|
import { DEFAULT_KINDS, WAVES, apps, appsInWave, resourceKinds, type App } from '@/lib/clusterMock'
|
||
|
|
|
||
|
|
const TOTAL_RESOURCES = apps.reduce((n, a) => n + a.resources, 0)
|
||
|
|
|
||
|
|
/** Resource count as discrete cells, capped so a 68-resource app stays readable. */
|
||
|
|
function CountBar({ n }: { n: number }) {
|
||
|
|
const cells = Math.min(n, 24)
|
||
|
|
return (
|
||
|
|
<div className="mt-2 flex flex-wrap gap-[2px]" aria-label={`${n} resources`}>
|
||
|
|
{Array.from({ length: cells }, (_, i) => (
|
||
|
|
<span key={i} className="h-1.5 w-1.5 rounded-[1px] bg-wire-lit" />
|
||
|
|
))}
|
||
|
|
{n > 24 && <span className="ml-1 font-data text-[9px] leading-none text-dim">+{n - 24}</span>}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function AppCard({
|
||
|
|
app,
|
||
|
|
state,
|
||
|
|
active,
|
||
|
|
onSelect,
|
||
|
|
}: {
|
||
|
|
app: App
|
||
|
|
state: 'idle' | 'syncing' | 'done'
|
||
|
|
active: boolean
|
||
|
|
onSelect: () => void
|
||
|
|
}) {
|
||
|
|
const degraded = app.health === 'degraded'
|
||
|
|
const drifted = app.sync === 'outofsync'
|
||
|
|
|
||
|
|
const border = active
|
||
|
|
? 'border-lamp'
|
||
|
|
: state === 'syncing'
|
||
|
|
? 'border-lamp/70'
|
||
|
|
: degraded
|
||
|
|
? 'border-rose/50'
|
||
|
|
: drifted
|
||
|
|
? 'border-lamp/40'
|
||
|
|
: 'border-wire'
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
onClick={onSelect}
|
||
|
|
aria-pressed={active}
|
||
|
|
className={[
|
||
|
|
'w-full border bg-deck p-2.5 text-left transition-all duration-300',
|
||
|
|
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp',
|
||
|
|
border,
|
||
|
|
state === 'syncing' ? 'bg-riser' : '',
|
||
|
|
active ? 'bg-riser' : 'hover:border-wire-lit',
|
||
|
|
].join(' ')}
|
||
|
|
>
|
||
|
|
<div className="flex items-start justify-between gap-2">
|
||
|
|
<span className="font-data text-[11px] leading-tight text-chalk">{app.name}</span>
|
||
|
|
<span
|
||
|
|
className={[
|
||
|
|
'mt-1 h-1.5 w-1.5 shrink-0 rounded-full transition-colors',
|
||
|
|
state === 'syncing' ? 'bg-lamp' : degraded ? 'bg-rose' : drifted ? 'bg-lamp' : 'bg-flux',
|
||
|
|
].join(' ')}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<CountBar n={app.resources} />
|
||
|
|
{(degraded || drifted) && (
|
||
|
|
<p className={`mt-2 font-data text-[9px] uppercase tracking-[0.14em] ${degraded ? 'text-rose' : 'text-lamp'}`}>
|
||
|
|
{degraded ? 'degraded' : 'out of sync'}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function DeliveryPage() {
|
||
|
|
const [selected, setSelected] = useState<string | null>('prometheus')
|
||
|
|
const [front, setFront] = useState<number | null>(null)
|
||
|
|
|
||
|
|
const replay = useCallback(() => setFront(0), [])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (front === null) return
|
||
|
|
if (front > 8) {
|
||
|
|
const done = setTimeout(() => setFront(null), 700)
|
||
|
|
return () => clearTimeout(done)
|
||
|
|
}
|
||
|
|
const next = setTimeout(() => setFront((w) => (w === null ? null : w + 1)), 520)
|
||
|
|
return () => clearTimeout(next)
|
||
|
|
}, [front])
|
||
|
|
|
||
|
|
const selectedApp = apps.find((a) => a.name === selected) ?? null
|
||
|
|
const kinds = selectedApp ? (resourceKinds[selectedApp.name] ?? DEFAULT_KINDS) : []
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<header className="border border-wire bg-deck p-6 md:p-8">
|
||
|
|
<p className="font-data text-[10px] uppercase tracking-[0.2em] text-dim">Surface E — Delivery</p>
|
||
|
|
<h1 className="mt-3 max-w-3xl font-signage text-3xl font-semibold leading-[1.1] tracking-signage md:text-5xl">
|
||
|
|
Nothing starts until the thing it needs is already running.
|
||
|
|
</h1>
|
||
|
|
<p className="mt-5 max-w-2xl font-plex text-sm leading-relaxed text-dim">
|
||
|
|
{apps.length} applications, {TOTAL_RESOURCES} resources, applied in nine ordered waves. Certificates
|
||
|
|
before issuers, issuers before ingress, storage before the databases that sit on it. The order is the
|
||
|
|
design.
|
||
|
|
</p>
|
||
|
|
<button
|
||
|
|
onClick={replay}
|
||
|
|
disabled={front !== null}
|
||
|
|
className="mt-6 border border-lamp px-4 py-2 font-data text-[11px] uppercase tracking-[0.16em] text-lamp transition-colors hover:bg-lamp hover:text-void focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lamp disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-lamp"
|
||
|
|
>
|
||
|
|
{front !== null ? `syncing wave ${Math.min(front, 8)}` : 'Replay sync'}
|
||
|
|
</button>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div className="grid gap-6 xl:grid-cols-[1fr_340px]">
|
||
|
|
<Panel label="Sync waves" hint="0 → 8, left to right" className="min-w-0">
|
||
|
|
<div className="-mx-4 overflow-x-auto px-4 pb-2">
|
||
|
|
<div className="flex min-w-max gap-3">
|
||
|
|
{WAVES.map((wave) => {
|
||
|
|
const inWave = appsInWave(wave)
|
||
|
|
const state = front === null ? 'idle' : front === wave ? 'syncing' : front > wave ? 'done' : 'idle'
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div key={wave} className="w-[168px] shrink-0">
|
||
|
|
<div
|
||
|
|
className={[
|
||
|
|
'mb-3 flex items-baseline justify-between border-b pb-1.5 transition-colors',
|
||
|
|
state === 'syncing' ? 'border-lamp' : 'border-wire',
|
||
|
|
].join(' ')}
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
className={[
|
||
|
|
'font-signage text-2xl font-semibold leading-none transition-colors',
|
||
|
|
state === 'syncing' ? 'text-lamp' : inWave.length ? 'text-chalk' : 'text-wire-lit',
|
||
|
|
].join(' ')}
|
||
|
|
>
|
||
|
|
{wave}
|
||
|
|
</span>
|
||
|
|
<span className="font-data text-[9px] uppercase tracking-[0.14em] text-dim">
|
||
|
|
{inWave.length || '—'}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{inWave.length === 0 ? (
|
||
|
|
<p className="border border-dashed border-wire p-2.5 font-data text-[10px] leading-relaxed text-dim">
|
||
|
|
unused — waves are sparse by design, not sequential
|
||
|
|
</p>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-2">
|
||
|
|
{inWave.map((app) => (
|
||
|
|
<AppCard
|
||
|
|
key={app.name}
|
||
|
|
app={app}
|
||
|
|
state={state}
|
||
|
|
active={selected === app.name}
|
||
|
|
onSelect={() => setSelected(app.name)}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Panel>
|
||
|
|
|
||
|
|
<Panel
|
||
|
|
label={selectedApp ? selectedApp.name : 'No selection'}
|
||
|
|
hint={selectedApp ? `wave ${selectedApp.wave ?? '—'} · ${selectedApp.resources} resources` : 'pick an app'}
|
||
|
|
>
|
||
|
|
{selectedApp ? (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<dl className="grid grid-cols-2 gap-3 font-data text-[11px]">
|
||
|
|
<div>
|
||
|
|
<dt className="text-dim">sync</dt>
|
||
|
|
<dd className={selectedApp.sync === 'synced' ? 'text-flux' : 'text-lamp'}>{selectedApp.sync}</dd>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<dt className="text-dim">health</dt>
|
||
|
|
<dd className={selectedApp.health === 'healthy' ? 'text-flux' : 'text-rose'}>
|
||
|
|
{selectedApp.health}
|
||
|
|
</dd>
|
||
|
|
</div>
|
||
|
|
</dl>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<p className="mb-2 font-data text-[10px] uppercase tracking-[0.16em] text-dim">Resources by kind</p>
|
||
|
|
<ul className="space-y-1.5">
|
||
|
|
{kinds.map((k) => (
|
||
|
|
<li key={k.kind} className="flex items-center gap-3 font-data text-[11px]">
|
||
|
|
<span className={k.named ? 'text-chalk' : 'text-dim'}>{k.kind}</span>
|
||
|
|
<span className="h-px flex-1 bg-wire" />
|
||
|
|
<span className="tabular-nums text-dim">{k.count}</span>
|
||
|
|
{!k.named && (
|
||
|
|
<span className="border border-wire-lit px-1 text-[9px] uppercase tracking-[0.1em] text-dim">
|
||
|
|
count only
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p className="border-t border-wire pt-3 font-plex text-[11px] leading-relaxed text-dim">
|
||
|
|
Secrets appear as counts. Names, source repositories, and condition messages are never sent to the
|
||
|
|
browser.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<p className="font-plex text-sm text-dim">Select an application to inspect its resources.</p>
|
||
|
|
)}
|
||
|
|
</Panel>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|