lopata 0.0.1

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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,287 @@
1
+ import type { OverviewData, WorkerInfo } from "../rpc/types";
2
+ import { useQuery } from "../rpc/hooks";
3
+ import { StatusBadge } from "../components";
4
+
5
+ /* ── Inventory items ── */
6
+
7
+ const INVENTORY = [
8
+ { key: "kv", label: "KV", path: "/kv", icon: "⬡", color: "text-blue-400" },
9
+ { key: "r2", label: "R2", path: "/r2", icon: "◧", color: "text-violet-400" },
10
+ { key: "d1", label: "D1", path: "/d1", icon: "⊞", color: "text-cyan-400" },
11
+ { key: "cache", label: "Cache", path: "/cache", icon: "◎", color: "text-teal-400" },
12
+ { key: "do", label: "DO", path: "/do", icon: "⬢", color: "text-emerald-400" },
13
+ { key: "workflows", label: "Workflows", path: "/workflows", icon: "⇶", color: "text-amber-400" },
14
+ { key: "containers", label: "Containers", path: "/containers", icon: "▣", color: "text-indigo-400" },
15
+ { key: "scheduled", label: "Scheduled", path: "/scheduled", icon: "⏱\uFE0E", color: "text-orange-400" },
16
+ { key: "queue", label: "Queues", path: "/queue", icon: "☰", color: "text-yellow-400" },
17
+ { key: "email", label: "Email", path: "/email", icon: "✉\uFE0E", color: "text-pink-400" },
18
+ { key: "ai", label: "AI", path: "/ai", icon: "⚡", color: "text-purple-400" },
19
+ ] as const;
20
+
21
+ const BINDING_COLORS: Record<string, string> = {
22
+ kv: "bg-blue-500/15 text-blue-400",
23
+ r2: "bg-violet-500/15 text-violet-400",
24
+ d1: "bg-cyan-500/15 text-cyan-400",
25
+ do: "bg-emerald-500/15 text-emerald-400",
26
+ queue: "bg-yellow-500/15 text-yellow-400",
27
+ workflow: "bg-amber-500/15 text-amber-400",
28
+ service: "bg-neutral-500/15 text-neutral-400",
29
+ images: "bg-pink-500/15 text-pink-400",
30
+ container: "bg-indigo-500/15 text-indigo-400",
31
+ ai: "bg-purple-500/15 text-purple-400",
32
+ };
33
+
34
+ /* ── Formatters ── */
35
+
36
+ function fmtBytes(b: number): string {
37
+ if (b < 1024) return `${b} B`;
38
+ if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`;
39
+ if (b < 1073741824) return `${(b / 1048576).toFixed(1)} MB`;
40
+ return `${(b / 1073741824).toFixed(2)} GB`;
41
+ }
42
+
43
+ function fmtUptime(s: number): string {
44
+ const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600),
45
+ m = Math.floor((s % 3600) / 60), sec = Math.floor(s % 60);
46
+ const p: string[] = [];
47
+ if (d) p.push(`${d}d`);
48
+ if (h) p.push(`${h}h`);
49
+ if (m) p.push(`${m}m`);
50
+ p.push(`${sec}s`);
51
+ return p.join(" ");
52
+ }
53
+
54
+ function fmtMicros(us: number): string {
55
+ if (us < 1000) return `${us}µs`;
56
+ if (us < 1_000_000) return `${(us / 1000).toFixed(1)}ms`;
57
+ return `${(us / 1_000_000).toFixed(2)}s`;
58
+ }
59
+
60
+ /* ── Gauge with semantic color ── */
61
+
62
+ function gaugeColor(pct: number): string {
63
+ if (pct > 90) return "bg-red-500";
64
+ if (pct > 70) return "bg-amber-500";
65
+ return "bg-blue-500";
66
+ }
67
+
68
+ function MiniGauge({ value, max, label }: { value: number; max: number; label: string }) {
69
+ const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
70
+ return (
71
+ <div>
72
+ <div class="flex justify-between items-baseline mb-1.5">
73
+ <span class="text-xs text-text-muted">{label}</span>
74
+ <span class="text-xs font-mono tabular-nums text-ink">{fmtBytes(value)}</span>
75
+ </div>
76
+ <div class="h-2 rounded-full bg-bar overflow-hidden">
77
+ <div class={`h-full rounded-full transition-all ${gaugeColor(pct)}`} style={{ width: `${Math.max(pct, 2)}%` }} />
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function Kv({ k, v }: { k: string; v: string }) {
84
+ return (
85
+ <div class="flex justify-between items-baseline gap-6 py-0.5">
86
+ <span class="text-xs text-text-muted shrink-0">{k}</span>
87
+ <span class="text-xs font-mono tabular-nums text-ink truncate text-right" title={v}>{v}</span>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ function Section({ title, children }: { title: string; children: any }) {
93
+ return (
94
+ <div>
95
+ <h2 class="text-xs font-semibold uppercase tracking-wider text-text-muted mb-3">{title}</h2>
96
+ {children}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ /* ── Main view ── */
102
+
103
+ export function HomeView() {
104
+ const { data } = useQuery("overview.get");
105
+ const { data: workers } = useQuery("workers.list");
106
+
107
+ if (!data) {
108
+ return (
109
+ <div class="p-8 lg:p-10">
110
+ <h1 class="text-2xl font-bold text-ink mb-1">Overview</h1>
111
+ <p class="text-sm text-text-muted">Loading...</p>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ const rt = data.runtime;
117
+ const hasErrors = data.errors > 0;
118
+ const envEntries = Object.entries(rt.env).filter(([k]) => k !== "PATH");
119
+
120
+ return (
121
+ <div class="p-8 lg:p-10 max-w-[1600px]">
122
+ {/* ── Header ── */}
123
+ <div class="flex items-start gap-4 mb-8">
124
+ <div class="flex-1">
125
+ <h1 class="text-2xl font-bold text-ink">Overview</h1>
126
+ <p class="text-sm text-text-muted mt-1">
127
+ Bun {rt.bunVersion} &middot; {rt.platform}/{rt.arch} &middot; up {fmtUptime(rt.uptime)}
128
+ </p>
129
+ </div>
130
+ <a
131
+ href="#/errors"
132
+ class={`flex items-center gap-2.5 px-4 py-2 rounded-full text-sm font-medium no-underline transition-colors ${
133
+ hasErrors
134
+ ? "bg-red-500/10 text-red-400 hover:bg-red-500/20"
135
+ : "bg-emerald-500/10 text-emerald-400"
136
+ }`}
137
+ >
138
+ <span class={`w-2.5 h-2.5 rounded-full ${hasErrors ? "bg-red-500" : "bg-emerald-500"}`} />
139
+ {hasErrors ? `${data.errors} error${data.errors > 1 ? "s" : ""}` : "Healthy"}
140
+ </a>
141
+ </div>
142
+
143
+ {/* ── Two-column layout ── */}
144
+ <div class="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-10">
145
+
146
+ {/* ── Left: main content ── */}
147
+ <div class="flex flex-col gap-10">
148
+
149
+ {/* ── Health: Errors hero card ── */}
150
+ {hasErrors && (
151
+ <a
152
+ href="#/errors"
153
+ class="flex items-center gap-5 bg-panel rounded-xl border border-red-500/30 border-l-[3px] border-l-red-500 px-6 py-5 no-underline hover:border-red-500/50 transition-colors"
154
+ >
155
+ <span class="text-3xl">⚠︎</span>
156
+ <div class="flex-1">
157
+ <div class="text-3xl font-bold text-red-400 tabular-nums">{data.errors}</div>
158
+ <div class="text-sm text-text-muted mt-0.5">Unresolved errors</div>
159
+ </div>
160
+ <span class="text-sm text-text-muted">View all &rarr;</span>
161
+ </a>
162
+ )}
163
+
164
+ {/* ── Inventory ── */}
165
+ <Section title="Bindings">
166
+ <div class="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-3">
167
+ {INVENTORY.map(item => {
168
+ const count = data[item.key as keyof OverviewData] as number;
169
+ const active = count > 0;
170
+ return (
171
+ <a
172
+ key={item.key}
173
+ href={`#${item.path}`}
174
+ class={`bg-panel rounded-lg border px-4 py-3 no-underline transition-all hover:shadow-card-hover ${
175
+ active
176
+ ? "border-border hover:border-text-dim"
177
+ : "border-border-subtle opacity-40 hover:opacity-65"
178
+ }`}
179
+ >
180
+ <div class="flex items-center gap-2.5">
181
+ <span class={`text-lg leading-none ${active ? item.color : "text-text-dim"}`}>{item.icon}</span>
182
+ <span class={`text-xl font-bold tabular-nums leading-none ${active ? "text-ink" : "text-text-dim"}`}>{count}</span>
183
+ </div>
184
+ <div class="text-xs text-text-muted mt-1.5">{item.label}</div>
185
+ </a>
186
+ );
187
+ })}
188
+ </div>
189
+ </Section>
190
+
191
+ {/* ── Workers ── */}
192
+ <Section title="Workers">
193
+ {workers && workers.length > 0 ? (
194
+ <div class="flex flex-col gap-3">
195
+ {workers.map((w: WorkerInfo) => {
196
+ const errCount = data.workerErrors[w.name] ?? 0;
197
+ const hasWorkerErrors = errCount > 0;
198
+ return (
199
+ <a
200
+ key={w.name}
201
+ href="#/workers"
202
+ class={`group bg-panel rounded-xl border px-5 py-4 no-underline transition-colors ${
203
+ hasWorkerErrors
204
+ ? "border-red-500/20 hover:border-red-500/40"
205
+ : "border-border hover:border-text-dim"
206
+ }`}
207
+ >
208
+ <div class="flex items-center gap-2.5 mb-2">
209
+ <span class={`w-2.5 h-2.5 rounded-full shrink-0 ${hasWorkerErrors ? "bg-red-500" : "bg-emerald-500"}`} />
210
+ <span class="text-sm font-semibold text-ink">{w.name}</span>
211
+ {w.isMain && (
212
+ <span class="px-2 py-0.5 rounded text-[11px] font-medium bg-ink text-surface leading-none">main</span>
213
+ )}
214
+ {hasWorkerErrors && (
215
+ <span class="px-2 py-0.5 rounded text-[11px] font-medium bg-red-500/15 text-red-400 leading-none">
216
+ {errCount} err
217
+ </span>
218
+ )}
219
+ <span class="text-xs text-text-muted ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
220
+ View &rarr;
221
+ </span>
222
+ </div>
223
+ {w.bindings.length > 0 && (
224
+ <div class="flex flex-wrap gap-1.5 ml-5">
225
+ {w.bindings.map(b => (
226
+ <span key={b.name} title={b.name}>
227
+ <StatusBadge status={b.type} colorMap={BINDING_COLORS} />
228
+ </span>
229
+ ))}
230
+ </div>
231
+ )}
232
+ </a>
233
+ );
234
+ })}
235
+ </div>
236
+ ) : (
237
+ <div class="bg-panel rounded-xl border border-border-subtle px-5 py-8 text-center text-sm text-text-muted">
238
+ No workers configured
239
+ </div>
240
+ )}
241
+ </Section>
242
+ </div>
243
+
244
+ {/* ── Right: System sidebar ── */}
245
+ <div class="flex flex-col gap-5">
246
+ <div class="bg-panel rounded-xl border border-border p-5">
247
+ <div class="text-xs font-semibold uppercase tracking-wider text-text-muted mb-4">Resources</div>
248
+ <div class="flex flex-col gap-2">
249
+ <Kv k="RSS" v={fmtBytes(rt.memory.rss)} />
250
+ <Kv k="Heap" v={fmtBytes(rt.memory.heapUsed)} />
251
+ <Kv k="External" v={fmtBytes(rt.memory.external)} />
252
+ </div>
253
+ <div class="mt-3 pt-3 border-t border-border-subtle flex flex-col gap-2">
254
+ <Kv k="CPU user" v={fmtMicros(rt.cpuUsage.user)} />
255
+ <Kv k="CPU system" v={fmtMicros(rt.cpuUsage.system)} />
256
+ </div>
257
+ </div>
258
+
259
+ <div class="bg-panel rounded-xl border border-border p-5">
260
+ <div class="text-xs font-semibold uppercase tracking-wider text-text-muted mb-4">Runtime</div>
261
+ <div class="flex flex-col gap-2">
262
+ <Kv k="Bun" v={rt.bunVersion} />
263
+ <Kv k="Platform" v={`${rt.platform}/${rt.arch}`} />
264
+ <Kv k="PID" v={String(rt.pid)} />
265
+ <Kv k="Uptime" v={fmtUptime(rt.uptime)} />
266
+ <Kv k="Started" v={new Date(rt.startedAt).toLocaleTimeString()} />
267
+ <Kv k="CWD" v={rt.cwd} />
268
+ </div>
269
+ </div>
270
+
271
+ {envEntries.length > 0 && (
272
+ <details class="bg-panel rounded-xl border border-border overflow-hidden">
273
+ <summary class="p-5 cursor-pointer select-none text-xs font-semibold uppercase tracking-wider text-text-muted hover:text-text-secondary transition-colors">
274
+ Environment ({envEntries.length})
275
+ </summary>
276
+ <div class="px-5 pb-5 flex flex-col gap-2">
277
+ {envEntries.map(([key, value]) => (
278
+ <Kv key={key} k={key} v={value} />
279
+ ))}
280
+ </div>
281
+ </details>
282
+ )}
283
+ </div>
284
+ </div>
285
+ </div>
286
+ );
287
+ }
@@ -0,0 +1,273 @@
1
+ import { useState } from "preact/hooks";
2
+ import { formatBytes } from "../lib";
3
+ import { useQuery, usePaginatedQuery, useMutation } from "../rpc/hooks";
4
+ import type { KvValue } from "../rpc/types";
5
+ import { EmptyState, PageHeader, Breadcrumb, Table, DetailField, CodeBlock, FilterInput, LoadMoreButton, DeleteButton, TableLink, ServiceInfo, PillButton, RefreshButton } from "../components";
6
+
7
+ export function KvView({ route }: { route: string }) {
8
+ const parts = route.split("/").filter(Boolean);
9
+
10
+ if (parts.length === 1) return <KvNamespaceList />;
11
+ if (parts.length === 2) return <KvKeyList ns={decodeURIComponent(parts[1]!)} />;
12
+ if (parts.length >= 3) return <KvKeyDetail ns={decodeURIComponent(parts[1]!)} keyName={decodeURIComponent(parts.slice(2).join("/"))} />;
13
+ return null;
14
+ }
15
+
16
+ function KvNamespaceList() {
17
+ const { data: namespaces, refetch } = useQuery("kv.listNamespaces");
18
+ const { data: configGroups } = useQuery("config.forService", { type: "kv" });
19
+
20
+ const totalKeys = namespaces?.reduce((s, ns) => s + ns.count, 0) ?? 0;
21
+
22
+ return (
23
+ <div class="p-8 max-w-6xl">
24
+ <PageHeader title="KV Namespaces" subtitle={`${namespaces?.length ?? 0} namespace(s)`} actions={<RefreshButton onClick={refetch} />} />
25
+ <div class="flex gap-6 items-start">
26
+ <div class="flex-1 min-w-0">
27
+ {!namespaces?.length ? (
28
+ <EmptyState message="No KV namespaces found" />
29
+ ) : (
30
+ <Table
31
+ headers={["Namespace", "Keys"]}
32
+ rows={namespaces.map(ns => [
33
+ <TableLink href={`#/kv/${encodeURIComponent(ns.namespace)}`}>{ns.namespace}</TableLink>,
34
+ <span class="tabular-nums">{ns.count}</span>,
35
+ ])}
36
+ />
37
+ )}
38
+ </div>
39
+ <ServiceInfo
40
+ description="Key-value storage for fast, globally distributed reads."
41
+ stats={[
42
+ { label: "Namespaces", value: namespaces?.length ?? 0 },
43
+ { label: "Total keys", value: totalKeys.toLocaleString() },
44
+ ]}
45
+ configGroups={configGroups}
46
+ links={[
47
+ { label: "Documentation", href: "https://developers.cloudflare.com/kv/" },
48
+ { label: "API Reference", href: "https://developers.cloudflare.com/api/resources/kv/" },
49
+ ]}
50
+ />
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ // ─── Put Key Form (add / edit) ─────────────────────────────────────
57
+
58
+ interface KvPutFormProps {
59
+ ns: string;
60
+ /** Pre-fill for editing an existing key */
61
+ initial?: { key: string; value: string; metadata?: string; expiration?: number | null };
62
+ onSaved: () => void;
63
+ onCancel: () => void;
64
+ }
65
+
66
+ function KvPutForm({ ns, initial, onSaved, onCancel }: KvPutFormProps) {
67
+ const isEdit = !!initial;
68
+ const [key, setKey] = useState(initial?.key ?? "");
69
+ const [value, setValue] = useState(initial?.value ?? "");
70
+ const [metadata, setMetadata] = useState(initial?.metadata ?? "");
71
+ const [ttl, setTtl] = useState("");
72
+ const [error, setError] = useState("");
73
+ const putKey = useMutation("kv.putKey");
74
+
75
+ const handleSubmit = async () => {
76
+ setError("");
77
+ if (!key.trim()) { setError("Key is required"); return; }
78
+
79
+ const input: { ns: string; key: string; value: string; metadata?: string; expirationTtl?: number } = { ns, key, value };
80
+ if (metadata.trim()) input.metadata = metadata.trim();
81
+ if (ttl.trim()) {
82
+ const ttlNum = parseInt(ttl, 10);
83
+ if (isNaN(ttlNum) || ttlNum < 60) { setError("TTL must be at least 60 seconds"); return; }
84
+ input.expirationTtl = ttlNum;
85
+ }
86
+
87
+ const result = await putKey.mutate(input);
88
+ if (result) {
89
+ onSaved();
90
+ } else if (putKey.error) {
91
+ setError(putKey.error.message);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <div class="bg-panel border border-border rounded-lg p-4 mb-6">
97
+ <div class="flex items-center justify-between mb-3">
98
+ <div class="text-sm font-semibold text-ink">{isEdit ? "Edit key" : "Add key"}</div>
99
+ <button onClick={onCancel} class="text-text-muted hover:text-text-data text-xs font-medium">
100
+ Cancel
101
+ </button>
102
+ </div>
103
+
104
+ <div class="space-y-3">
105
+ <div>
106
+ <label class="block text-xs font-medium text-text-secondary mb-1">Key</label>
107
+ <input
108
+ type="text"
109
+ value={key}
110
+ onInput={e => setKey((e.target as HTMLInputElement).value)}
111
+ placeholder="my-key"
112
+ disabled={isEdit}
113
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all disabled:opacity-50"
114
+ />
115
+ </div>
116
+
117
+ <div>
118
+ <label class="block text-xs font-medium text-text-secondary mb-1">Value</label>
119
+ <textarea
120
+ value={value}
121
+ onInput={e => setValue((e.target as HTMLTextAreaElement).value)}
122
+ placeholder="Value..."
123
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all resize-y min-h-[80px]"
124
+ rows={4}
125
+ />
126
+ </div>
127
+
128
+ <div class="flex gap-3">
129
+ <div class="flex-1">
130
+ <label class="block text-xs font-medium text-text-secondary mb-1">Metadata (JSON, optional)</label>
131
+ <input
132
+ type="text"
133
+ value={metadata}
134
+ onInput={e => setMetadata((e.target as HTMLInputElement).value)}
135
+ placeholder='{"type": "config"}'
136
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
137
+ />
138
+ </div>
139
+ <div class="w-40">
140
+ <label class="block text-xs font-medium text-text-secondary mb-1">TTL (seconds)</label>
141
+ <input
142
+ type="text"
143
+ value={ttl}
144
+ onInput={e => setTtl((e.target as HTMLInputElement).value)}
145
+ placeholder="e.g. 3600"
146
+ class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
147
+ />
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ {error && <div class="text-red-500 text-xs mt-2">{error}</div>}
153
+ <div class="flex justify-end mt-3">
154
+ <button
155
+ onClick={handleSubmit}
156
+ disabled={putKey.isLoading || !key.trim()}
157
+ class="rounded-md px-4 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
158
+ >
159
+ {putKey.isLoading ? "Saving..." : isEdit ? "Save" : "Add key"}
160
+ </button>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ // ─── Key list ───────────────────────────────────────────────────────
167
+
168
+ function KvKeyList({ ns }: { ns: string }) {
169
+ const [prefix, setPrefix] = useState("");
170
+ const { items: keys, hasMore, loadMore, refetch } = usePaginatedQuery("kv.listKeys", { ns, prefix });
171
+ const deleteKey = useMutation("kv.deleteKey");
172
+ const [showAdd, setShowAdd] = useState(false);
173
+
174
+ const handleDelete = async (key: string) => {
175
+ if (!confirm(`Delete key "${key}"?`)) return;
176
+ await deleteKey.mutate({ ns, key });
177
+ refetch();
178
+ };
179
+
180
+ return (
181
+ <div class="p-8">
182
+ <Breadcrumb items={[{ label: "KV", href: "#/kv" }, { label: ns }]} />
183
+ <div class="mb-6 flex gap-3 items-center justify-between">
184
+ <FilterInput value={prefix} onInput={setPrefix} placeholder="Filter by prefix..." />
185
+ <div class="flex gap-2 items-center">
186
+ <RefreshButton onClick={refetch} />
187
+ {!showAdd && (
188
+ <button
189
+ onClick={() => setShowAdd(true)}
190
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
191
+ >
192
+ Add key
193
+ </button>
194
+ )}
195
+ </div>
196
+ </div>
197
+ {showAdd && (
198
+ <KvPutForm
199
+ ns={ns}
200
+ onSaved={() => { setShowAdd(false); refetch(); }}
201
+ onCancel={() => setShowAdd(false)}
202
+ />
203
+ )}
204
+ {keys.length === 0 ? (
205
+ <EmptyState message="No keys found" />
206
+ ) : (
207
+ <>
208
+ <Table
209
+ headers={["Key", "Size", "Expiration", ""]}
210
+ rows={keys.map(k => [
211
+ <TableLink href={`#/kv/${encodeURIComponent(ns)}/${encodeURIComponent(k.key)}`} mono>{k.key}</TableLink>,
212
+ formatBytes(k.size),
213
+ k.expiration ? new Date(k.expiration * 1000).toLocaleString() : "—",
214
+ <DeleteButton onClick={() => handleDelete(k.key)} />,
215
+ ])}
216
+ />
217
+ {hasMore && <LoadMoreButton onClick={loadMore} />}
218
+ </>
219
+ )}
220
+ </div>
221
+ );
222
+ }
223
+
224
+ // ─── Key detail ─────────────────────────────────────────────────────
225
+
226
+ function KvKeyDetail({ ns, keyName }: { ns: string; keyName: string }) {
227
+ const { data, refetch } = useQuery("kv.getKey", { ns, key: keyName });
228
+ const [editing, setEditing] = useState(false);
229
+
230
+ if (!data) return <div class="p-8 text-text-muted">Loading...</div>;
231
+
232
+ return (
233
+ <div class="p-8">
234
+ <Breadcrumb items={[{ label: "KV", href: "#/kv" }, { label: ns, href: `#/kv/${encodeURIComponent(ns)}` }, { label: keyName }]} />
235
+ {editing ? (
236
+ <KvPutForm
237
+ ns={ns}
238
+ initial={{
239
+ key: data.key,
240
+ value: data.value,
241
+ metadata: data.metadata ? JSON.stringify(data.metadata, null, 2) : "",
242
+ expiration: data.expiration,
243
+ }}
244
+ onSaved={() => { setEditing(false); refetch(); }}
245
+ onCancel={() => setEditing(false)}
246
+ />
247
+ ) : (
248
+ <div class="space-y-5">
249
+ <div class="flex justify-end">
250
+ <button
251
+ onClick={() => setEditing(true)}
252
+ class="rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-data hover:bg-panel-hover transition-all"
253
+ >
254
+ Edit
255
+ </button>
256
+ </div>
257
+ <DetailField label="Key" value={data.key} />
258
+ <DetailField label="Value">
259
+ <CodeBlock class="max-h-96">{data.value}</CodeBlock>
260
+ </DetailField>
261
+ {data.metadata && (
262
+ <DetailField label="Metadata">
263
+ <CodeBlock>{JSON.stringify(data.metadata, null, 2)}</CodeBlock>
264
+ </DetailField>
265
+ )}
266
+ {data.expiration && (
267
+ <DetailField label="Expiration" value={new Date(data.expiration * 1000).toLocaleString()} />
268
+ )}
269
+ </div>
270
+ )}
271
+ </div>
272
+ );
273
+ }