@nurix/etna 0.18.19 → 0.18.20
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 +2 -2
- package/data/harnesses/code/skills/design/SKILL.md +65 -0
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
- package/data/harnesses/code/skills/design/references/create.md +126 -0
- package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
- package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
- package/data/harnesses/code/skills/design/references/improve.md +85 -0
- package/data/harnesses/code/skills/design/references/interaction.md +77 -0
- package/data/harnesses/code/skills/design/references/introspect.md +100 -0
- package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
- package/data/harnesses/code/skills/design/references/personality.md +504 -0
- package/data/harnesses/code/skills/design/references/structure.md +138 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
- package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
- package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
- package/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/create-edit.tsx
|
|
2
|
+
// personality-invariant: composition anchor, not a look source.
|
|
3
|
+
import { useState, type FormEvent, type ReactNode } from "react";
|
|
4
|
+
import { TriangleAlert, CircleCheck, Plus, RotateCcw } from "lucide-react";
|
|
5
|
+
import { Sheet, SheetContent, SheetHeader, SheetBody, SheetFooter, SheetTitle, SheetDescription } from "@nurix/components/sheet";
|
|
6
|
+
import { Button } from "@nurix/components/button";
|
|
7
|
+
import { Input } from "@nurix/components/input";
|
|
8
|
+
import { NumberInput } from "@nurix/components/number-input";
|
|
9
|
+
import { Switch } from "@nurix/components/switch";
|
|
10
|
+
import { Checkbox } from "@nurix/components/checkbox";
|
|
11
|
+
import { Label } from "@nurix/components/label";
|
|
12
|
+
import { Badge } from "@nurix/components/badge";
|
|
13
|
+
import { Kbd } from "@nurix/components/kbd";
|
|
14
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
15
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
16
|
+
import { Alert, AlertTitle, AlertDescription } from "@nurix/components/alert";
|
|
17
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@nurix/components/native-select";
|
|
18
|
+
import { RadioGroupSegmented, RadioGroupSegmentedItem } from "@nurix/components/radio-group";
|
|
19
|
+
|
|
20
|
+
export type RuleSurfaceStatus = "loading" | "empty" | "error" | "degraded" | "ready";
|
|
21
|
+
export type ThresholdDirection = "above" | "below";
|
|
22
|
+
type FormPhase = "form" | "preview" | "success";
|
|
23
|
+
type SubmitStatus = "idle" | "submitting" | "failed";
|
|
24
|
+
type FieldErrors = Partial<Record<keyof AlertRuleDraft, string>>;
|
|
25
|
+
|
|
26
|
+
export interface NotifyChannel { id: string; label: string; kind: "email" | "webhook" | "pager"; }
|
|
27
|
+
export interface MetricOption { slug: string; label: string; unit: string; }
|
|
28
|
+
export interface SubmitOutcome { ok: boolean; message?: string; }
|
|
29
|
+
|
|
30
|
+
export interface AlertRuleDraft {
|
|
31
|
+
name: string; metricSlug: string; direction: ThresholdDirection;
|
|
32
|
+
threshold: number | null; channelIds: string[]; enabled: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateEditRuleSurfaceProps {
|
|
36
|
+
mode: "create" | "edit"; status: RuleSurfaceStatus; open: boolean;
|
|
37
|
+
metrics: MetricOption[]; channels: NotifyChannel[]; metricsCachedAt?: string | null;
|
|
38
|
+
initial?: Partial<AlertRuleDraft>; onSubmit: (draft: AlertRuleDraft) => Promise<SubmitOutcome>;
|
|
39
|
+
onCancel: () => void; onAddChannel: () => void; onRetryLoad: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const BLANK_DRAFT: AlertRuleDraft = { name: "", metricSlug: "", direction: "above", threshold: null, channelIds: [], enabled: true };
|
|
43
|
+
|
|
44
|
+
// Collect blocking errors for the whole draft; re-run on every render so blur and submit share one source.
|
|
45
|
+
function validate(draft: AlertRuleDraft): FieldErrors {
|
|
46
|
+
const errors: FieldErrors = {};
|
|
47
|
+
if (draft.name.trim().length < 3) errors.name = "Name the rule (3 characters or more) so it's findable in the alert list.";
|
|
48
|
+
if (!draft.metricSlug) errors.metricSlug = "Choose the metric this rule watches.";
|
|
49
|
+
if (draft.threshold == null) errors.threshold = "Set the threshold value to compare against.";
|
|
50
|
+
if (draft.channelIds.length === 0) errors.channelIds = "Add at least one channel so alerts have somewhere to go.";
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Label-over-input wrapper: the visible label, the control, then an inline error (or hint) below.
|
|
55
|
+
function Field({ id, label, hint, error, htmlFor, children }: { id: string; label: string; hint?: string; error?: string; htmlFor?: string; children: ReactNode }) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="grid gap-1.5">
|
|
58
|
+
<Label id={`${id}-label`} htmlFor={htmlFor} className="text-sm font-medium">{label}</Label>
|
|
59
|
+
{children}
|
|
60
|
+
{error ? <p id={`${id}-error`} className="text-sm text-destructive">{error}</p> : hint ? <p className="text-sm text-muted-foreground">{hint}</p> : null}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
// One label/value row inside the preview summary.
|
|
65
|
+
function PreviewRow({ label, children }: { label: string; children: ReactNode }) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="grid grid-cols-[8rem_1fr] gap-4">
|
|
68
|
+
<dt className="text-muted-foreground">{label}</dt>
|
|
69
|
+
<dd className="text-foreground">{children}</dd>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// The create/edit alert-rule surface: a side sheet that opens beside the alert list and returns to it on close.
|
|
74
|
+
export function CreateEditRuleSurface(props: CreateEditRuleSurfaceProps) {
|
|
75
|
+
const { mode, status, open, metrics, channels, metricsCachedAt, onSubmit, onCancel, onAddChannel, onRetryLoad } = props;
|
|
76
|
+
const [draft, setDraft] = useState<AlertRuleDraft>(() => ({ ...BLANK_DRAFT, ...props.initial }));
|
|
77
|
+
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
78
|
+
const [attempted, setAttempted] = useState(false);
|
|
79
|
+
const [phase, setPhase] = useState<FormPhase>("form");
|
|
80
|
+
const [submitStatus, setSubmitStatus] = useState<SubmitStatus>("idle");
|
|
81
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
82
|
+
const errors = validate(draft);
|
|
83
|
+
const metric = metrics.find((m) => m.slug === draft.metricSlug);
|
|
84
|
+
const selectedChannels = channels.filter((c) => draft.channelIds.includes(c.id));
|
|
85
|
+
const isForm = status === "ready" || status === "degraded";
|
|
86
|
+
const busy = submitStatus === "submitting";
|
|
87
|
+
function patch<K extends keyof AlertRuleDraft>(key: K, value: AlertRuleDraft[K]) { setDraft((d) => ({ ...d, [key]: value })); }
|
|
88
|
+
function blur(key: keyof AlertRuleDraft) { setTouched((t) => ({ ...t, [key]: true })); }
|
|
89
|
+
const show = (key: keyof AlertRuleDraft): string | undefined => ((attempted || touched[key]) && errors[key]) || undefined;
|
|
90
|
+
// Validate the whole draft; a clean draft advances to the preview-then-commit step.
|
|
91
|
+
function handlePreview(event: FormEvent) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
setAttempted(true);
|
|
94
|
+
setSubmitError(null);
|
|
95
|
+
if (Object.keys(errors).length === 0) setPhase("preview");
|
|
96
|
+
}
|
|
97
|
+
// Commit the reviewed draft; failure returns to the form with entries preserved and the reason inline.
|
|
98
|
+
async function handleConfirm() {
|
|
99
|
+
setSubmitStatus("submitting");
|
|
100
|
+
const outcome = await onSubmit(draft);
|
|
101
|
+
if (outcome.ok) { setSubmitStatus("idle"); setPhase("success"); }
|
|
102
|
+
else { setSubmitStatus("failed"); setSubmitError(outcome.message ?? "The alerting service rejected the rule."); setPhase("form"); }
|
|
103
|
+
}
|
|
104
|
+
function reset() { setDraft({ ...BLANK_DRAFT }); setTouched({}); setAttempted(false); setSubmitStatus("idle"); setSubmitError(null); setPhase("form"); }
|
|
105
|
+
return (
|
|
106
|
+
<Sheet open={open} onOpenChange={(next) => { if (!next) onCancel(); }}>
|
|
107
|
+
<SheetContent side="right" size="lg" data-density-step="looser">
|
|
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>
|
|
111
|
+
</SheetHeader>
|
|
112
|
+
<SheetBody>
|
|
113
|
+
{status === "loading" && (
|
|
114
|
+
<div className="grid gap-6" aria-hidden="true">
|
|
115
|
+
{[0, 1, 2, 3].map((i) => <div key={i} className="grid gap-2"><Skeleton className="h-4 w-28" /><Skeleton className="h-9 w-full" /></div>)}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
{status === "error" && (
|
|
119
|
+
<div className="grid justify-items-center gap-4 py-8 text-center" role="alert">
|
|
120
|
+
<TriangleAlert className="size-8 text-destructive" />
|
|
121
|
+
<p className="text-base font-semibold text-foreground">Couldn't load this rule</p>
|
|
122
|
+
<p className="max-w-sm text-sm 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
|
+
<Button variant="outline" onClick={onRetryLoad}><RotateCcw className="size-4" /> Retry</Button>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
{status === "empty" && (
|
|
127
|
+
<EmptyState
|
|
128
|
+
icon={<Plus className="size-6 text-muted-foreground" />}
|
|
129
|
+
title="No notification channels yet"
|
|
130
|
+
description="An alert rule needs somewhere to send alerts. Add a channel, then come back to finish this rule."
|
|
131
|
+
action={<Button onClick={onAddChannel}><Plus className="size-4" /> Add a channel</Button>}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
{isForm && phase === "form" && (
|
|
135
|
+
<form id="rule-form" onSubmit={handlePreview} className="grid gap-6">
|
|
136
|
+
{status === "degraded" && (
|
|
137
|
+
<Alert role="status" aria-live="polite">
|
|
138
|
+
<TriangleAlert className="size-4" /><AlertTitle>Metric list may be stale</AlertTitle>
|
|
139
|
+
<AlertDescription>The metrics service is unreachable, so this is a cached catalog from {metricsCachedAt}. A metric added since then won't appear here yet.</AlertDescription>
|
|
140
|
+
</Alert>
|
|
141
|
+
)}
|
|
142
|
+
{submitStatus === "failed" && submitError && (
|
|
143
|
+
<Alert variant="destructive" role="alert">
|
|
144
|
+
<TriangleAlert className="size-4" /><AlertTitle>Rule not created</AlertTitle>
|
|
145
|
+
<AlertDescription>{submitError} Your entries are preserved, adjust and try again.</AlertDescription>
|
|
146
|
+
</Alert>
|
|
147
|
+
)}
|
|
148
|
+
<Field id="name" label="Rule name" htmlFor="name" error={show("name")}>
|
|
149
|
+
<Input id="name" value={draft.name} placeholder="API error rate over 2 percent" onChange={(e) => patch("name", e.target.value)} onBlur={() => blur("name")} aria-invalid={show("name") ? true : undefined} aria-describedby={show("name") ? "name-error" : undefined} />
|
|
150
|
+
</Field>
|
|
151
|
+
<Field id="metric" label="Metric" htmlFor="metric" error={show("metricSlug")}>
|
|
152
|
+
<Select value={draft.metricSlug} onValueChange={(v) => { patch("metricSlug", v); blur("metricSlug"); }}>
|
|
153
|
+
<SelectTrigger id="metric" aria-invalid={show("metricSlug") ? true : undefined}><SelectValue placeholder="Choose a metric to watch" /></SelectTrigger>
|
|
154
|
+
<SelectContent>{metrics.map((m) => <SelectItem key={m.slug} value={m.slug}>{m.label}</SelectItem>)}</SelectContent>
|
|
155
|
+
</Select>
|
|
156
|
+
</Field>
|
|
157
|
+
<div className="grid grid-cols-2 gap-4">
|
|
158
|
+
<Field id="direction" label="Condition">
|
|
159
|
+
<RadioGroupSegmented value={draft.direction} aria-labelledby="direction-label" onValueChange={(v) => patch("direction", v as ThresholdDirection)}>
|
|
160
|
+
<RadioGroupSegmentedItem value="above">Above</RadioGroupSegmentedItem>
|
|
161
|
+
<RadioGroupSegmentedItem value="below">Below</RadioGroupSegmentedItem>
|
|
162
|
+
</RadioGroupSegmented>
|
|
163
|
+
</Field>
|
|
164
|
+
<Field id="threshold" label={`Threshold${metric ? ` (${metric.unit})` : ""}`} htmlFor="threshold" error={show("threshold")}>
|
|
165
|
+
<NumberInput id="threshold" className="tabular-nums" value={draft.threshold} step={0.5} allowDecimals onSave={(v) => patch("threshold", v)} onBlur={() => blur("threshold")} aria-invalid={show("threshold") ? true : undefined} aria-describedby={show("threshold") ? "threshold-error" : undefined} />
|
|
166
|
+
</Field>
|
|
167
|
+
</div>
|
|
168
|
+
<Field id="channels" label="Notify channels" error={show("channelIds")}>
|
|
169
|
+
<div className="grid gap-2" role="group" aria-labelledby="channels-label">
|
|
170
|
+
{channels.map((c) => (
|
|
171
|
+
<Label key={c.id} className="flex items-center gap-3 text-sm font-normal">
|
|
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
|
+
<span className="text-foreground">{c.label}</span>
|
|
174
|
+
<Badge variant="outline" className="ml-auto">{c.kind}</Badge>
|
|
175
|
+
</Label>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</Field>
|
|
179
|
+
<Field id="enabled" label="Enable on save">
|
|
180
|
+
<div className="flex items-center gap-3">
|
|
181
|
+
<Switch checked={draft.enabled} aria-labelledby="enabled-label" onCheckedChange={(v) => patch("enabled", v)} />
|
|
182
|
+
<span className="text-sm text-muted-foreground">{draft.enabled ? "Starts evaluating as soon as it's created." : "Saved paused, enable it later from the alert list."}</span>
|
|
183
|
+
</div>
|
|
184
|
+
</Field>
|
|
185
|
+
</form>
|
|
186
|
+
)}
|
|
187
|
+
{isForm && phase === "preview" && (
|
|
188
|
+
<div className="grid gap-4">
|
|
189
|
+
<p className="text-base font-semibold text-foreground">Review before creating</p>
|
|
190
|
+
<dl className="grid gap-3 text-sm">
|
|
191
|
+
<PreviewRow label="Rule"><span className="font-medium text-foreground">{draft.name}</span></PreviewRow>
|
|
192
|
+
<PreviewRow label="Condition">{metric?.label} goes {draft.direction} <span className="font-mono tabular-nums">{draft.threshold}</span> {metric?.unit}</PreviewRow>
|
|
193
|
+
<PreviewRow label="Channels">{selectedChannels.map((c) => c.label).join(", ")}</PreviewRow>
|
|
194
|
+
<PreviewRow label="State">{draft.enabled ? "Enabled on create" : "Created paused"}</PreviewRow>
|
|
195
|
+
</dl>
|
|
196
|
+
<p className="text-sm text-muted-foreground">A rule that pages is hard to unsend, so confirm the scope before creating.</p>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
{isForm && phase === "success" && (
|
|
200
|
+
<div className="grid justify-items-center gap-4 py-8 text-center" role="status" aria-live="polite">
|
|
201
|
+
<CircleCheck className="size-8 text-success" />
|
|
202
|
+
<p className="text-base font-semibold text-foreground">Alert rule created</p>
|
|
203
|
+
<p className="max-w-sm text-sm 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
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
<Button variant="outline" onClick={onCancel}>View rule</Button>
|
|
206
|
+
<Button onClick={reset}>Create another rule</Button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</SheetBody>
|
|
211
|
+
{isForm && phase !== "success" && (
|
|
212
|
+
<SheetFooter showDivider>
|
|
213
|
+
<div className="flex w-full items-center justify-between gap-4">
|
|
214
|
+
<p className="text-sm text-muted-foreground" role="status" aria-live="polite">
|
|
215
|
+
{busy ? "Creating rule" : phase === "form" ? <>Press <Kbd size="sm">Enter</Kbd> to review before creating.</> : "Nothing is created until you confirm."}
|
|
216
|
+
</p>
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
{phase === "form" ? (
|
|
219
|
+
<>
|
|
220
|
+
<Button type="button" variant="ghost" onClick={onCancel}>Cancel</Button>
|
|
221
|
+
<Button type="submit" form="rule-form" size="lg">{mode === "create" ? "Create rule" : "Save changes"}</Button>
|
|
222
|
+
</>
|
|
223
|
+
) : (
|
|
224
|
+
<>
|
|
225
|
+
<Button type="button" variant="ghost" onClick={() => setPhase("form")} disabled={busy}>Back to edit</Button>
|
|
226
|
+
<Button type="button" size="lg" onClick={handleConfirm} disabled={busy}>{busy ? "Creating rule" : "Confirm and create"}</Button>
|
|
227
|
+
</>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</SheetFooter>
|
|
232
|
+
)}
|
|
233
|
+
</SheetContent>
|
|
234
|
+
</Sheet>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
239
|
+
export const metricCatalog: MetricOption[] = [
|
|
240
|
+
{ slug: "api_error_rate", label: "API error rate", unit: "%" },
|
|
241
|
+
{ slug: "p95_latency", label: "p95 response time", unit: "ms" },
|
|
242
|
+
{ slug: "queue_depth", label: "Ingest queue depth", unit: "jobs" },
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
export const notifyChannels: NotifyChannel[] = [
|
|
246
|
+
{ id: "ch_oncall_primary", label: "On-call primary", kind: "pager" },
|
|
247
|
+
{ id: "ch_platform_alerts", label: "#platform-alerts", kind: "webhook" },
|
|
248
|
+
{ id: "ch_sre_mailing", label: "sre-team mailing list", kind: "email" },
|
|
249
|
+
];
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/dashboard.tsx — personality-invariant: composition anchor, not a look source.
|
|
2
|
+
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";
|
|
7
|
+
import { Alert, AlertDescription, AlertTitle } from "@nurix/components/alert";
|
|
8
|
+
import { Badge } from "@nurix/components/badge";
|
|
9
|
+
import { Button } from "@nurix/components/button";
|
|
10
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
11
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
12
|
+
import {
|
|
13
|
+
Statistics, StatisticsActions, StatisticsCaption, StatisticsDescription, StatisticsFooter,
|
|
14
|
+
StatisticsHeader, StatisticsHeading, StatisticsIcon, StatisticsLegend, StatisticsLegendItem,
|
|
15
|
+
StatisticsLink, StatisticsMetric, StatisticsSegmentedBar, StatisticsSparkline, StatisticsTitle,
|
|
16
|
+
StatisticsTrend, StatisticsUnit, StatisticsValue, type StatisticsSegment,
|
|
17
|
+
} from "@nurix/components/statistics";
|
|
18
|
+
|
|
19
|
+
type SurfaceStatus = "loading" | "ready" | "empty" | "error";
|
|
20
|
+
type SortKey = "records" | "errorRate";
|
|
21
|
+
type SortDir = "asc" | "desc";
|
|
22
|
+
type Health = "healthy" | "elevated" | "failing";
|
|
23
|
+
interface KpiDatum { key: string; label: string; value: string; unit?: string; trend: number; invertTrend?: boolean; spark: number[]; chart: string; stale?: boolean }
|
|
24
|
+
interface ConnectorRow { id: string; name: string; kind: string; records: number; errorRate: number; health: Health }
|
|
25
|
+
export interface OperationsDashboardProps {
|
|
26
|
+
status?: SurfaceStatus; degraded?: boolean; kpis?: KpiDatum[]; connectors?: ConnectorRow[];
|
|
27
|
+
lastUpdated?: string; onRefresh?: () => void; onRetry?: () => void; onConnectSource?: () => void;
|
|
28
|
+
}
|
|
29
|
+
const TIMEFRAMES = [{ key: "24h", label: "24h" }, { key: "7d", label: "7d" }, { key: "30d", label: "30d" }] as const;
|
|
30
|
+
const HEALTH: Record<Health, { label: string; className: string; icon: ReactNode }> = {
|
|
31
|
+
healthy: { label: "Healthy", className: "text-success", icon: <Activity className="size-4" aria-hidden /> },
|
|
32
|
+
elevated: { label: "Elevated", className: "text-warning", icon: <TriangleAlert className="size-4" aria-hidden /> },
|
|
33
|
+
failing: { label: "Failing", className: "text-destructive", icon: <CircleAlert className="size-4" aria-hidden /> },
|
|
34
|
+
};
|
|
35
|
+
// Signed percentage for a KPI trend chip.
|
|
36
|
+
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.
|
|
38
|
+
function KpiCard({ kpi }: { kpi: KpiDatum }) {
|
|
39
|
+
return (
|
|
40
|
+
<Statistics variant="card" size="md" className="gap-3">
|
|
41
|
+
<StatisticsHeader>
|
|
42
|
+
<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>
|
|
46
|
+
</StatisticsHeader>
|
|
47
|
+
<StatisticsMetric>
|
|
48
|
+
<StatisticsValue className="tabular-nums">{kpi.value}{/* mock */}</StatisticsValue>
|
|
49
|
+
{kpi.unit ? <StatisticsUnit>{kpi.unit}</StatisticsUnit> : null}
|
|
50
|
+
{kpi.trend !== 0 ? <StatisticsTrend value={kpi.trend} invert={kpi.invertTrend} pill format={formatPct} /> : null}
|
|
51
|
+
</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>
|
|
57
|
+
</Statistics>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
// One sortable, right-aligned numeric column header carrying its aria-sort state and a keyboard-native toggle.
|
|
61
|
+
function SortHeader({ label, sortKey, active, dir, onSort }: { label: string; sortKey: SortKey; active: boolean; dir: SortDir; onSort: (k: SortKey) => void }) {
|
|
62
|
+
const ariaSort: "none" | "ascending" | "descending" = active ? (dir === "asc" ? "ascending" : "descending") : "none";
|
|
63
|
+
return (
|
|
64
|
+
<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="inline-flex items-center gap-1 rounded text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
67
|
+
{label}
|
|
68
|
+
{active ? (dir === "asc" ? <ArrowUp className="size-3" aria-hidden /> : <ArrowDown className="size-3" aria-hidden />) : <ChevronsUpDown className="size-3" aria-hidden />}
|
|
69
|
+
</button>
|
|
70
|
+
</th>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// The connectors breakdown: a bounded top-volume table with sortable numeric columns and per-row health.
|
|
74
|
+
function ConnectorsTile({ connectors }: { connectors: ConnectorRow[] }) {
|
|
75
|
+
const [sortKey, setSortKey] = useState<SortKey>("records");
|
|
76
|
+
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
|
77
|
+
const rows = useMemo(() => [...connectors].sort((a, b) => (sortDir === "asc" ? a[sortKey] - b[sortKey] : b[sortKey] - a[sortKey])), [connectors, sortKey, sortDir]);
|
|
78
|
+
const onSort = (k: SortKey) => k === sortKey ? setSortDir((d) => (d === "asc" ? "desc" : "asc")) : (setSortKey(k), setSortDir("desc"));
|
|
79
|
+
return (
|
|
80
|
+
<Statistics variant="outline" className="gap-4">
|
|
81
|
+
<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>
|
|
89
|
+
</StatisticsHeader>
|
|
90
|
+
<div className="overflow-x-auto">
|
|
91
|
+
<table className="w-full text-sm">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr className="border-b border-border text-left text-muted-foreground">
|
|
94
|
+
<th scope="col" className="py-2 pr-4 font-medium">Connector</th>
|
|
95
|
+
<SortHeader label="Records" sortKey="records" active={sortKey === "records"} dir={sortDir} onSort={onSort} />
|
|
96
|
+
<SortHeader label="Error rate" sortKey="errorRate" active={sortKey === "errorRate"} dir={sortDir} onSort={onSort} />
|
|
97
|
+
<th scope="col" className="py-2 pl-4 font-medium">Health</th>
|
|
98
|
+
</tr>
|
|
99
|
+
</thead>
|
|
100
|
+
<tbody>
|
|
101
|
+
{rows.map((c) => (
|
|
102
|
+
<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>
|
|
110
|
+
<td className="py-2 pr-4 text-right tabular-nums">{c.records.toLocaleString()}</td>
|
|
111
|
+
<td className="py-2 pr-4 text-right tabular-nums">{c.errorRate.toFixed(2)}%</td>
|
|
112
|
+
<td className="py-2 pl-4"><span className={`inline-flex items-center gap-1 ${HEALTH[c.health].className}`}>{HEALTH[c.health].icon}{HEALTH[c.health].label}</span></td>
|
|
113
|
+
</tr>
|
|
114
|
+
))}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
</Statistics>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Loading skeleton mirroring the real shape: four KPI cards over two breakdown tiles.
|
|
122
|
+
function DashboardSkeleton() {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex flex-col gap-6" aria-hidden>
|
|
125
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
126
|
+
{[0, 1, 2, 3].map((i) => (
|
|
127
|
+
<div key={i} className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
|
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>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// Ingestion-operations overview: a read-mostly KPI + breakdown dashboard with every lifecycle state reachable via the status prop.
|
|
143
|
+
export function OperationsDashboard(props: OperationsDashboardProps) {
|
|
144
|
+
const { status = "ready", degraded = false, lastUpdated = "14:32:08", onRefresh, onRetry, onConnectSource } = props;
|
|
145
|
+
const kpis = props.kpis ?? DEFAULT_KPIS;
|
|
146
|
+
const connectors = props.connectors ?? DEFAULT_CONNECTORS;
|
|
147
|
+
const [timeframe, setTimeframe] = useState<string>("24h");
|
|
148
|
+
return (
|
|
149
|
+
<main className="flex min-h-[100dvh] flex-col bg-background text-foreground" data-density-step="looser">
|
|
150
|
+
<header className="flex flex-col gap-2 border-b border-border px-6 py-4">
|
|
151
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
152
|
+
<div className="flex flex-col gap-1">
|
|
153
|
+
<h1 className="text-xl font-medium text-foreground">Ingestion operations</h1>
|
|
154
|
+
<p className="text-sm text-muted-foreground">Cross-connector health for the data platform.</p>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<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
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
<Button size="sm" variant="secondary" onClick={onRefresh}><RefreshCw className="size-4" aria-hidden /> Refresh</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<p role="status" aria-live="polite" className="text-xs text-muted-foreground">{status === "loading" ? "Loading ingestion overview…" : status === "error" ? "Overview failed to load."
|
|
166
|
+
: <>Updated <span className="font-mono">{lastUpdated}</span>{degraded ? " · latency source stale" : ""}</>}</p>
|
|
167
|
+
</header>
|
|
168
|
+
<div className="flex flex-col gap-6 px-6 py-6">
|
|
169
|
+
{status === "loading" ? <DashboardSkeleton /> : null}
|
|
170
|
+
{status === "error" ? (
|
|
171
|
+
<Alert variant="destructive" role="alert">
|
|
172
|
+
<TriangleAlert className="size-4" aria-hidden />
|
|
173
|
+
<AlertTitle>Overview failed to load</AlertTitle>
|
|
174
|
+
<AlertDescription>
|
|
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>
|
|
178
|
+
</Alert>
|
|
179
|
+
) : null}
|
|
180
|
+
{status === "empty" ? (
|
|
181
|
+
<EmptyState icon={<Plug className="size-6 text-muted-foreground" aria-hidden />} title="No connectors yet"
|
|
182
|
+
description="Connect a data source to start ingesting records. Throughput, failure rates, and latency appear here once the first batch runs."
|
|
183
|
+
action={<Button size="sm" onClick={onConnectSource}><Plug className="size-4" aria-hidden /> Connect a source</Button>} />
|
|
184
|
+
) : null}
|
|
185
|
+
{status === "ready" ? (
|
|
186
|
+
<>
|
|
187
|
+
{degraded ? (
|
|
188
|
+
<Alert role="status">
|
|
189
|
+
<Clock className="size-4" aria-hidden />
|
|
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>
|
|
193
|
+
) : null}
|
|
194
|
+
<section className="flex flex-col gap-3" aria-labelledby="kpi-heading">
|
|
195
|
+
<h2 id="kpi-heading" className="text-sm font-medium text-muted-foreground">Today at a glance</h2>
|
|
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>
|
|
197
|
+
</section>
|
|
198
|
+
<section className="flex flex-col gap-3" aria-labelledby="breakdown-heading">
|
|
199
|
+
<h2 id="breakdown-heading" className="text-sm font-medium text-muted-foreground">Breakdowns</h2>
|
|
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>
|
|
220
|
+
</section>
|
|
221
|
+
</>
|
|
222
|
+
) : null}
|
|
223
|
+
</div>
|
|
224
|
+
</main>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
229
|
+
const FIXTURE_SEED = 6820;
|
|
230
|
+
const DEFAULT_KPIS: KpiDatum[] = [
|
|
231
|
+
{ 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
|
+
{ 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
|
+
{ 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
|
+
{ 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
|
+
];
|
|
236
|
+
const OUTCOME_SEGMENTS: StatisticsSegment[] = [
|
|
237
|
+
{ label: "Succeeded", value: 8420, color: "var(--chart-3)" },
|
|
238
|
+
{ label: "Retried", value: 612, color: "var(--chart-4)" },
|
|
239
|
+
{ label: "Failed", value: 148, color: "var(--chart-5)" },
|
|
240
|
+
];
|
|
241
|
+
const DEFAULT_CONNECTORS: ConnectorRow[] = [
|
|
242
|
+
{ id: "cn_stripe_events", name: "Stripe billing events", kind: "webhook", records: 486210, errorRate: 0.08, health: "healthy" },
|
|
243
|
+
{ id: "cn_salesforce_crm", name: "Salesforce CRM accounts and opportunities", kind: "rest", records: 312440, errorRate: 0.31, health: "healthy" },
|
|
244
|
+
{ id: "cn_postgres_orders", name: "Postgres orders replica", kind: "cdc", records: 274900, errorRate: 1.94, health: "elevated" },
|
|
245
|
+
{ id: "cn_segment_web", name: "Segment web analytics", kind: "stream", records: 158030, errorRate: 0.12, health: "healthy" },
|
|
246
|
+
{ id: "cn_zendesk_tickets", name: "Zendesk support tickets", kind: "rest", records: 42110, errorRate: 6.71, health: "failing" },
|
|
247
|
+
{ id: "cn_s3_archive", name: "S3 nightly archive export", kind: "batch", records: 11240, errorRate: 0, health: "healthy" },
|
|
248
|
+
];
|