@nurix/etna 0.19.3 → 0.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/rules/frontend.md +3 -3
- package/data/harnesses/code/skills/design/SKILL.md +20 -8
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +10 -5
- package/data/harnesses/code/skills/design/data/combinations/Readme.md +8 -4
- package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +113 -0
- package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +11 -0
- package/data/harnesses/code/skills/design/data/combinations/manifest.json +57 -10
- package/data/harnesses/code/skills/design/data/combinations/motion.csv +10 -0
- package/data/harnesses/code/skills/design/data/combinations/palettes.csv +92 -49
- package/data/harnesses/code/skills/design/data/combinations/styles.csv +67 -54
- package/data/harnesses/code/skills/design/data/combinations/typography.csv +64 -61
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -5
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +6 -6
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +2 -2
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -6
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +5 -5
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +11 -7
- package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +256 -141
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +123 -123
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +307 -2
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +310 -2
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +25 -25
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +18 -18
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +82 -105
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +97 -93
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +16 -15
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +34 -34
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +5 -5
- package/data/harnesses/code/skills/design/references/create.md +49 -7
- package/data/harnesses/code/skills/design/references/diagnosis.md +3 -3
- package/data/harnesses/code/skills/design/references/hard-rules.md +25 -3
- package/data/harnesses/code/skills/design/references/improve.md +32 -3
- package/data/harnesses/code/skills/design/references/interaction.md +3 -3
- package/data/harnesses/code/skills/design/references/introspect.md +1 -1
- package/data/harnesses/code/skills/design/references/lifecycle.md +2 -2
- package/data/harnesses/code/skills/design/references/personality.md +280 -133
- package/data/harnesses/code/skills/design/references/structure.md +22 -1
- package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +306 -26
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -104
- package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +200 -11
- package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +65 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +95 -11
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +50 -5
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +214 -14
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +242 -25
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +81 -6
- package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +101 -5
- package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +142 -17
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// skills/design/data/operator-pack/exemplars/command-center.tsx —
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/command-center.tsx — reference render — imitate the ambition and the composition; the project's tokens supply the look.
|
|
2
|
+
// Surface: baseline
|
|
2
3
|
import { useMemo, useState } from "react";
|
|
3
4
|
import { Activity, AlertTriangle, ArrowDown, ArrowUp, Hand, Radio, RefreshCw, ShieldAlert, Signal, Undo2, WifiOff, type LucideIcon } from "lucide-react";
|
|
4
5
|
import { Button } from "@nurix/components/button";
|
|
@@ -44,9 +45,7 @@ function fmtWait(sec: number): string {
|
|
|
44
45
|
function KpiTile({ label, value, unit, trend }: { label: string; value: string; unit?: string; trend?: number }) {
|
|
45
46
|
return (
|
|
46
47
|
<Statistics variant="card" size="md">
|
|
47
|
-
<StatisticsHeader>
|
|
48
|
-
<StatisticsHeading><StatisticsTitle size="sm">{label}</StatisticsTitle></StatisticsHeading>
|
|
49
|
-
</StatisticsHeader>
|
|
48
|
+
<StatisticsHeader><StatisticsHeading><StatisticsTitle size="sm">{label}</StatisticsTitle></StatisticsHeading></StatisticsHeader>
|
|
50
49
|
<StatisticsMetric>
|
|
51
50
|
<StatisticsValue className="tabular-nums">{value}</StatisticsValue>
|
|
52
51
|
{unit ? <StatisticsUnit>{unit}</StatisticsUnit> : null}
|
|
@@ -68,16 +67,16 @@ function HealthTile({ panel }: { panel: HealthPanel }) {
|
|
|
68
67
|
return (
|
|
69
68
|
<Card className="flex flex-col gap-2 p-4">
|
|
70
69
|
<div className="flex items-center justify-between">
|
|
71
|
-
<span className="text-
|
|
72
|
-
<span className={`flex items-center gap-1 text-
|
|
70
|
+
<span className="text-small font-medium text-foreground">{panel.label}</span>
|
|
71
|
+
<span className={`flex items-center gap-1 text-caption ${tone.text}`}><tone.Icon className="size-4" aria-hidden />{tone.word}</span>
|
|
73
72
|
</div>
|
|
74
73
|
<div className="flex items-baseline gap-1">
|
|
75
|
-
<span className="text-
|
|
76
|
-
<span className="text-
|
|
74
|
+
<span className="text-h3 tabular-nums text-foreground">{panel.p95}</span>
|
|
75
|
+
<span className="text-caption text-muted-foreground">ms p95</span>
|
|
77
76
|
</div>
|
|
78
77
|
<Sparkline points={panel.trend} type="line" height={28} className="w-full text-primary" />
|
|
79
|
-
{panel.status === "degraded" ? <p className="text-
|
|
80
|
-
{panel.status === "down" ? <p className="text-
|
|
78
|
+
{panel.status === "degraded" ? <p className="text-caption text-warning">Stale since {panel.since}; showing last known values.</p> : null}
|
|
79
|
+
{panel.status === "down" ? <p className="text-caption text-destructive">Probe timed out {panel.since} ago; retrying automatically.</p> : null}
|
|
81
80
|
</Card>
|
|
82
81
|
);
|
|
83
82
|
}
|
|
@@ -87,17 +86,18 @@ function StreamPane({ events }: { events: StreamEvent[] }) {
|
|
|
87
86
|
<Card className="flex min-h-0 flex-col">
|
|
88
87
|
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
89
88
|
<Activity className="size-4 text-primary" aria-hidden />
|
|
90
|
-
<h2 className="text-
|
|
89
|
+
<h2 className="text-h3">Live session stream</h2>
|
|
91
90
|
<Badge variant="outline" className="ml-auto tabular-nums">{events.length}</Badge>
|
|
92
91
|
</div>
|
|
93
92
|
<ScrollArea className="min-h-0 flex-1">
|
|
94
|
-
|
|
93
|
+
{/* w-0 min-w-full pins the list to the ScrollArea viewport, so truncation paints at the visible edge */}
|
|
94
|
+
<ul aria-live="polite" aria-relevant="additions" className="w-0 min-w-full divide-y divide-border">
|
|
95
95
|
{events.map((e) => (
|
|
96
|
-
<li key={e.id} className="flex items-baseline gap-3 px-4 py-2 text-
|
|
96
|
+
<li key={e.id} className="flex items-baseline gap-3 px-4 py-2 text-small">
|
|
97
97
|
<time className="shrink-0 font-mono tabular-nums text-muted-foreground">{e.at}</time>
|
|
98
98
|
<span className="shrink-0 font-mono text-muted-foreground">{e.session}</span>
|
|
99
99
|
<span className="shrink-0 text-foreground">{KIND_LABEL[e.kind]}</span>
|
|
100
|
-
<span className="truncate text-muted-foreground">{e.detail}</span>
|
|
100
|
+
<span className="min-w-0 flex-1 truncate text-muted-foreground" title={e.detail}>{e.detail}</span>
|
|
101
101
|
</li>
|
|
102
102
|
))}
|
|
103
103
|
</ul>
|
|
@@ -108,9 +108,9 @@ function StreamPane({ events }: { events: StreamEvent[] }) {
|
|
|
108
108
|
// The escalation queue: a sortable, keyboard-actionable table of sessions flagged for human takeover.
|
|
109
109
|
function EscalationTable({ rows, sort, onToggleSort, onTakeOver, onDismiss }: { rows: Escalation[]; sort: "asc" | "desc"; onToggleSort: () => void; onTakeOver: (e: Escalation) => void; onDismiss: (e: Escalation) => void }) {
|
|
110
110
|
return (
|
|
111
|
-
<table className="w-full border-collapse text-
|
|
111
|
+
<table className="w-full border-collapse text-small">
|
|
112
112
|
<thead className="sticky top-0 bg-card">
|
|
113
|
-
<tr className="border-b border-border text-left text-muted-foreground">
|
|
113
|
+
<tr className="border-b border-border text-left text-caption text-muted-foreground">
|
|
114
114
|
<th scope="col" className="px-3 py-2 font-medium">Session</th>
|
|
115
115
|
<th scope="col" className="px-3 py-2 font-medium">Reason</th>
|
|
116
116
|
<th scope="col" className="px-3 py-2 font-medium">Sentiment</th>
|
|
@@ -122,8 +122,8 @@ function EscalationTable({ rows, sort, onToggleSort, onTakeOver, onDismiss }: {
|
|
|
122
122
|
</thead>
|
|
123
123
|
<tbody>
|
|
124
124
|
{rows.map((e) => (
|
|
125
|
-
<tr key={e.id} className="border-b border-border">
|
|
126
|
-
<td className="px-3 py-2"><span className="font-mono tabular-nums text-foreground">{e.session}</span></td>
|
|
125
|
+
<tr key={e.id} className="motion-lift border-b border-border">
|
|
126
|
+
<td className="min-w-[9ch] px-3 py-2"><span className="whitespace-nowrap font-mono tabular-nums text-foreground">{e.session}</span></td>
|
|
127
127
|
<td className="px-3 py-2 text-muted-foreground"><span className="line-clamp-1">{e.reason}</span></td>
|
|
128
128
|
<td className="px-3 py-2"><span className={SENTIMENT_TONE[e.sentiment]}>{e.sentiment}</span></td>
|
|
129
129
|
<td className="px-3 py-2 text-right"><span className="font-mono tabular-nums text-foreground">{fmtWait(e.waitingSec)}</span></td>
|
|
@@ -159,12 +159,12 @@ export function CommandCenter({ state = "ready", panels = SEED_PANELS, events =
|
|
|
159
159
|
return [...visible].sort((a, b) => (sort === "asc" ? a.waitingSec - b.waitingSec : b.waitingSec - a.waitingSec));
|
|
160
160
|
}, [escalations, handled, sort]);
|
|
161
161
|
return (
|
|
162
|
-
<main data-density-step="tighter" className="flex min-h-[100dvh] flex-col gap-4 bg-background p-4 text-foreground">
|
|
162
|
+
<main data-density-step="tighter" className="flex min-h-[100dvh] flex-col gap-4 bg-background p-4 font-sans text-foreground">
|
|
163
163
|
<header className="flex flex-wrap items-center gap-3">
|
|
164
164
|
<Radio className="size-5 text-primary" aria-hidden />
|
|
165
|
-
<h1 className="text-
|
|
166
|
-
<span className="flex items-center gap-1 text-
|
|
167
|
-
<span className="ml-auto flex items-center gap-2 text-muted-foreground">Updated <time className="font-mono tabular-nums">{updatedAt}</time><Kbd>/</Kbd>to search</span>
|
|
165
|
+
<h1 className="text-h1">Voice operations bridge</h1>
|
|
166
|
+
<span className="flex items-center gap-1 text-small text-success"><Signal className="size-4" aria-hidden />Live</span>
|
|
167
|
+
<span className="ml-auto flex items-center gap-2 text-small text-muted-foreground">Updated <time className="font-mono tabular-nums">{updatedAt}</time><Kbd>/</Kbd>to search</span>
|
|
168
168
|
</header>
|
|
169
169
|
{state === "loading" ? <CommandSkeleton /> : state === "error" ? (
|
|
170
170
|
<div className="flex flex-1 items-center justify-center">
|
|
@@ -190,7 +190,7 @@ export function CommandCenter({ state = "ready", panels = SEED_PANELS, events =
|
|
|
190
190
|
<Card className="flex min-h-0 flex-col">
|
|
191
191
|
<div className="flex items-center gap-2 border-b border-border px-4 py-3">
|
|
192
192
|
<ShieldAlert className="size-4 text-primary" aria-hidden />
|
|
193
|
-
<h2 className="text-
|
|
193
|
+
<h2 className="text-h3">Escalation queue</h2>
|
|
194
194
|
<Badge variant="outline" className="ml-auto tabular-nums">{rows.length}</Badge>
|
|
195
195
|
</div>
|
|
196
196
|
<ScrollArea className="min-h-0 flex-1">
|
|
@@ -203,7 +203,7 @@ export function CommandCenter({ state = "ready", panels = SEED_PANELS, events =
|
|
|
203
203
|
</Card>
|
|
204
204
|
</section>
|
|
205
205
|
{handled ? (
|
|
206
|
-
<div role="status" className="flex items-center gap-3 rounded-md border border-border bg-muted px-4 py-2 text-
|
|
206
|
+
<div role="status" className="flex items-center gap-3 rounded-md border border-border bg-muted px-4 py-2 text-small text-foreground">
|
|
207
207
|
<span>{handled.verb} session <span className="font-mono tabular-nums">{handled.session}</span>.</span>
|
|
208
208
|
<Button size="sm" variant="ghost" className="ml-auto" onClick={() => setHandled(null)}><Undo2 className="size-4" aria-hidden />Undo</Button>
|
|
209
209
|
</div>
|
|
@@ -216,7 +216,7 @@ export function CommandCenter({ state = "ready", panels = SEED_PANELS, events =
|
|
|
216
216
|
<DialogTitle>Take over session {active?.session}?</DialogTitle>
|
|
217
217
|
<DialogDescription>The AI agent will be muted and the caller connected to a human. The current turn is interrupted immediately.</DialogDescription>
|
|
218
218
|
</DialogHeader>
|
|
219
|
-
<DialogBody className="text-
|
|
219
|
+
<DialogBody className="text-small text-muted-foreground">Flagged for: {active?.reason} Waiting {active ? fmtWait(active.waitingSec) : ""}.</DialogBody>
|
|
220
220
|
<DialogFooter>
|
|
221
221
|
<DialogClose asChild><Button variant="ghost" size="sm">Cancel</Button></DialogClose>
|
|
222
222
|
<Button size="sm" onClick={() => { if (active) setHandled({ id: active.id, session: active.session, verb: "Took over" }); setActive(null); }}><Hand className="size-4" aria-hidden />Take over session</Button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// skills/design/data/operator-pack/exemplars/create-edit.tsx
|
|
2
|
-
//
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/create-edit.tsx — reference render — imitate the ambition and the composition; the project's tokens supply the look.
|
|
2
|
+
// Surface: brand-forward (delight-allowing)
|
|
3
3
|
import { useState, type FormEvent, type ReactNode } from "react";
|
|
4
4
|
import { TriangleAlert, CircleCheck, Plus, RotateCcw } from "lucide-react";
|
|
5
5
|
import { Sheet, SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription } from "@nurix/components/sheet";
|
|
@@ -55,9 +55,9 @@ function validate(draft: AlertRuleDraft): FieldErrors {
|
|
|
55
55
|
function Field({ id, label, hint, error, htmlFor, children }: { id: string; label: string; hint?: string; error?: string; htmlFor?: string; children: ReactNode }) {
|
|
56
56
|
return (
|
|
57
57
|
<div className="grid gap-1.5">
|
|
58
|
-
<Label id={`${id}-label`} htmlFor={htmlFor} className="text-
|
|
58
|
+
<Label id={`${id}-label`} htmlFor={htmlFor} className="text-small font-medium">{label}</Label>
|
|
59
59
|
{children}
|
|
60
|
-
{error ? <p id={`${id}-error`} className="text-
|
|
60
|
+
{error ? <p id={`${id}-error`} className="text-small text-destructive">{error}</p> : hint ? <p className="text-small text-muted-foreground">{hint}</p> : null}
|
|
61
61
|
</div>
|
|
62
62
|
);
|
|
63
63
|
}
|
|
@@ -104,10 +104,10 @@ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
|
104
104
|
function reset() { setDraft({ ...BLANK_DRAFT }); setTouched({}); setAttempted(false); setSubmitStatus("idle"); setSubmitError(null); setPhase("form"); }
|
|
105
105
|
return (
|
|
106
106
|
<Sheet open={open} onOpenChange={(next) => { if (!next) onCancel(); }}>
|
|
107
|
-
<SheetContent side="right" size="lg" data-density-step="looser">
|
|
107
|
+
<SheetContent side="right" size="lg" data-density-step="looser" className="bg-surface-overlay font-sans shadow-overlay">
|
|
108
108
|
<SheetHeader showDivider>
|
|
109
|
-
<SheetTitle>{mode === "create" ? "New alert rule" : "Edit alert rule"}</SheetTitle>
|
|
110
|
-
<SheetDescription>Watches one metric and notifies your channels when it crosses a threshold.</SheetDescription>
|
|
109
|
+
<SheetTitle className="text-h3">{mode === "create" ? "New alert rule" : "Edit alert rule"}</SheetTitle>
|
|
110
|
+
<SheetDescription className="text-small">Watches one metric and notifies your channels when it crosses a threshold.</SheetDescription>
|
|
111
111
|
</SheetHeader>
|
|
112
112
|
<SheetBody>
|
|
113
113
|
{status === "loading" && (
|
|
@@ -118,8 +118,8 @@ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
|
118
118
|
{status === "error" && (
|
|
119
119
|
<div className="grid justify-items-center gap-4 py-8 text-center" role="alert">
|
|
120
120
|
<TriangleAlert className="size-8 text-destructive" />
|
|
121
|
-
<p className="text-
|
|
122
|
-
<p className="max-w-sm text-
|
|
121
|
+
<p className="text-h3 text-foreground">Couldn't load this rule</p>
|
|
122
|
+
<p className="max-w-sm text-small text-muted-foreground">The alerting service didn't respond, so its current settings aren't available. Retry, or close and reopen it from the alert list.</p>
|
|
123
123
|
<Button variant="outline" onClick={onRetryLoad}><RotateCcw className="size-4" /> Retry</Button>
|
|
124
124
|
</div>
|
|
125
125
|
)}
|
|
@@ -168,7 +168,7 @@ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
|
168
168
|
<Field id="channels" label="Notify channels" error={show("channelIds")}>
|
|
169
169
|
<div className="grid gap-2" role="group" aria-labelledby="channels-label">
|
|
170
170
|
{channels.map((c) => (
|
|
171
|
-
<Label key={c.id} className="flex items-center gap-3 text-
|
|
171
|
+
<Label key={c.id} className="flex items-center gap-3 text-small font-normal">
|
|
172
172
|
<Checkbox checked={draft.channelIds.includes(c.id)} onCheckedChange={(v) => { patch("channelIds", v ? [...draft.channelIds, c.id] : draft.channelIds.filter((id) => id !== c.id)); blur("channelIds"); }} />
|
|
173
173
|
<span className="text-foreground">{c.label}</span>
|
|
174
174
|
<Badge variant="outline" className="ml-auto">{c.kind}</Badge>
|
|
@@ -179,28 +179,28 @@ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
|
179
179
|
<Field id="enabled" label="Enable on save">
|
|
180
180
|
<div className="flex items-center gap-3">
|
|
181
181
|
<Switch checked={draft.enabled} aria-labelledby="enabled-label" onCheckedChange={(v) => patch("enabled", v)} />
|
|
182
|
-
<span className="text-
|
|
182
|
+
<span className="text-small text-muted-foreground">{draft.enabled ? "Starts evaluating as soon as it's created." : "Saved paused, enable it later from the alert list."}</span>
|
|
183
183
|
</div>
|
|
184
184
|
</Field>
|
|
185
185
|
</form>
|
|
186
186
|
)}
|
|
187
187
|
{isForm && phase === "preview" && (
|
|
188
188
|
<div className="grid gap-4">
|
|
189
|
-
<p className="text-
|
|
190
|
-
<dl className="grid gap-3 text-
|
|
189
|
+
<p className="text-h3 text-foreground">Review before creating</p>
|
|
190
|
+
<dl className="grid gap-3 text-small">
|
|
191
191
|
<PreviewRow label="Rule"><span className="font-medium text-foreground">{draft.name}</span></PreviewRow>
|
|
192
192
|
<PreviewRow label="Condition">{metric?.label} goes {draft.direction} <span className="font-mono tabular-nums">{draft.threshold}</span> {metric?.unit}</PreviewRow>
|
|
193
193
|
<PreviewRow label="Channels">{selectedChannels.map((c) => c.label).join(", ")}</PreviewRow>
|
|
194
194
|
<PreviewRow label="State">{draft.enabled ? "Enabled on create" : "Created paused"}</PreviewRow>
|
|
195
195
|
</dl>
|
|
196
|
-
<p className="text-
|
|
196
|
+
<p className="text-small text-muted-foreground">A rule that pages is hard to unsend, so confirm the scope before creating.</p>
|
|
197
197
|
</div>
|
|
198
198
|
)}
|
|
199
199
|
{isForm && phase === "success" && (
|
|
200
200
|
<div className="grid justify-items-center gap-4 py-8 text-center" role="status" aria-live="polite">
|
|
201
|
-
<
|
|
202
|
-
<p className="text-
|
|
203
|
-
<p className="max-w-sm text-
|
|
201
|
+
<span className="grid size-14 place-items-center rounded-full bg-surface-raised text-success shadow-md"><CircleCheck className="size-8" /></span>
|
|
202
|
+
<p className="text-h3 text-foreground">Alert rule created</p>
|
|
203
|
+
<p className="max-w-sm text-small text-muted-foreground">{draft.name} is {draft.enabled ? "live and evaluating now" : "saved and paused"}. It notifies <span className="tabular-nums">{selectedChannels.length}</span> channel{selectedChannels.length === 1 ? "" : "s"} when it fires.</p>
|
|
204
204
|
<div className="flex items-center gap-2">
|
|
205
205
|
<Button variant="outline" onClick={onCancel}>View rule</Button>
|
|
206
206
|
<Button onClick={reset}>Create another rule</Button>
|
|
@@ -211,7 +211,7 @@ export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
|
211
211
|
{isForm && phase !== "success" && (
|
|
212
212
|
<SheetFooter showDivider>
|
|
213
213
|
<div className="flex w-full items-center justify-between gap-4">
|
|
214
|
-
<p className="text-
|
|
214
|
+
<p className="text-small text-muted-foreground" role="status" aria-live="polite">
|
|
215
215
|
{busy ? "Creating rule" : phase === "form" ? <>Press <Kbd size="sm">Enter</Kbd> to review before creating.</> : "Nothing is created until you confirm."}
|
|
216
216
|
</p>
|
|
217
217
|
<div className="flex items-center gap-2">
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// skills/design/data/operator-pack/exemplars/dashboard.tsx —
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/dashboard.tsx — reference render — imitate the ambition and the composition; the project's tokens supply the look.
|
|
2
|
+
// Surface: brand-forward (delight-allowing)
|
|
2
3
|
import { useMemo, useState, type ReactNode } from "react";
|
|
3
|
-
import {
|
|
4
|
-
Activity, ArrowDown, ArrowUp, ArrowUpRight, ChevronsUpDown, CircleAlert,
|
|
5
|
-
Clock, Database, Plug, RefreshCw, TriangleAlert,
|
|
6
|
-
} from "lucide-react";
|
|
4
|
+
import { Activity, ArrowDown, ArrowUp, ArrowUpRight, ChevronsUpDown, CircleAlert, Clock, Database, Plug, RefreshCw, TriangleAlert } from "lucide-react";
|
|
7
5
|
import { Alert, AlertDescription, AlertTitle } from "@nurix/components/alert";
|
|
8
6
|
import { Badge } from "@nurix/components/badge";
|
|
9
7
|
import { Button } from "@nurix/components/button";
|
|
10
8
|
import { EmptyState } from "@nurix/components/empty-state";
|
|
11
9
|
import { Skeleton } from "@nurix/components/skeleton";
|
|
10
|
+
import { BarGraph, type BarGraphData } from "@nurix/components/bar-graph";
|
|
11
|
+
import { LineGraph, type LineGraphData } from "@nurix/components/line-graph";
|
|
12
12
|
import {
|
|
13
13
|
Statistics, StatisticsActions, StatisticsCaption, StatisticsDescription, StatisticsFooter,
|
|
14
|
-
StatisticsHeader, StatisticsHeading, StatisticsIcon,
|
|
15
|
-
|
|
16
|
-
StatisticsTrend, StatisticsUnit, StatisticsValue, type StatisticsSegment,
|
|
14
|
+
StatisticsHeader, StatisticsHeading, StatisticsIcon, StatisticsMetric,
|
|
15
|
+
StatisticsSparkline, StatisticsTitle, StatisticsTrend, StatisticsUnit, StatisticsValue,
|
|
17
16
|
} from "@nurix/components/statistics";
|
|
18
17
|
|
|
19
18
|
type SurfaceStatus = "loading" | "ready" | "empty" | "error";
|
|
@@ -34,82 +33,75 @@ const HEALTH: Record<Health, { label: string; className: string; icon: ReactNode
|
|
|
34
33
|
};
|
|
35
34
|
// Signed percentage for a KPI trend chip.
|
|
36
35
|
const formatPct = (n: number) => `${n > 0 ? "+" : ""}${n.toFixed(1)}%`;
|
|
37
|
-
// KPI stat card: headline value, trend, and a per-metric sparkline; the degraded metric shows a cached marker instead.
|
|
36
|
+
// KPI stat card: headline value, trend, and a per-metric sparkline on a raised (elevation.md) panel; the degraded metric shows a cached marker instead.
|
|
38
37
|
function KpiCard({ kpi }: { kpi: KpiDatum }) {
|
|
39
38
|
return (
|
|
40
|
-
<Statistics variant="card" size="md" className="gap-3">
|
|
39
|
+
<Statistics variant="card" size="md" className="gap-3 bg-surface-raised shadow-md">
|
|
41
40
|
<StatisticsHeader>
|
|
42
41
|
<StatisticsHeading><StatisticsTitle size="sm">{kpi.label}</StatisticsTitle></StatisticsHeading>
|
|
43
|
-
<StatisticsActions>{kpi.stale
|
|
44
|
-
? <Badge variant="outline" className="gap-1 text-warning"><Clock className="size-3" aria-hidden /> Cached</Badge>
|
|
45
|
-
: <StatisticsIcon variant="primary" tone="soft" size="sm"><Activity className="size-4" aria-hidden /></StatisticsIcon>}</StatisticsActions>
|
|
42
|
+
<StatisticsActions>{kpi.stale ? <Badge variant="outline" className="gap-1 text-warning"><Clock className="size-3" aria-hidden /> Cached</Badge> : <StatisticsIcon variant="primary" tone="soft" size="sm"><Activity className="size-4" aria-hidden /></StatisticsIcon>}</StatisticsActions>
|
|
46
43
|
</StatisticsHeader>
|
|
47
44
|
<StatisticsMetric>
|
|
48
45
|
<StatisticsValue className="tabular-nums">{kpi.value}{/* mock */}</StatisticsValue>
|
|
49
46
|
{kpi.unit ? <StatisticsUnit>{kpi.unit}</StatisticsUnit> : null}
|
|
50
47
|
{kpi.trend !== 0 ? <StatisticsTrend value={kpi.trend} invert={kpi.invertTrend} pill format={formatPct} /> : null}
|
|
51
48
|
</StatisticsMetric>
|
|
52
|
-
<StatisticsFooter>
|
|
53
|
-
{kpi.stale
|
|
54
|
-
? <StatisticsCaption className="text-warning">Last cached at 14:12</StatisticsCaption>
|
|
55
|
-
: <StatisticsSparkline data={kpi.spark} color={kpi.chart} height={28} className="w-full" />}
|
|
56
|
-
</StatisticsFooter>
|
|
49
|
+
<StatisticsFooter>{kpi.stale ? <StatisticsCaption className="text-warning">Last cached at 14:12</StatisticsCaption> : <StatisticsSparkline data={kpi.spark} color={kpi.chart} height={28} className="w-full" />}</StatisticsFooter>
|
|
57
50
|
</Statistics>
|
|
58
51
|
);
|
|
59
52
|
}
|
|
53
|
+
// The focal hero: the one display-step metric this surface exists to answer, on an elevation.lg raised panel with an optional wash tint, its trend read through the chart tokens.
|
|
54
|
+
function ThroughputHero({ total, trend }: { total: string; trend: LineGraphData }) {
|
|
55
|
+
return (
|
|
56
|
+
<section aria-labelledby="hero-heading" className="flex flex-col gap-4 rounded-xl bg-surface-raised p-6 shadow-lg lg:flex-row lg:items-center lg:justify-between" style={{ backgroundImage: "var(--wash-surface, transparent)" }}>
|
|
57
|
+
<div className="flex flex-col gap-1">
|
|
58
|
+
<h2 id="hero-heading" className="text-small font-medium text-muted-foreground">Records ingested today</h2>
|
|
59
|
+
<div className="flex items-baseline gap-3"><span className="text-display tabular-nums text-foreground">{total}{/* mock */}</span><span className="inline-flex items-center gap-1 text-success"><ArrowUp className="size-4" aria-hidden /><span className="tabular-nums">6.4%</span></span></div>
|
|
60
|
+
<p className="text-body text-muted-foreground">Across every connector, versus the same window yesterday.</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="min-w-0 lg:w-1/2"><LineGraph data={trend} height="sm" filled colors={["var(--chart-1)", "var(--chart-2)"]} /></div>
|
|
63
|
+
</section>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
60
66
|
// One sortable, right-aligned numeric column header carrying its aria-sort state and a keyboard-native toggle.
|
|
61
67
|
function SortHeader({ label, sortKey, active, dir, onSort }: { label: string; sortKey: SortKey; active: boolean; dir: SortDir; onSort: (k: SortKey) => void }) {
|
|
62
68
|
const ariaSort: "none" | "ascending" | "descending" = active ? (dir === "asc" ? "ascending" : "descending") : "none";
|
|
63
69
|
return (
|
|
64
70
|
<th scope="col" aria-sort={ariaSort} className="py-2 pr-4 text-right font-medium">
|
|
65
|
-
<button type="button" onClick={() => onSort(sortKey)} aria-label={`Sort by ${label}`}
|
|
66
|
-
className="
|
|
67
|
-
{label}
|
|
68
|
-
{active ? (dir === "asc" ? <ArrowUp className="size-3" aria-hidden /> : <ArrowDown className="size-3" aria-hidden />) : <ChevronsUpDown className="size-3" aria-hidden />}
|
|
71
|
+
<button type="button" onClick={() => onSort(sortKey)} aria-label={`Sort by ${label}`} className="inline-flex items-center gap-1 rounded text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
72
|
+
{label}{active ? (dir === "asc" ? <ArrowUp className="size-3" aria-hidden /> : <ArrowDown className="size-3" aria-hidden />) : <ChevronsUpDown className="size-3" aria-hidden />}
|
|
69
73
|
</button>
|
|
70
74
|
</th>
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
|
-
// The connectors breakdown: a bounded top-volume table
|
|
77
|
+
// The connectors breakdown: a bounded top-volume table on a raised (elevation.md) panel, sortable numeric columns, per-row health.
|
|
74
78
|
function ConnectorsTile({ connectors }: { connectors: ConnectorRow[] }) {
|
|
75
79
|
const [sortKey, setSortKey] = useState<SortKey>("records");
|
|
76
80
|
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
|
77
81
|
const rows = useMemo(() => [...connectors].sort((a, b) => (sortDir === "asc" ? a[sortKey] - b[sortKey] : b[sortKey] - a[sortKey])), [connectors, sortKey, sortDir]);
|
|
78
82
|
const onSort = (k: SortKey) => k === sortKey ? setSortDir((d) => (d === "asc" ? "desc" : "asc")) : (setSortKey(k), setSortDir("desc"));
|
|
79
83
|
return (
|
|
80
|
-
<Statistics variant="outline" className="gap-4">
|
|
84
|
+
<Statistics variant="outline" className="gap-4 bg-surface-raised shadow-md">
|
|
81
85
|
<StatisticsHeader>
|
|
82
|
-
<StatisticsHeading>
|
|
83
|
-
<StatisticsTitle size="sm">Connectors by volume</StatisticsTitle>
|
|
84
|
-
<StatisticsDescription>Top sources in the selected range.</StatisticsDescription>
|
|
85
|
-
</StatisticsHeading>
|
|
86
|
-
<StatisticsActions>
|
|
87
|
-
<StatisticsLink href="/connectors" className="inline-flex items-center gap-1 text-primary">All connectors <ArrowUpRight className="size-4" aria-hidden /></StatisticsLink>
|
|
88
|
-
</StatisticsActions>
|
|
86
|
+
<StatisticsHeading><StatisticsTitle size="sm">Connectors by volume</StatisticsTitle><StatisticsDescription>Top sources in the selected range.</StatisticsDescription></StatisticsHeading>
|
|
89
87
|
</StatisticsHeader>
|
|
90
88
|
<div className="overflow-x-auto">
|
|
91
|
-
<table className="w-full text-
|
|
89
|
+
<table className="w-full text-small">
|
|
92
90
|
<thead>
|
|
93
|
-
<tr className="border-b border-border text-left text-muted-foreground">
|
|
91
|
+
<tr className="border-b border-border text-left text-caption text-muted-foreground">
|
|
94
92
|
<th scope="col" className="py-2 pr-4 font-medium">Connector</th>
|
|
95
93
|
<SortHeader label="Records" sortKey="records" active={sortKey === "records"} dir={sortDir} onSort={onSort} />
|
|
96
94
|
<SortHeader label="Error rate" sortKey="errorRate" active={sortKey === "errorRate"} dir={sortDir} onSort={onSort} />
|
|
97
|
-
<th scope="col" className="py-2 pl-
|
|
95
|
+
<th scope="col" className="whitespace-nowrap py-2 pl-2 font-medium">Health</th>
|
|
98
96
|
</tr>
|
|
99
97
|
</thead>
|
|
100
98
|
<tbody>
|
|
101
99
|
{rows.map((c) => (
|
|
102
100
|
<tr key={c.id} className="border-b border-border last:border-0">
|
|
103
|
-
<td className="py-2 pr-4">
|
|
104
|
-
<div className="flex items-center gap-2">
|
|
105
|
-
<Database className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
106
|
-
<span className="truncate font-medium text-foreground">{c.name}</span>
|
|
107
|
-
<span className="shrink-0 font-mono text-muted-foreground">{c.kind}</span>
|
|
108
|
-
</div>
|
|
109
|
-
</td>
|
|
101
|
+
<td className="py-2 pr-4"><div className="flex items-center gap-2"><Database className="size-4 shrink-0 text-muted-foreground" aria-hidden /><span className="truncate font-medium text-foreground">{c.name}</span><span className="shrink-0 font-mono text-muted-foreground">{c.kind}</span></div></td>
|
|
110
102
|
<td className="py-2 pr-4 text-right tabular-nums">{c.records.toLocaleString()}</td>
|
|
111
103
|
<td className="py-2 pr-4 text-right tabular-nums">{c.errorRate.toFixed(2)}%</td>
|
|
112
|
-
<td className="py-2 pl-
|
|
104
|
+
<td className="py-2 pl-2"><span className={`inline-flex items-center gap-1 whitespace-nowrap ${HEALTH[c.health].className}`}>{HEALTH[c.health].icon}{HEALTH[c.health].label}</span></td>
|
|
113
105
|
</tr>
|
|
114
106
|
))}
|
|
115
107
|
</tbody>
|
|
@@ -118,63 +110,65 @@ function ConnectorsTile({ connectors }: { connectors: ConnectorRow[] }) {
|
|
|
118
110
|
</Statistics>
|
|
119
111
|
);
|
|
120
112
|
}
|
|
121
|
-
//
|
|
113
|
+
// The ingest-health strip: per-day request outcome, its status roles wired through the BarGraph map; the graph and tally fill the card.
|
|
114
|
+
function HealthStripTile({ days }: { days: BarGraphData[] }) {
|
|
115
|
+
const tally = days.reduce((acc, d) => { const s = d.bar[0]?.status ?? "empty"; acc[s] = (acc[s] ?? 0) + 1; return acc; }, {} as Record<string, number>);
|
|
116
|
+
return (
|
|
117
|
+
<Statistics variant="outline" className="gap-4 bg-surface-raised shadow-md">
|
|
118
|
+
<StatisticsHeader>
|
|
119
|
+
<StatisticsHeading><StatisticsTitle size="sm">Ingest health</StatisticsTitle><StatisticsDescription>Daily request outcome over the last twelve days.</StatisticsDescription></StatisticsHeading>
|
|
120
|
+
<StatisticsActions><a href="/incidents" className="inline-flex items-center gap-1 text-caption text-primary">Incidents <ArrowUpRight className="size-4" aria-hidden /></a></StatisticsActions>
|
|
121
|
+
</StatisticsHeader>
|
|
122
|
+
<div className="flex-1"><BarGraph data={days} height="lg" roundedCorners="all" colors={{ success: "var(--success)", degraded: "var(--warning)", error: "var(--destructive)", info: "var(--primary)", empty: "var(--muted-foreground)" }} /></div>
|
|
123
|
+
<StatisticsFooter>
|
|
124
|
+
<StatisticsCaption className="flex flex-wrap items-center gap-3 tabular-nums">
|
|
125
|
+
<span className="text-success">{tally.success ?? 0} healthy</span><span className="text-warning">{tally.degraded ?? 0} degraded</span><span className="text-destructive">{tally.error ?? 0} errored</span><span>across {days.length} days</span>
|
|
126
|
+
</StatisticsCaption>
|
|
127
|
+
</StatisticsFooter>
|
|
128
|
+
</Statistics>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
// Loading skeleton mirroring the real shape: a hero band, four KPI cards, then two breakdown tiles.
|
|
122
132
|
function DashboardSkeleton() {
|
|
123
133
|
return (
|
|
124
134
|
<div className="flex flex-col gap-6" aria-hidden>
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<Skeleton className="h-4 w-24" /><Skeleton className="h-8 w-32" /><Skeleton className="h-7 w-full" />
|
|
129
|
-
</div>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
133
|
-
{[0, 1].map((i) => (
|
|
134
|
-
<div key={i} className="flex flex-col gap-4 rounded-lg border border-border p-4">
|
|
135
|
-
<Skeleton className="h-4 w-40" /><Skeleton className="h-16 w-full" /><Skeleton className="h-4 w-3/4" />
|
|
136
|
-
</div>
|
|
137
|
-
))}
|
|
138
|
-
</div>
|
|
135
|
+
<Skeleton className="h-28 w-full rounded-xl" />
|
|
136
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">{[0, 1, 2, 3].map((i) => <div key={i} className="flex flex-col gap-3 rounded-lg border border-border p-4"><Skeleton className="h-4 w-24" /><Skeleton className="h-8 w-32" /><Skeleton className="h-7 w-full" /></div>)}</div>
|
|
137
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">{[0, 1].map((i) => <div key={i} className="flex flex-col gap-4 rounded-lg border border-border p-4"><Skeleton className="h-4 w-40" /><Skeleton className="h-16 w-full" /><Skeleton className="h-4 w-3/4" /></div>)}</div>
|
|
139
138
|
</div>
|
|
140
139
|
);
|
|
141
140
|
}
|
|
142
|
-
// Ingestion-operations overview: a read-mostly KPI + breakdown dashboard with every lifecycle state reachable via the status prop.
|
|
141
|
+
// Ingestion-operations overview: a read-mostly hero + KPI + breakdown dashboard with every lifecycle state reachable via the status prop.
|
|
143
142
|
export function OperationsDashboard(props: OperationsDashboardProps) {
|
|
144
143
|
const { status = "ready", degraded = false, lastUpdated = "14:32:08", onRefresh, onRetry, onConnectSource } = props;
|
|
145
144
|
const kpis = props.kpis ?? DEFAULT_KPIS;
|
|
146
145
|
const connectors = props.connectors ?? DEFAULT_CONNECTORS;
|
|
147
146
|
const [timeframe, setTimeframe] = useState<string>("24h");
|
|
148
147
|
return (
|
|
149
|
-
<main className="flex min-h-[100dvh] flex-col bg-background text-foreground" data-density-step="looser">
|
|
148
|
+
<main className="flex min-h-[100dvh] flex-col bg-background font-sans text-foreground" data-density-step="looser">
|
|
150
149
|
<header className="flex flex-col gap-2 border-b border-border px-6 py-4">
|
|
151
150
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
152
151
|
<div className="flex flex-col gap-1">
|
|
153
|
-
<h1 className="text-
|
|
154
|
-
<p className="text-
|
|
152
|
+
<h1 className="text-h1 text-foreground">Ingestion operations</h1>
|
|
153
|
+
<p className="text-small text-muted-foreground">Cross-connector health for the data platform.</p>
|
|
155
154
|
</div>
|
|
156
155
|
<div className="flex items-center gap-2">
|
|
157
156
|
<div className="flex items-center gap-2" role="group" aria-label="Time range">
|
|
158
|
-
{TIMEFRAMES.map((tf) => (
|
|
159
|
-
<Button key={tf.key} size="sm" variant={tf.key === timeframe ? "secondary" : "ghost"} aria-pressed={tf.key === timeframe} onClick={() => setTimeframe(tf.key)}>{tf.label}</Button>
|
|
160
|
-
))}
|
|
157
|
+
{TIMEFRAMES.map((tf) => <Button key={tf.key} size="sm" variant={tf.key === timeframe ? "secondary" : "ghost"} aria-pressed={tf.key === timeframe} onClick={() => setTimeframe(tf.key)}>{tf.label}</Button>)}
|
|
161
158
|
</div>
|
|
162
159
|
<Button size="sm" variant="secondary" onClick={onRefresh}><RefreshCw className="size-4" aria-hidden /> Refresh</Button>
|
|
163
160
|
</div>
|
|
164
161
|
</div>
|
|
165
|
-
<p role="status" aria-live="polite" className="text-
|
|
162
|
+
<p role="status" aria-live="polite" className="text-caption text-muted-foreground">{status === "loading" ? "Loading ingestion overview…" : status === "error" ? "Overview failed to load."
|
|
166
163
|
: <>Updated <span className="font-mono">{lastUpdated}</span>{degraded ? " · latency source stale" : ""}</>}</p>
|
|
167
164
|
</header>
|
|
168
165
|
<div className="flex flex-col gap-6 px-6 py-6">
|
|
169
166
|
{status === "loading" ? <DashboardSkeleton /> : null}
|
|
170
167
|
{status === "error" ? (
|
|
171
168
|
<Alert variant="destructive" role="alert">
|
|
172
|
-
<TriangleAlert className="size-4" aria-hidden
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
The metrics service did not answer within the request timeout, usually a transient gateway hiccup. Retry now, or check the metrics service status if it keeps failing.
|
|
176
|
-
<span className="mt-3 block"><Button size="sm" variant="secondary" onClick={onRetry}><RefreshCw className="size-4" aria-hidden /> Retry</Button></span>
|
|
177
|
-
</AlertDescription>
|
|
169
|
+
<TriangleAlert className="size-4" aria-hidden /><AlertTitle>Overview failed to load</AlertTitle>
|
|
170
|
+
<AlertDescription>The metrics service did not answer within the request timeout, usually a transient gateway hiccup. Retry now, or check the metrics service status if it keeps failing.
|
|
171
|
+
<span className="mt-3 block"><Button size="sm" variant="secondary" onClick={onRetry}><RefreshCw className="size-4" aria-hidden /> Retry</Button></span></AlertDescription>
|
|
178
172
|
</Alert>
|
|
179
173
|
) : null}
|
|
180
174
|
{status === "empty" ? (
|
|
@@ -185,38 +179,17 @@ export function OperationsDashboard(props: OperationsDashboardProps) {
|
|
|
185
179
|
{status === "ready" ? (
|
|
186
180
|
<>
|
|
187
181
|
{degraded ? (
|
|
188
|
-
<Alert role="status">
|
|
189
|
-
<
|
|
190
|
-
<AlertTitle>Latency source unreachable</AlertTitle>
|
|
191
|
-
<AlertDescription>The p95 processing panel is showing the last cached value from 14:12. Every other panel is live.</AlertDescription>
|
|
192
|
-
</Alert>
|
|
182
|
+
<Alert role="status"><Clock className="size-4" aria-hidden /><AlertTitle>Latency source unreachable</AlertTitle>
|
|
183
|
+
<AlertDescription>The p95 processing panel is showing the last cached value from 14:12. Every other panel is live.</AlertDescription></Alert>
|
|
193
184
|
) : null}
|
|
185
|
+
<ThroughputHero total="1,284,930" trend={THROUGHPUT_TREND} />
|
|
194
186
|
<section className="flex flex-col gap-3" aria-labelledby="kpi-heading">
|
|
195
|
-
<h2 id="kpi-heading" className="text-
|
|
196
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">{kpis.map((kpi) => <KpiCard key={kpi.key} kpi={kpi} />)}</div>
|
|
187
|
+
<h2 id="kpi-heading" className="text-small font-medium text-muted-foreground">Today at a glance</h2>
|
|
188
|
+
<div className="motion-stagger grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">{kpis.map((kpi) => <KpiCard key={kpi.key} kpi={kpi} />)}</div>
|
|
197
189
|
</section>
|
|
198
190
|
<section className="flex flex-col gap-3" aria-labelledby="breakdown-heading">
|
|
199
|
-
<h2 id="breakdown-heading" className="text-
|
|
200
|
-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
201
|
-
<Statistics variant="outline" className="gap-4">
|
|
202
|
-
<StatisticsHeader>
|
|
203
|
-
<StatisticsHeading>
|
|
204
|
-
<StatisticsTitle size="sm">Batches by outcome</StatisticsTitle>
|
|
205
|
-
<StatisticsDescription>Last 24 hours across all connectors.</StatisticsDescription>
|
|
206
|
-
</StatisticsHeading>
|
|
207
|
-
<StatisticsActions>
|
|
208
|
-
<StatisticsLink href="/batches" className="inline-flex items-center gap-1 text-primary">Open batches <ArrowUpRight className="size-4" aria-hidden /></StatisticsLink>
|
|
209
|
-
</StatisticsActions>
|
|
210
|
-
</StatisticsHeader>
|
|
211
|
-
<StatisticsSegmentedBar segments={OUTCOME_SEGMENTS} />
|
|
212
|
-
<StatisticsLegend>
|
|
213
|
-
{OUTCOME_SEGMENTS.map((s, i) => (
|
|
214
|
-
<StatisticsLegendItem key={i} color={s.color} label={s.label} value={<span className="tabular-nums">{s.value.toLocaleString()}</span>} />
|
|
215
|
-
))}
|
|
216
|
-
</StatisticsLegend>
|
|
217
|
-
</Statistics>
|
|
218
|
-
<ConnectorsTile connectors={connectors} />
|
|
219
|
-
</div>
|
|
191
|
+
<h2 id="breakdown-heading" className="text-small font-medium text-muted-foreground">Breakdowns</h2>
|
|
192
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"><HealthStripTile days={INGEST_HEALTH} /><ConnectorsTile connectors={connectors} /></div>
|
|
220
193
|
</section>
|
|
221
194
|
</>
|
|
222
195
|
) : null}
|
|
@@ -226,18 +199,22 @@ export function OperationsDashboard(props: OperationsDashboardProps) {
|
|
|
226
199
|
}
|
|
227
200
|
|
|
228
201
|
// exemplar fixture data — seeded, domain-shaped
|
|
229
|
-
const FIXTURE_SEED = 6820;
|
|
202
|
+
export const FIXTURE_SEED = 6820;
|
|
230
203
|
const DEFAULT_KPIS: KpiDatum[] = [
|
|
231
204
|
{ key: "throughput", label: "Records ingested", value: "1,284,930", unit: "today", trend: 6.4, spark: [820, 910, 880, 1020, 1180, 1240, 1284], chart: "var(--chart-1)" },
|
|
232
205
|
{ key: "failures", label: "Batch failure rate", value: "0.42", unit: "%", trend: 1.8, invertTrend: true, spark: [0.3, 0.28, 0.35, 0.31, 0.4, 0.44, 0.42], chart: "var(--chart-5)" },
|
|
233
206
|
{ key: "latency", label: "p95 processing", value: "312", unit: "ms", trend: -4.1, spark: [360, 352, 341, 338, 330, 318, 312], chart: "var(--chart-2)", stale: true },
|
|
234
207
|
{ key: "connectors", label: "Active connectors", value: "18", unit: "of 21", trend: 0, spark: [17, 17, 18, 18, 18, 17, 18], chart: "var(--chart-3)" },
|
|
235
208
|
];
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
]
|
|
209
|
+
const THROUGHPUT_TREND: LineGraphData = {
|
|
210
|
+
labels: ["08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00"],
|
|
211
|
+
series: [{ name: "Ingested", values: [820, 910, 880, 1020, 1180, 1240, 1284] }, { name: "Queued", values: [140, 120, 210, 90, 160, 130, 118] }],
|
|
212
|
+
};
|
|
213
|
+
const INGEST_HEALTH: BarGraphData[] = ["Jun 23", "Jun 24", "Jun 25", "Jun 26", "Jun 27", "Jun 28", "Jun 29", "Jun 30", "Jul 01", "Jul 02", "Jul 03", "Jul 04"].map((day, i) => {
|
|
214
|
+
const status = i === 6 ? "error" : i === 3 || i === 9 ? "degraded" : "success";
|
|
215
|
+
const value = status === "success" ? "100.0% healthy" : status === "degraded" ? "97.4% degraded" : "91.2% errored";
|
|
216
|
+
return { day, bar: [{ status, height: 100 }], card: [{ status, value }], events: [] };
|
|
217
|
+
});
|
|
241
218
|
const DEFAULT_CONNECTORS: ConnectorRow[] = [
|
|
242
219
|
{ id: "cn_stripe_events", name: "Stripe billing events", kind: "webhook", records: 486210, errorRate: 0.08, health: "healthy" },
|
|
243
220
|
{ id: "cn_salesforce_crm", name: "Salesforce CRM accounts and opportunities", kind: "rest", records: 312440, errorRate: 0.31, health: "healthy" },
|