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,396 @@
1
+ // ─── Shared data types ───────────────────────────────────────────────
2
+
3
+ export type { GenerationInfo } from "../../generation";
4
+ import type { GenerationInfo } from "../../generation";
5
+
6
+ export interface Paginated<T> {
7
+ items: T[];
8
+ cursor: string | null;
9
+ }
10
+
11
+ export interface OkResponse {
12
+ ok: true;
13
+ }
14
+
15
+ // Overview
16
+ export interface OverviewData {
17
+ kv: number;
18
+ r2: number;
19
+ queue: number;
20
+ do: number;
21
+ workflows: number;
22
+ d1: number;
23
+ containers: number;
24
+ cache: number;
25
+ errors: number;
26
+ scheduled: number;
27
+ email: number;
28
+ ai: number;
29
+ analyticsEngine: number;
30
+ generations: GenerationInfo[];
31
+ runtime: {
32
+ bunVersion: string;
33
+ platform: string;
34
+ arch: string;
35
+ pid: number;
36
+ cwd: string;
37
+ uptime: number;
38
+ startedAt: number;
39
+ memory: {
40
+ rss: number;
41
+ heapUsed: number;
42
+ heapTotal: number;
43
+ external: number;
44
+ };
45
+ cpuUsage: {
46
+ user: number;
47
+ system: number;
48
+ };
49
+ env: Record<string, string>;
50
+ };
51
+ workerErrors: Record<string, number>;
52
+ }
53
+
54
+ // Containers
55
+ export interface ContainerSummary {
56
+ className: string;
57
+ image: string;
58
+ maxInstances: number | null;
59
+ bindingName: string;
60
+ instanceCount: number;
61
+ runningCount: number;
62
+ }
63
+
64
+ export interface ContainerInstance {
65
+ id: string;
66
+ doName: string | null;
67
+ containerName: string;
68
+ state: string;
69
+ ports: Record<string, string>;
70
+ }
71
+
72
+ export interface ContainerDetail {
73
+ id: string;
74
+ doName: string | null;
75
+ containerName: string;
76
+ image: string;
77
+ state: string;
78
+ exitCode: number | null;
79
+ ports: Record<string, string>;
80
+ created: string | null;
81
+ config: {
82
+ defaultPort: number;
83
+ sleepAfter: string | number | null;
84
+ enableInternet: boolean;
85
+ pingEndpoint: string;
86
+ };
87
+ }
88
+
89
+ // KV
90
+ export interface KvNamespace {
91
+ namespace: string;
92
+ count: number;
93
+ }
94
+
95
+ export interface KvKey {
96
+ key: string;
97
+ size: number;
98
+ metadata: string | null;
99
+ expiration: number | null;
100
+ }
101
+
102
+ export interface KvValue {
103
+ key: string;
104
+ value: string;
105
+ metadata: unknown;
106
+ expiration: number | null;
107
+ }
108
+
109
+ // R2
110
+ export interface R2Bucket {
111
+ bucket: string;
112
+ count: number;
113
+ total_size: number;
114
+ }
115
+
116
+ export interface R2Object {
117
+ key: string;
118
+ size: number;
119
+ etag: string;
120
+ uploaded: string;
121
+ http_metadata: string | null;
122
+ custom_metadata: string | null;
123
+ }
124
+
125
+ // Queue
126
+ export interface QueueInfo {
127
+ queue: string;
128
+ pending: number;
129
+ acked: number;
130
+ failed: number;
131
+ }
132
+
133
+ export interface QueueMessage {
134
+ id: string;
135
+ body: string;
136
+ content_type: string;
137
+ status: string;
138
+ attempts: number;
139
+ visible_at: number;
140
+ created_at: number;
141
+ completed_at: number | null;
142
+ }
143
+
144
+ // Durable Objects
145
+ export interface DoNamespace {
146
+ namespace: string;
147
+ count: number;
148
+ }
149
+
150
+ export interface DoInstance {
151
+ id: string;
152
+ name?: string | null;
153
+ key_count: number;
154
+ alarm: number | null;
155
+ }
156
+
157
+ export interface DoDetail {
158
+ entries: { key: string; value: string }[];
159
+ alarm: number | null;
160
+ hasAlarmHandler: boolean;
161
+ }
162
+
163
+ // Workflows
164
+ export interface WorkflowSummary {
165
+ name: string;
166
+ total: number;
167
+ byStatus: Record<string, number>;
168
+ }
169
+
170
+ export interface WorkflowInstance {
171
+ id: string;
172
+ status: string;
173
+ params: string | null;
174
+ output: string | null;
175
+ error: string | null;
176
+ created_at: number;
177
+ updated_at: number;
178
+ }
179
+
180
+ export interface WorkflowStepAttempt {
181
+ step_name: string;
182
+ failed_attempts: number;
183
+ last_error: string | null;
184
+ last_error_name: string | null;
185
+ last_error_id: string | null;
186
+ updated_at: number | null;
187
+ }
188
+
189
+ export interface WorkflowDetail extends WorkflowInstance {
190
+ steps: { step_name: string; output: string | null; completed_at: number }[];
191
+ stepAttempts: WorkflowStepAttempt[];
192
+ events: { id: number; event_type: string; payload: string | null; created_at: number }[];
193
+ activeSleep: { stepName: string; until: number } | null;
194
+ waitingForEvents: string[];
195
+ }
196
+
197
+ // D1
198
+ export interface D1Database {
199
+ name: string;
200
+ tables: number;
201
+ }
202
+
203
+ export interface D1Table {
204
+ name: string;
205
+ sql: string;
206
+ rows: number;
207
+ }
208
+
209
+ export interface QueryResult {
210
+ columns: string[];
211
+ rows: Record<string, unknown>[];
212
+ count: number;
213
+ message?: string;
214
+ error?: string;
215
+ }
216
+
217
+ // Cache
218
+ export interface CacheName {
219
+ cache_name: string;
220
+ count: number;
221
+ }
222
+
223
+ export interface CacheEntry {
224
+ url: string;
225
+ status: number;
226
+ headers: string;
227
+ expires_at: number | null;
228
+ }
229
+
230
+ // Generations
231
+ export interface WorkerGenerations {
232
+ workerName: string;
233
+ generations: GenerationInfo[];
234
+ gracePeriodMs: number;
235
+ }
236
+
237
+ export interface GenerationsData {
238
+ generations: GenerationInfo[];
239
+ gracePeriodMs: number;
240
+ workers?: WorkerGenerations[];
241
+ }
242
+
243
+ // Workers
244
+ export interface WorkerBinding {
245
+ type: string;
246
+ name: string;
247
+ target: string;
248
+ href: string | null;
249
+ }
250
+
251
+ export interface WorkerInfo {
252
+ name: string;
253
+ isMain: boolean;
254
+ bindings: WorkerBinding[];
255
+ }
256
+
257
+ // Errors
258
+ export interface ErrorSummary {
259
+ id: string;
260
+ timestamp: number;
261
+ errorName: string;
262
+ errorMessage: string;
263
+ requestMethod: string | null;
264
+ requestUrl: string | null;
265
+ workerName: string | null;
266
+ traceId: string | null;
267
+ spanId: string | null;
268
+ source: string | null;
269
+ }
270
+
271
+ export interface ErrorDetail {
272
+ id: string;
273
+ timestamp: number;
274
+ traceId: string | null;
275
+ spanId: string | null;
276
+ source: string | null;
277
+ data: {
278
+ error: {
279
+ name: string;
280
+ message: string;
281
+ stack: string;
282
+ frames: Array<{
283
+ file: string;
284
+ line: number;
285
+ column: number;
286
+ function: string;
287
+ source?: string[];
288
+ sourceLine?: number;
289
+ }>;
290
+ };
291
+ request: {
292
+ method: string;
293
+ url: string;
294
+ headers: Record<string, string>;
295
+ };
296
+ env: Record<string, string>;
297
+ bindings: Array<{ name: string; type: string }>;
298
+ runtime: {
299
+ bunVersion: string;
300
+ platform: string;
301
+ arch: string;
302
+ workerName?: string;
303
+ configName?: string;
304
+ };
305
+ };
306
+ }
307
+
308
+ export interface TraceErrorSummary {
309
+ id: string;
310
+ timestamp: number;
311
+ errorName: string;
312
+ errorMessage: string;
313
+ source: string | null;
314
+ }
315
+
316
+ // AI
317
+ export interface AiRequest {
318
+ id: string;
319
+ model: string;
320
+ input_summary: string | null;
321
+ output_summary: string | null;
322
+ duration_ms: number;
323
+ status: string;
324
+ error: string | null;
325
+ is_streaming: number;
326
+ created_at: number;
327
+ }
328
+
329
+ // Analytics Engine
330
+ export interface AnalyticsEngineDataPoint {
331
+ id: string;
332
+ dataset: string;
333
+ timestamp: number;
334
+ _sample_interval: number;
335
+ index1: string | null;
336
+ blob1: string | null;
337
+ blob2: string | null;
338
+ blob3: string | null;
339
+ blob4: string | null;
340
+ blob5: string | null;
341
+ double1: number | null;
342
+ double2: number | null;
343
+ double3: number | null;
344
+ double4: number | null;
345
+ double5: number | null;
346
+ }
347
+
348
+ // Email
349
+ export interface EmailRecord {
350
+ id: string;
351
+ binding: string;
352
+ from_addr: string;
353
+ to_addr: string;
354
+ raw_size: number;
355
+ status: string;
356
+ reject_reason: string | null;
357
+ created_at: number;
358
+ }
359
+
360
+ // Traces (re-export from tracing module)
361
+ export type { TraceSummary, TraceDetail, SpanData, SpanEventData, TraceEvent } from "../../tracing/types";
362
+
363
+ // ─── Handler context ─────────────────────────────────────────────────
364
+
365
+ import type { WranglerConfig } from "../../config";
366
+ import type { GenerationManager } from "../../generation-manager";
367
+ import type { WorkerRegistry } from "../../worker-registry";
368
+
369
+ export interface HandlerContext {
370
+ config: WranglerConfig | null;
371
+ manager: GenerationManager | null;
372
+ registry: WorkerRegistry | null;
373
+ }
374
+
375
+ /** Collect configs from all workers (registry) or fall back to single config. */
376
+ export function getAllConfigs(ctx: HandlerContext): WranglerConfig[] {
377
+ if (ctx.registry) {
378
+ return Array.from(ctx.registry.listManagers().values()).map(m => m.config);
379
+ }
380
+ return ctx.config ? [ctx.config] : [];
381
+ }
382
+
383
+ /** Find a DO namespace by class name across all active generations. */
384
+ export function getDoNamespace(ctx: HandlerContext, ns: string) {
385
+ if (ctx.registry) {
386
+ for (const manager of ctx.registry.listManagers().values()) {
387
+ const entry = manager.active?.registry.durableObjects.find(d => d.className === ns);
388
+ if (entry) return entry.namespace;
389
+ }
390
+ }
391
+ if (ctx.manager) {
392
+ const entry = ctx.manager.active?.registry.durableObjects.find(d => d.className === ns);
393
+ if (entry) return entry.namespace;
394
+ }
395
+ return null;
396
+ }
@@ -0,0 +1,122 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import type { D1Table, QueryResult } from "../rpc/types";
3
+ import { navigate, replaceRoute } from "../lib";
4
+ import type { SortDir, BrowserHistoryEntry } from "./types";
5
+ import type { useHistory, useBrowserHistory } from "./hooks";
6
+ import { TableDataView } from "./table-data-view";
7
+ import { TableSidebar } from "./table-sidebar";
8
+
9
+ export function DataBrowserTab({ tables, execQuery, onOpenInConsole, history, browserHistory, historyScope, basePath, routeTable, routeQuery }: {
10
+ tables?: D1Table[] | null;
11
+ execQuery: (sql: string) => Promise<QueryResult>;
12
+ onOpenInConsole: (sql: string) => void;
13
+ history: ReturnType<typeof useHistory>;
14
+ browserHistory: ReturnType<typeof useBrowserHistory>;
15
+ historyScope?: string;
16
+ basePath?: string;
17
+ routeTable?: string | null;
18
+ routeQuery?: URLSearchParams;
19
+ }) {
20
+ // Table selection: URL-driven when basePath is set, local state otherwise
21
+ const [localTable, setLocalTable] = useState<string | null>(null);
22
+ const [localRestoredState, setLocalRestoredState] = useState<{ filters: Record<string, string>; sortCol: string | null; sortDir: SortDir } | null>(null);
23
+ const selectedTable = basePath ? (routeTable ?? null) : localTable;
24
+
25
+ // Auto-select first table
26
+ useEffect(() => {
27
+ if (!selectedTable && tables?.length) {
28
+ if (basePath) {
29
+ replaceRoute(basePath + "/data/" + encodeURIComponent(tables[0]!.name));
30
+ } else {
31
+ setLocalTable(tables[0]!.name);
32
+ }
33
+ }
34
+ }, [tables, selectedTable, basePath]);
35
+
36
+ const tableInfo = tables?.find(t => t.name === selectedTable) ?? null;
37
+
38
+ const handleRestoreHistory = (entry: BrowserHistoryEntry) => {
39
+ if (basePath) {
40
+ const params = new URLSearchParams();
41
+ for (const [col, val] of Object.entries(entry.filters)) {
42
+ if (val.trim()) params.set("f." + col, val);
43
+ }
44
+ if (entry.sortCol) {
45
+ params.set("s", entry.sortCol);
46
+ params.set("d", entry.sortDir);
47
+ }
48
+ const qs = params.toString();
49
+ navigate(basePath + "/data/" + encodeURIComponent(entry.table) + (qs ? "?" + qs : ""));
50
+ } else {
51
+ setLocalTable(entry.table);
52
+ setLocalRestoredState({ filters: entry.filters, sortCol: entry.sortCol, sortDir: entry.sortDir });
53
+ }
54
+ };
55
+
56
+ const handleNavigateFK = (targetTable: string, targetColumn: string, value: unknown) => {
57
+ if (!tables?.some(t => t.name === targetTable)) return;
58
+ if (basePath) {
59
+ const params = new URLSearchParams();
60
+ params.set("f." + targetColumn, `=${String(value)}`);
61
+ navigate(basePath + "/data/" + encodeURIComponent(targetTable) + "?" + params.toString());
62
+ } else {
63
+ setLocalTable(targetTable);
64
+ setLocalRestoredState({ filters: { [targetColumn]: `=${String(value)}` }, sortCol: null, sortDir: "ASC" });
65
+ }
66
+ };
67
+
68
+ const handleTableSelect = (name: string) => {
69
+ if (basePath) {
70
+ navigate(basePath + "/data/" + encodeURIComponent(name));
71
+ } else {
72
+ setLocalTable(name);
73
+ setLocalRestoredState(null);
74
+ }
75
+ };
76
+
77
+ // Build effective query: from URL route or from local restored state
78
+ const effectiveQuery = (() => {
79
+ if (basePath) return routeQuery;
80
+ if (!localRestoredState) return undefined;
81
+ const params = new URLSearchParams();
82
+ for (const [col, val] of Object.entries(localRestoredState.filters)) {
83
+ if (val.trim()) params.set("f." + col, val);
84
+ }
85
+ if (localRestoredState.sortCol) {
86
+ params.set("s", localRestoredState.sortCol);
87
+ params.set("d", localRestoredState.sortDir);
88
+ }
89
+ return params;
90
+ })();
91
+
92
+ return (
93
+ <div class="flex gap-5">
94
+ <TableSidebar
95
+ tables={tables}
96
+ selected={selectedTable}
97
+ onSelect={handleTableSelect}
98
+ />
99
+ <div class="flex-1 min-w-0">
100
+ {tableInfo ? (
101
+ <TableDataView
102
+ key={tableInfo.name + "?" + (effectiveQuery?.toString() ?? "")}
103
+ table={tableInfo}
104
+ execQuery={execQuery}
105
+ onOpenInConsole={onOpenInConsole}
106
+ history={history}
107
+ browserHistory={browserHistory}
108
+ onRestoreHistory={handleRestoreHistory}
109
+ onNavigateFK={handleNavigateFK}
110
+ historyScope={historyScope}
111
+ basePath={basePath}
112
+ routeQuery={effectiveQuery}
113
+ />
114
+ ) : (
115
+ <div class="text-center py-16 text-text-muted text-sm font-medium">
116
+ {tables?.length ? "Select a table" : "No tables found"}
117
+ </div>
118
+ )}
119
+ </div>
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,117 @@
1
+ import { useState, useEffect, useRef } from "preact/hooks";
2
+ import type { ForeignKeyInfo } from "./types";
3
+
4
+ export function EditableCell({ value, onSave, foreignKey, onNavigateFK, onInspect, alignRight }: {
5
+ value: unknown;
6
+ onSave: (v: unknown) => void;
7
+ foreignKey?: ForeignKeyInfo | null;
8
+ onNavigateFK?: (fk: ForeignKeyInfo) => void;
9
+ onInspect?: () => void;
10
+ alignRight?: boolean;
11
+ }) {
12
+ const [editing, setEditing] = useState(false);
13
+ const [editValue, setEditValue] = useState("");
14
+ const [isNull, setIsNull] = useState(false);
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+
17
+ const isLong = value !== null && value !== undefined && String(value).length > 80;
18
+
19
+ const startEdit = () => {
20
+ setIsNull(value === null);
21
+ setEditValue(value === null ? "" : String(value));
22
+ setEditing(true);
23
+ };
24
+
25
+ useEffect(() => {
26
+ if (editing && inputRef.current) {
27
+ inputRef.current.focus();
28
+ inputRef.current.select();
29
+ }
30
+ }, [editing]);
31
+
32
+ const save = () => {
33
+ const newValue = isNull ? null : editValue;
34
+ setEditing(false);
35
+ // Only save if value actually changed
36
+ if (newValue !== (value === null ? null : String(value))) {
37
+ onSave(newValue);
38
+ }
39
+ };
40
+
41
+ const cancel = () => {
42
+ setEditing(false);
43
+ };
44
+
45
+ if (editing) {
46
+ return (
47
+ <div class="flex items-center gap-1 py-1">
48
+ <input
49
+ ref={inputRef}
50
+ type="text"
51
+ value={isNull ? "" : editValue}
52
+ disabled={isNull}
53
+ onInput={e => setEditValue((e.target as HTMLInputElement).value)}
54
+ onKeyDown={e => {
55
+ if (e.key === "Enter") save();
56
+ else if (e.key === "Escape") cancel();
57
+ }}
58
+ class={`w-full bg-panel-secondary border border-border rounded px-2 py-1 font-mono text-xs outline-none focus:border-ink focus:ring-1 focus:ring-border ${isNull ? "opacity-40" : ""}`}
59
+ />
60
+ <button
61
+ onClick={() => { setIsNull(!isNull); if (!isNull) setEditValue(""); }}
62
+ title={isNull ? "Set to value" : "Set to NULL"}
63
+ class={`flex-shrink-0 rounded px-1.5 py-1 text-xs font-bold transition-colors ${
64
+ isNull ? "bg-amber-100 text-amber-700" : "bg-panel-hover text-text-muted hover:bg-panel-active"
65
+ }`}
66
+ >
67
+ N
68
+ </button>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ const isFkNav = foreignKey && value != null && onNavigateFK;
74
+
75
+ return (
76
+ <div class={`group/cell flex items-center gap-1 font-mono text-xs py-2 min-h-[2rem] ${alignRight ? "justify-end" : ""}`}>
77
+ {isFkNav ? (
78
+ <>
79
+ <span
80
+ onClick={() => onNavigateFK!(foreignKey!)}
81
+ class="truncate max-w-xs text-link hover:underline cursor-pointer"
82
+ title={`${String(value)} \u2192 ${foreignKey!.targetTable}.${foreignKey!.targetColumn}`}
83
+ >
84
+ {String(value)}
85
+ </span>
86
+ <button
87
+ onClick={(e) => { e.stopPropagation(); startEdit(); }}
88
+ class="flex-shrink-0 opacity-0 group-hover/cell:opacity-100 text-xs text-text-muted hover:text-text-data px-1.5 py-0.5 rounded hover:bg-panel-hover transition-all"
89
+ title="Edit value"
90
+ >
91
+ edit
92
+ </button>
93
+ </>
94
+ ) : (
95
+ <div
96
+ onClick={startEdit}
97
+ class={`cursor-pointer flex-1 min-w-0 flex items-center ${alignRight ? "justify-end" : ""}`}
98
+ >
99
+ {value === null ? (
100
+ <span class="text-text-dim italic">NULL</span>
101
+ ) : (
102
+ <span class="truncate max-w-xs" title={String(value)}>{String(value)}</span>
103
+ )}
104
+ </div>
105
+ )}
106
+ {isLong && onInspect && (
107
+ <button
108
+ onClick={e => { e.stopPropagation(); onInspect(); }}
109
+ class="flex-shrink-0 text-[10px] text-text-muted hover:text-text-data px-1 py-0.5 rounded hover:bg-panel-hover transition-colors"
110
+ title="Inspect value"
111
+ >
112
+ &#x2922;
113
+ </button>
114
+ )}
115
+ </div>
116
+ );
117
+ }