feat(routing): redesign node cards to the System Sheet recipe (#1133)

Lift <RoutingNodeCard> primitive from audit §21 / DESIGN §9.13. Migrate
the fleet adapter to consume it: collapse five inline state pills into
a single nodeState prop, replace the k/v stat list with a mono meta
line, drop the per-row HEALTHY chip in favor of a state dot, and add a
real empty-state block with a primary CTA for every state. Compact
density body swap stays inside the component, no -compact fork.
This commit is contained in:
Anso
2026-05-21 10:18:03 -04:00
committed by GitHub
parent 620e28f352
commit cdb9cd414e
4 changed files with 741 additions and 160 deletions
+136 -158
View File
@@ -1,12 +1,14 @@
import { useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { TogglePill } from '@/components/ui/toggle-pill';
import { Plus, ServerCog, Activity, Loader2 } from 'lucide-react';
import { formatAgeShort } from '@/lib/relativeTime';
import type { MeshAlias, MeshNodeStatus } from '@/types/mesh';
import { meshRouteStateFor, meshRouteStateTokens } from './meshRouteState';
import {
RoutingNodeCard as RoutingNodeCardPrimitive,
type RoutingAliasRow,
type RoutingNodeCardMeta,
type RoutingNodeState,
} from '@/components/ui/routing-node-card';
interface Props {
status: MeshNodeStatus;
@@ -18,25 +20,108 @@ interface Props {
onChanged: () => void;
}
const REVERSE_BRIDGE: Record<MeshNodeStatus['reverseCallbackStatus'], RoutingNodeCardMeta['reverseBridge']> = {
connected: 'up',
connecting: 'unavailable',
unavailable: 'unavailable',
not_applicable: 'na',
};
function deriveNodeState(status: MeshNodeStatus): RoutingNodeState {
if (status.reachableMode === 'unreachable') return 'offline';
if (!status.enabled) return 'idle';
if (status.reachableMode === 'pilot' && !status.pilotConnected) return 'degraded';
if (status.reverseCallbackStatus === 'unavailable' || status.reverseCallbackStatus === 'connecting') return 'degraded';
return 'meshed';
}
function buildFooterContext(
nodeState: RoutingNodeState,
status: MeshNodeStatus,
seenAgeMs: number,
): string {
const seen = formatAgeShort(seenAgeMs);
switch (nodeState) {
case 'meshed': {
const reverse = status.reverseCallbackStatus === 'connected'
? 'up'
: status.reverseCallbackStatus === 'not_applicable'
? 'n/a'
: 'unavail';
return `Reverse bridge ${reverse} · reconcile ${seen}`;
}
case 'idle':
return `Mesh off · seen ${seen}`;
case 'degraded':
return status.reverseCallbackStatus === 'connecting'
? `Redialing · last update ${seen}`
: `Last seen ${seen}`;
case 'offline':
return `Last seen ${seen}`;
}
}
export function RoutingNodeCard({
status, aliases, onAddStack, onShowDiagnostics, onShowAlias, onTestUpstream, onChanged,
}: Props) {
const [toggling, setToggling] = useState(false);
const [testingAlias, setTestingAlias] = useState<string | null>(null);
const [lastTestedByHost, setLastTestedByHost] = useState<Map<string, { at: number; ok: boolean }>>(() => new Map());
const lastSeenRef = useRef<number>(Date.now());
const lastStatusSignatureRef = useRef<string>('');
const nodeAliases = aliases.filter((a) => a.nodeId === status.nodeId);
const hasOptIns = status.optedInStacks.length > 0;
// Defensive de-dup: `/mesh/status` and `/mesh/aliases` are fetched
// separately, so a transient inconsistency between the two snapshots
// could otherwise render both a live alias row and a suspended row for
// the same stack. Drop suspended entries that the alias snapshot still
// covers; the alias row already represents the live state.
const stackNamesWithAliases = new Set(nodeAliases.map((a) => a.stackName));
const suspendedOptIns = status.optedInStacks.filter(
(s) => !s.currentlyResolvable && !stackNamesWithAliases.has(s.stackName),
// Track only health-bearing fields; `activeStreamCount` and stack-count
// churn would otherwise reset the "seen" clock on every reconcile tick.
const signature = `${status.enabled}|${status.reachableMode}|${status.pilotConnected}|${status.reverseCallbackStatus}`;
if (signature !== lastStatusSignatureRef.current) {
lastStatusSignatureRef.current = signature;
lastSeenRef.current = Date.now();
}
const nodeAliases = useMemo(() => aliases.filter((a) => a.nodeId === status.nodeId), [aliases, status.nodeId]);
// Defensive de-dup: `/mesh/status` and `/mesh/aliases` are fetched separately,
// so a transient inconsistency could otherwise render both a live row and a
// suspended row for the same stack. Drop suspended entries the alias snapshot
// still covers; the alias row already represents the live state.
const stackNamesWithAliases = useMemo(
() => new Set(nodeAliases.map((a) => a.stackName)),
[nodeAliases],
);
const suspended = useMemo(
() => status.optedInStacks.filter(
(s) => !s.currentlyResolvable && !stackNamesWithAliases.has(s.stackName),
),
[status.optedInStacks, stackNamesWithAliases],
);
const rows: RoutingAliasRow[] = useMemo(() => {
const live: RoutingAliasRow[] = nodeAliases.map((a) => ({
host: a.host,
port: a.port,
kind: 'alias',
lastTested: lastTestedByHost.get(a.host),
testing: testingAlias === a.host,
}));
const sus: RoutingAliasRow[] = suspended.map((s) => ({
host: s.stackName,
port: 0,
kind: 'suspended',
}));
return [...live, ...sus];
}, [nodeAliases, suspended, lastTestedByHost, testingAlias]);
const nodeState = deriveNodeState(status);
const meta: RoutingNodeCardMeta = {
pilotConnected: status.pilotConnected,
reverseBridge: REVERSE_BRIDGE[status.reverseCallbackStatus],
stacks: status.optedInStacks.length,
aliases: nodeAliases.length,
};
const seenAgeMs = Date.now() - lastSeenRef.current;
const footerContext = buildFooterContext(nodeState, status, seenAgeMs);
const toggleEnabled = async (next: boolean) => {
if (toggling) return;
setToggling(true);
try {
const action = next ? 'enable' : 'disable';
@@ -53,150 +138,43 @@ export function RoutingNodeCard({
}
};
const runTest = async (alias: string) => {
setTestingAlias(alias);
try { await onTestUpstream(alias); } finally { setTestingAlias(null); }
const handleTestAlias = async (host: string) => {
if (testingAlias) return;
setTestingAlias(host);
try {
await onTestUpstream(host);
setLastTestedByHost((prev) => {
const next = new Map(prev);
next.set(host, { at: Date.now(), ok: true });
return next;
});
} catch {
setLastTestedByHost((prev) => {
const next = new Map(prev);
next.set(host, { at: Date.now(), ok: false });
return next;
});
} finally {
setTestingAlias(null);
}
};
return (
<Card className="bg-card shadow-card-bevel">
<CardContent className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{status.nodeName}</span>
{status.reachableMode === 'local' && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-brand/40 bg-brand/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-brand">
Local
</span>
)}
{status.reachableMode === 'pilot' && !status.pilotConnected && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-destructive/40 bg-destructive/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-destructive">
pilot offline
</span>
)}
{status.reachableMode === 'unreachable' && (
<span
title={status.reachableReason ?? undefined}
className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-destructive/40 bg-destructive/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-destructive"
>
unreachable
</span>
)}
{status.reverseCallbackStatus === 'connecting' && (
<span
title="Central is dialing the reverse bridge to this peer."
className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-card-border bg-card text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-stat-subtitle"
>
reconnecting
</span>
)}
{status.reverseCallbackStatus === 'unavailable' && (
<span
title="Peer→central tunnel is between dials. Central will redial on its next reconcile tick."
className="inline-flex items-center px-1.5 py-0.5 rounded-sm border border-amber-500/40 bg-amber-500/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-amber-600 dark:text-amber-400"
>
reverse unavailable
</span>
)}
</div>
<div className="flex items-center gap-2">
<TogglePill
checked={status.enabled}
disabled={toggling || status.reachableMode === 'unreachable'}
onChange={(next) => { void toggleEnabled(next); }}
/>
<Button variant="outline" size="sm" onClick={onShowDiagnostics}>
<ServerCog className="w-3 h-3 mr-1" /> Diagnostics
</Button>
</div>
</div>
{status.reachableMode === 'unreachable' && status.reachableReason && (
<div className="text-[11px] text-destructive">
{status.reachableReason}
</div>
)}
{status.reachableMode === 'pilot' && !status.pilotConnected && (
<div className="text-[11px] text-stat-subtitle">
Pilot tunnel is not connected. Mesh traffic resumes when the agent reconnects.
</div>
)}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="text-stat-subtitle">Mesh stacks</div>
<div className="font-mono text-stat-value">{status.optedInStacks.length}</div>
<div className="text-stat-subtitle">Aliases</div>
<div className="font-mono text-stat-value">{nodeAliases.length}</div>
</div>
{status.enabled && (
<>
<div className="border-t border-card-border pt-2 space-y-1">
{!hasOptIns && nodeAliases.length === 0 && (
<div className="text-[11px] text-stat-subtitle">No mesh services on this node yet.</div>
)}
{nodeAliases.map((a) => {
const pillState = meshRouteStateFor({
optedIn: true,
pilotConnected: status.pilotConnected,
});
const pill = meshRouteStateTokens(pillState);
return (
<div key={a.host} className="flex items-center justify-between gap-2 rounded border border-card-border bg-card px-2 py-1.5">
<button
type="button"
className="text-[11px] font-mono text-left truncate hover:text-brand transition-colors"
onClick={() => onShowAlias(a.host)}
>
{a.host}:{a.port}
</button>
<span className={`shrink-0 px-1.5 py-0.5 rounded-sm border text-[10px] leading-3 font-mono uppercase tracking-[0.18em] ${pill.toneClass}`}>
{pill.label}
</span>
<Button
variant="ghost" size="sm"
onClick={() => { void runTest(a.host); }}
disabled={testingAlias === a.host}
className="h-6 px-1.5"
>
{testingAlias === a.host
? <Loader2 className="w-3 h-3 animate-spin" />
: <Activity className="w-3 h-3" />}
</Button>
</div>
);
})}
{suspendedOptIns.map((s) => (
<div
key={`suspended-${s.stackName}`}
className="flex items-center justify-between gap-2 rounded border border-card-border bg-card px-2 py-1.5"
title="Stack is opted into the mesh but has no running services. Aliases will publish when the stack starts."
>
<span className="text-[11px] font-mono text-left truncate text-stat-subtitle">
{s.stackName}
</span>
<span className="shrink-0 px-1.5 py-0.5 rounded-sm border border-amber-500/40 bg-amber-500/10 text-[10px] leading-3 font-mono uppercase tracking-[0.18em] text-amber-600 dark:text-amber-400">
suspended
</span>
</div>
))}
{suspendedOptIns.length > 0 && (
<div className="text-[11px] text-stat-subtitle pt-1">
Stack stopped, alias resumes when services start.
</div>
)}
</div>
<Button
variant="outline" size="sm" className="w-full"
onClick={onAddStack}
disabled={status.reachableMode === 'unreachable'}
>
<Plus className="w-3 h-3 mr-1" /> Add stack to mesh
</Button>
</>
)}
</CardContent>
</Card>
<RoutingNodeCardPrimitive
crumb={['Routing', 'Node', status.nodeName]}
name={status.nodeName}
isLocal={status.reachableMode === 'local'}
nodeState={nodeState}
meta={meta}
aliases={rows}
onToggleEnabled={(next) => { void toggleEnabled(next); }}
onShowDiagnostics={onShowDiagnostics}
onShowAlias={onShowAlias}
onTestAlias={(host) => { void handleTestAlias(host); }}
onAddStack={onAddStack}
onRetry={onChanged}
footerContext={footerContext}
offlineReason={status.reachableReason}
/>
);
}
+2 -2
View File
@@ -166,7 +166,7 @@ export function RoutingTab() {
Add a stack to the mesh and its services become reachable from any other meshed
stack by hostname. No VPN, no firewall changes.
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-[18px] grid-auto-rows-min-content items-start w-full">
{status.map((s) => (
<RoutingNodeCard
key={s.nodeId}
@@ -223,7 +223,7 @@ export function RoutingTab() {
)}
</div>
{viewMode === 'table' ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-[18px] grid-auto-rows-min-content items-start">
{status.map((s) => (
<RoutingNodeCard
key={s.nodeId}
@@ -0,0 +1,588 @@
import * as React from 'react';
import { Loader2, Play, Plus, RotateCw, ServerCog } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { TogglePill } from '@/components/ui/toggle-pill';
import { useDensity } from '@/hooks/use-density';
import { formatAgeShort } from '@/lib/relativeTime';
import { cn } from '@/lib/utils';
export type RoutingNodeState = 'meshed' | 'idle' | 'degraded' | 'offline';
export interface RoutingAliasRow {
host: string;
port: number;
kind: 'alias' | 'suspended';
lastTested?: { at: number; ok: boolean };
testing?: boolean;
}
export interface RoutingNodeCardMeta {
pilotConnected: boolean;
reverseBridge: 'up' | 'unavailable' | 'na';
stacks: number;
aliases: number;
}
export interface RoutingNodeCardProps {
crumb: string[];
name: string;
isLocal?: boolean;
nodeState: RoutingNodeState;
meta: RoutingNodeCardMeta;
aliases: RoutingAliasRow[];
onToggleEnabled: (next: boolean) => void;
onShowDiagnostics: () => void;
onShowAlias?: (alias: string) => void;
onTestAlias?: (alias: string) => void;
onAddStack?: () => void;
onRetry?: () => void;
footerContext: string;
/** Offline-state reason copy; falls back to a generic line. */
offlineReason?: string | null;
}
const KICKER = 'font-mono text-[10px] uppercase tracking-[0.18em]';
const RAIL_CLASS: Record<RoutingNodeState, string> = {
meshed: 'bg-brand',
idle: '',
degraded: 'bg-warning',
offline: 'bg-destructive',
};
const RAIL_INLINE_STYLE: Record<RoutingNodeState, React.CSSProperties | undefined> = {
meshed: undefined,
idle: { background: 'oklch(0.28 0 0)' },
degraded: undefined,
offline: undefined,
};
const STATE_CHIP: Record<RoutingNodeState, { label: string; tone: string }> = {
meshed: { label: 'Meshed', tone: 'border-brand/40 bg-brand/10 text-brand' },
idle: { label: 'Idle', tone: 'border-card-border bg-card text-stat-subtitle' },
degraded: { label: 'Degraded', tone: 'border-warning/40 bg-warning/10 text-warning' },
offline: { label: 'Offline', tone: 'border-destructive/40 bg-destructive/10 text-destructive' },
};
const REVERSE_LABEL: Record<RoutingNodeCardMeta['reverseBridge'], string> = {
up: 'up',
unavailable: 'unavailable',
na: 'n/a',
};
export function RoutingNodeCard(props: RoutingNodeCardProps) {
const {
crumb, name, isLocal, nodeState, meta, aliases,
onToggleEnabled, onShowDiagnostics, onShowAlias, onTestAlias,
onAddStack, onRetry, footerContext, offlineReason,
} = props;
const [density] = useDensity();
const compact = density === 'compact';
const toggleDisabled = nodeState === 'offline';
const diagnosticsDisabled = nodeState === 'offline';
const isEnabled = nodeState === 'meshed' || nodeState === 'degraded';
const chip = STATE_CHIP[nodeState];
const railClass = RAIL_CLASS[nodeState];
const railStyle = RAIL_INLINE_STYLE[nodeState];
return (
<Card className="relative overflow-hidden p-0">
<span
aria-hidden="true"
className={cn('absolute inset-y-0 left-0 w-[3px]', railClass)}
style={railStyle}
/>
{compact
? <CompactBody
name={name}
isLocal={isLocal}
chip={chip}
meta={meta}
nodeState={nodeState}
isEnabled={isEnabled}
toggleDisabled={toggleDisabled}
diagnosticsDisabled={diagnosticsDisabled}
onToggleEnabled={onToggleEnabled}
onShowDiagnostics={onShowDiagnostics}
footerContext={footerContext}
aliasesEmpty={aliases.length === 0}
onAddStack={onAddStack}
onRetry={onRetry}
/>
: <ComfortableBody
crumb={crumb}
name={name}
isLocal={isLocal}
chip={chip}
meta={meta}
nodeState={nodeState}
isEnabled={isEnabled}
toggleDisabled={toggleDisabled}
diagnosticsDisabled={diagnosticsDisabled}
onToggleEnabled={onToggleEnabled}
onShowDiagnostics={onShowDiagnostics}
aliases={aliases}
onShowAlias={onShowAlias}
onTestAlias={onTestAlias}
onAddStack={onAddStack}
onRetry={onRetry}
footerContext={footerContext}
offlineReason={offlineReason}
/>}
</Card>
);
}
interface BodyChrome {
name: string;
isLocal?: boolean;
chip: { label: string; tone: string };
meta: RoutingNodeCardMeta;
nodeState: RoutingNodeState;
isEnabled: boolean;
toggleDisabled: boolean;
diagnosticsDisabled: boolean;
onToggleEnabled: (next: boolean) => void;
onShowDiagnostics: () => void;
footerContext: string;
onAddStack?: () => void;
onRetry?: () => void;
}
interface CompactProps extends BodyChrome {
aliasesEmpty: boolean;
}
interface ComfortableProps extends BodyChrome {
crumb: string[];
aliases: RoutingAliasRow[];
onShowAlias?: (alias: string) => void;
onTestAlias?: (alias: string) => void;
offlineReason?: string | null;
}
function ComfortableBody(props: ComfortableProps) {
const {
crumb, name, isLocal, chip, meta, nodeState, isEnabled,
toggleDisabled, diagnosticsDisabled, onToggleEnabled, onShowDiagnostics,
aliases, onShowAlias, onTestAlias, onAddStack, onRetry, footerContext, offlineReason,
} = props;
const published = aliases.filter((a) => a.kind === 'alias').length;
const showAliases = aliases.length > 0;
const sectionTitle = published < aliases.length
? `Aliases · ${published} of ${aliases.length}`
: `Aliases · ${aliases.length}`;
return (
<>
<header className="px-4 pt-4 pb-3 pl-5">
<div className={cn(KICKER, 'text-stat-subtitle leading-none')}>
{crumb.join(' ')}
</div>
<div className="mt-2 flex items-center gap-2">
<h3 className="font-display italic text-[22px] leading-[28px] text-stat-value">
{name}
</h3>
{isLocal && (
<span className={cn(KICKER, 'inline-flex items-center px-1.5 py-0.5 rounded-sm border border-brand/40 bg-brand/10 text-brand')}>
Local
</span>
)}
</div>
<div className={cn(KICKER, 'mt-1.5 text-stat-subtitle leading-none tracking-[0.14em]')}>
pilot {meta.pilotConnected ? 'connected' : 'offline'}
{' · '}reverse {REVERSE_LABEL[meta.reverseBridge]}
{' · '}{meta.stacks} stacks
{' · '}{meta.aliases} aliases
</div>
</header>
<Toolbar
chip={chip}
isEnabled={isEnabled}
toggleDisabled={toggleDisabled}
diagnosticsDisabled={diagnosticsDisabled}
onToggleEnabled={onToggleEnabled}
onShowDiagnostics={onShowDiagnostics}
/>
<div className="px-4 py-3 pl-5">
{showAliases
? <AliasList
title={sectionTitle}
aliases={aliases}
onShowAlias={onShowAlias}
onTestAlias={onTestAlias}
/>
: <EmptyState
nodeState={nodeState}
name={name}
offlineReason={offlineReason}
onAddStack={onAddStack}
onRetry={onRetry}
onToggleEnabled={onToggleEnabled}
/>}
</div>
<Footer context={footerContext} />
</>
);
}
function CompactBody(props: CompactProps) {
const {
name, isLocal, chip, meta, nodeState, isEnabled,
toggleDisabled, diagnosticsDisabled, onToggleEnabled, onShowDiagnostics,
footerContext, aliasesEmpty, onAddStack, onRetry,
} = props;
return (
<>
<header className="flex items-center gap-2 px-4 py-2.5 pl-5">
<h3 className="font-display italic text-[18px] leading-[22px] text-stat-value mr-1">
{name}
</h3>
{isLocal && (
<span className={cn(KICKER, 'inline-flex items-center px-1 py-0.5 rounded-sm border border-brand/40 bg-brand/10 text-brand')}>
</span>
)}
<span className={cn(KICKER, 'inline-flex items-center px-1.5 py-0.5 rounded-sm border', chip.tone)}>
{chip.label}
</span>
<div className="ml-auto flex items-center gap-1.5">
<TogglePill
checked={isEnabled}
disabled={toggleDisabled}
onChange={onToggleEnabled}
/>
<Button
variant="outline"
size="sm"
onClick={onShowDiagnostics}
disabled={diagnosticsDisabled}
className="h-7 w-7 p-0"
aria-label="Diagnostics"
>
<ServerCog className="w-3 h-3" />
</Button>
</div>
</header>
<div className="grid grid-cols-4 divide-x divide-card-border/40 border-y border-card-border/40 bg-card/60 pl-[3px]">
<CompactCell
dot={meta.stacks > 0 ? 'success' : 'muted'}
value={String(meta.stacks)}
sub="opted in"
/>
<CompactCell
dot={meta.aliases > 0 ? 'success' : 'muted'}
value={String(meta.aliases)}
sub="published"
/>
<CompactCell
dot={meta.pilotConnected ? 'success' : 'warning'}
value={meta.pilotConnected ? 'on' : 'off'}
sub="agent"
/>
<CompactCell
dot={reverseDot(meta.reverseBridge)}
value={REVERSE_LABEL[meta.reverseBridge]}
sub="bridge"
/>
</div>
<CompactFooter
context={footerContext}
nodeState={nodeState}
name={name}
aliasesEmpty={aliasesEmpty}
onAddStack={onAddStack}
onRetry={onRetry}
onToggleEnabled={onToggleEnabled}
/>
</>
);
}
function reverseDot(reverse: RoutingNodeCardMeta['reverseBridge']): DotTone {
if (reverse === 'up') return 'success';
if (reverse === 'unavailable') return 'warning';
return 'muted';
}
type DotTone = 'success' | 'warning' | 'destructive' | 'muted';
function CompactCell({ dot, value, sub }: { dot: DotTone; value: string; sub: string }) {
return (
<div className="px-3 py-2.5 flex items-center gap-2">
<Dot tone={dot} />
<div className="min-w-0">
<div className="font-mono text-[12px] leading-none text-stat-value tabular-nums">{value}</div>
<div className={cn(KICKER, 'text-stat-subtitle leading-none mt-1')}>{sub}</div>
</div>
</div>
);
}
interface ToolbarProps {
chip: { label: string; tone: string };
isEnabled: boolean;
toggleDisabled: boolean;
diagnosticsDisabled: boolean;
onToggleEnabled: (next: boolean) => void;
onShowDiagnostics: () => void;
}
function Toolbar({ chip, isEnabled, toggleDisabled, diagnosticsDisabled, onToggleEnabled, onShowDiagnostics }: ToolbarProps) {
return (
<div className="flex items-center gap-2 px-4 py-2 pl-5 border-y border-card-border/40 bg-card/60">
<span className={cn(KICKER, 'inline-flex items-center px-1.5 py-0.5 rounded-sm border', chip.tone)}>
{chip.label}
</span>
<div className="flex-1" />
<TogglePill
checked={isEnabled}
disabled={toggleDisabled}
onChange={onToggleEnabled}
/>
<Button
variant="outline"
size="sm"
onClick={onShowDiagnostics}
disabled={diagnosticsDisabled}
>
<ServerCog className="w-3 h-3 mr-1" /> Diagnostics
</Button>
</div>
);
}
interface AliasListProps {
title: string;
aliases: RoutingAliasRow[];
onShowAlias?: (alias: string) => void;
onTestAlias?: (alias: string) => void;
}
function AliasList({ title, aliases, onShowAlias, onTestAlias }: AliasListProps) {
return (
<section>
<h4 className={cn(KICKER, 'text-stat-subtitle leading-none mb-2 tracking-[0.22em]')}>
{title}
</h4>
<div className="space-y-1">
{aliases.map((row) => (
<AliasRow
key={`${row.kind}:${row.host}`}
row={row}
onShowAlias={onShowAlias}
onTestAlias={onTestAlias}
/>
))}
</div>
</section>
);
}
interface AliasRowProps {
row: RoutingAliasRow;
onShowAlias?: (alias: string) => void;
onTestAlias?: (alias: string) => void;
}
function AliasRow({ row, onShowAlias, onTestAlias }: AliasRowProps) {
const isSuspended = row.kind === 'suspended';
const dot: DotTone = isSuspended
? 'warning'
: row.lastTested
? (row.lastTested.ok ? 'success' : 'destructive')
: 'muted';
const ageLabel = row.lastTested
? formatAgeShort(Date.now() - row.lastTested.at)
: '';
return (
<div className="flex items-center gap-2 rounded border border-card-border/40 bg-card/40 px-2 py-1.5 shadow-[inset_0_1px_0_oklch(0_0_0_/_0.03)]">
<Dot tone={dot} />
{isSuspended
? <span className="flex-1 min-w-0 truncate font-mono text-[11px] text-stat-subtitle">
{row.host} <span className="opacity-70">(suspended)</span>
</span>
: onShowAlias
? <button
type="button"
className="flex-1 min-w-0 truncate text-left font-mono text-[11px] text-stat-value hover:text-brand transition-colors"
onClick={() => onShowAlias(row.host)}
>
{row.host}<span className="text-brand">:{row.port}</span>
</button>
: <span className="flex-1 min-w-0 truncate font-mono text-[11px] text-stat-value">
{row.host}<span className="text-brand">:{row.port}</span>
</span>}
{!isSuspended && ageLabel && (
<span className={cn(KICKER, 'shrink-0 text-stat-subtitle tabular-nums')}>{ageLabel}</span>
)}
<Button
variant="ghost"
size="sm"
onClick={() => onTestAlias?.(row.host)}
disabled={isSuspended || row.testing || !onTestAlias}
className="h-[22px] w-[22px] p-0 shrink-0"
aria-label={`Test ${row.host}`}
>
{row.testing
? <Loader2 className="w-3 h-3 animate-spin" />
: <Play className="w-3 h-3" strokeWidth={1.75} />}
</Button>
</div>
);
}
interface EmptyStateProps {
nodeState: RoutingNodeState;
name: string;
offlineReason?: string | null;
onAddStack?: () => void;
onRetry?: () => void;
onToggleEnabled: (next: boolean) => void;
}
function EmptyState({ nodeState, name, offlineReason, onAddStack, onRetry, onToggleEnabled }: EmptyStateProps) {
const { headline, sub, cta } = emptyStateCopy(nodeState, name, offlineReason);
const handleClick = () => {
if (nodeState === 'idle') onToggleEnabled(true);
else if (nodeState === 'meshed') onAddStack?.();
else onRetry?.();
};
return (
<div className="flex flex-col items-start gap-2 py-3">
<div className="font-display italic text-[18px] leading-[24px] text-stat-value">
{headline}
</div>
<div className="font-mono text-[11px] leading-snug text-stat-subtitle">
{sub}
</div>
<Button
variant="outline"
size="sm"
onClick={handleClick}
className={cn('mt-1', ctaToneFor(nodeState))}
>
{ctaIconFor(nodeState)}{cta}
</Button>
</div>
);
}
function ctaIconFor(state: RoutingNodeState): React.ReactNode {
if (state === 'meshed') return <Plus className="w-3 h-3 mr-1" />;
if (state === 'degraded' || state === 'offline') return <RotateCw className="w-3 h-3 mr-1" />;
return null;
}
const CTA_TONE: Record<RoutingNodeState, string> = {
meshed: '',
idle: '',
degraded: 'border-warning/40 text-warning hover:bg-warning/10',
offline: 'border-destructive/40 text-destructive hover:bg-destructive/10',
};
function ctaToneFor(state: RoutingNodeState): string {
return CTA_TONE[state];
}
function emptyStateCopy(state: RoutingNodeState, name: string, offlineReason?: string | null): {
headline: string; sub: string; cta: string;
} {
switch (state) {
case 'idle':
return {
headline: 'Not in the mesh.',
sub: 'Toggle on to start publishing aliases from this node.',
cta: `Enable mesh on ${name}`,
};
case 'meshed':
return {
headline: 'No services routed yet.',
sub: 'Mesh is on. Opt a stack in to start publishing aliases.',
cta: '+ Add stack to mesh',
};
case 'degraded':
return {
headline: 'Pilot tunnel disconnected.',
sub: 'Mesh traffic resumes when the agent reconnects.',
cta: 'Retry now',
};
case 'offline':
return {
headline: 'Node unreachable.',
sub: offlineReason || 'Connection refused.',
cta: 'Retry now',
};
}
}
function Footer({ context }: { context: string }) {
return (
<div className="px-4 py-2 pl-5 border-t border-card-border/40 bg-card/60">
<div className={cn(KICKER, 'text-stat-subtitle leading-none')}>
{context}
</div>
</div>
);
}
interface CompactFooterProps {
context: string;
nodeState: RoutingNodeState;
name: string;
aliasesEmpty: boolean;
onAddStack?: () => void;
onRetry?: () => void;
onToggleEnabled: (next: boolean) => void;
}
function CompactFooter({ context, nodeState, name, aliasesEmpty, onAddStack, onRetry, onToggleEnabled }: CompactFooterProps) {
const showCta = nodeState !== 'meshed' || aliasesEmpty;
const { cta } = emptyStateCopy(nodeState, name);
const handleClick = () => {
if (nodeState === 'idle') onToggleEnabled(true);
else if (nodeState === 'meshed') onAddStack?.();
else onRetry?.();
};
return (
<div className="flex items-center gap-2 px-4 py-2 pl-5 border-t border-card-border/40 bg-card/60">
<div className={cn(KICKER, 'text-stat-subtitle leading-none truncate')}>
{context}
</div>
{showCta && (
<Button
variant="outline"
size="sm"
onClick={handleClick}
className={cn('ml-auto h-7', ctaToneFor(nodeState))}
>
{ctaIconFor(nodeState)}{cta}
</Button>
)}
</div>
);
}
const DOT_CLASS: Record<DotTone, string> = {
success: 'bg-success',
warning: 'bg-warning',
destructive: 'bg-destructive',
muted: 'bg-stat-subtitle/50',
};
function Dot({ tone }: { tone: DotTone }) {
return <span aria-hidden="true" className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', DOT_CLASS[tone])} />;
}
+15
View File
@@ -18,3 +18,18 @@ export function formatTimeAgo(timestamp: number): string {
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
/**
* Short age formatter for cockpit-style "12s / 4m / 3h / 2d" badges. Always
* returns under five characters. Negative deltas (clock skew) read as `0s`.
*/
export function formatAgeShort(ms: number): string {
if (ms < 1000) return '0s';
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h`;
return `${Math.floor(h / 24)}d`;
}