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,99 @@
1
+ import { useRef } from "preact/hooks";
2
+ import { Modal } from "../components/modal";
3
+
4
+ export function FilterRow({ columns, filters, onFilterChange, onClearAll, hasCheckboxCol }: {
5
+ columns: string[];
6
+ filters: Record<string, string>;
7
+ onFilterChange: (col: string, value: string) => void;
8
+ onClearAll: () => void;
9
+ hasCheckboxCol?: boolean;
10
+ }) {
11
+ const debounceRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
12
+ const hasAny = Object.values(filters).some(v => v.trim());
13
+
14
+ const handleInput = (col: string, value: string) => {
15
+ clearTimeout(debounceRef.current[col]);
16
+ debounceRef.current[col] = setTimeout(() => {
17
+ onFilterChange(col, value);
18
+ }, 400);
19
+ };
20
+
21
+ return (
22
+ <tr class="border-b border-border-subtle bg-panel-hover/50">
23
+ {hasCheckboxCol && <th class="w-10 px-3 py-1.5"></th>}
24
+ {columns.map(col => (
25
+ <th key={col} class="px-4 py-1.5">
26
+ <input
27
+ type="text"
28
+ placeholder="filter..."
29
+ defaultValue={filters[col] ?? ""}
30
+ onInput={e => handleInput(col, (e.target as HTMLInputElement).value)}
31
+ onKeyDown={e => {
32
+ if (e.key === "Enter") {
33
+ clearTimeout(debounceRef.current[col]);
34
+ onFilterChange(col, (e.target as HTMLInputElement).value);
35
+ }
36
+ }}
37
+ class="w-full bg-panel-secondary border border-border rounded px-2 py-1 font-mono text-xs font-normal outline-none focus:border-text-dim focus:ring-1 focus:ring-text-dim/30 placeholder:text-text-dim transition-all"
38
+ />
39
+ </th>
40
+ ))}
41
+ <th class="px-4 py-1.5">
42
+ {hasAny && (
43
+ <button
44
+ onClick={onClearAll}
45
+ class="text-xs text-text-muted hover:text-text-data transition-colors whitespace-nowrap"
46
+ title="Clear all filters"
47
+ >
48
+ clear
49
+ </button>
50
+ )}
51
+ </th>
52
+ </tr>
53
+ );
54
+ }
55
+
56
+ // ─── FilterHelpModal ─────────────────────────────────────────────────
57
+
58
+ const FILTER_HELP: { expr: string; desc: string; example: string }[] = [
59
+ { expr: "text", desc: "Contains (case-insensitive match)", example: "alice" },
60
+ { expr: "=value", desc: "Exact match", example: "=pending" },
61
+ { expr: "!value", desc: "Not equal", example: "!cancelled" },
62
+ { expr: ">value", desc: "Greater than", example: ">100" },
63
+ { expr: "<value", desc: "Less than", example: "<50" },
64
+ { expr: ">=value", desc: "Greater or equal", example: ">=10.5" },
65
+ { expr: "<=value", desc: "Less or equal", example: "<=99" },
66
+ { expr: "%pat%", desc: "LIKE pattern (% = any, _ = one char)", example: "%@example%" },
67
+ { expr: "NULL", desc: "Value is NULL", example: "NULL" },
68
+ { expr: "!NULL", desc: "Value is not NULL", example: "!NULL" },
69
+ ];
70
+
71
+ export function FilterHelpModal({ onClose }: { onClose: () => void }) {
72
+ return (
73
+ <Modal title="Filter Syntax" onClose={onClose} maxWidth="max-w-md">
74
+ <div class="px-5 py-3">
75
+ <table class="w-full text-sm">
76
+ <thead>
77
+ <tr class="text-xs text-text-muted uppercase tracking-wider">
78
+ <th class="text-left py-1.5 font-medium">Expression</th>
79
+ <th class="text-left py-1.5 font-medium">Description</th>
80
+ <th class="text-left py-1.5 font-medium">Example</th>
81
+ </tr>
82
+ </thead>
83
+ <tbody>
84
+ {FILTER_HELP.map(h => (
85
+ <tr key={h.expr} class="border-t border-border-row">
86
+ <td class="py-1.5 pr-3"><code class="text-xs font-mono bg-panel-secondary px-1.5 py-0.5 rounded text-ink">{h.expr}</code></td>
87
+ <td class="py-1.5 pr-3 text-xs text-text-secondary">{h.desc}</td>
88
+ <td class="py-1.5"><code class="text-xs font-mono text-text-muted">{h.example}</code></td>
89
+ </tr>
90
+ ))}
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+ <div class="px-5 py-3 border-t border-border-subtle text-xs text-text-muted">
95
+ Filters apply per column. Multiple column filters combine with AND.
96
+ </div>
97
+ </Modal>
98
+ );
99
+ }
@@ -0,0 +1,110 @@
1
+ import type { HistoryEntry, BrowserHistoryEntry } from "./types";
2
+
3
+ function formatTime(ts: number) {
4
+ const d = new Date(ts);
5
+ const now = new Date();
6
+ const isToday = d.toDateString() === now.toDateString();
7
+ const time = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
8
+ return isToday ? time : `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`;
9
+ }
10
+
11
+ // ─── HistoryPanel ────────────────────────────────────────────────────
12
+
13
+ export function HistoryPanel({ entries, onSelect, onClear }: {
14
+ entries: HistoryEntry[];
15
+ onSelect: (entry: HistoryEntry) => void;
16
+ onClear: () => void;
17
+ }) {
18
+ if (entries.length === 0) {
19
+ return (
20
+ <div class="bg-panel rounded-lg border border-border p-5 mb-6 text-center text-sm text-text-muted">
21
+ No history yet
22
+ </div>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div class="bg-panel rounded-lg border border-border mb-6 overflow-hidden">
28
+ <div class="flex items-center justify-between px-4 py-2.5 border-b border-border-subtle bg-panel-hover/50">
29
+ <span class="text-xs font-semibold text-text-muted uppercase tracking-wider">Query History</span>
30
+ <button onClick={onClear} class="text-xs text-text-muted hover:text-red-500 transition-colors">Clear all</button>
31
+ </div>
32
+ <div class="max-h-64 overflow-y-auto divide-y divide-border-row">
33
+ {entries.map((entry, i) => (
34
+ <button
35
+ key={i}
36
+ onClick={() => onSelect(entry)}
37
+ class="w-full text-left px-4 py-2.5 hover:bg-panel-hover transition-colors flex items-center gap-3 group"
38
+ >
39
+ <code class="flex-1 text-xs font-mono text-text-data truncate group-hover:text-ink transition-colors">{entry.sql}</code>
40
+ <span class="text-[10px] text-text-dim tabular-nums flex-shrink-0">{formatTime(entry.ts)}</span>
41
+ </button>
42
+ ))}
43
+ </div>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ // ─── BrowserHistoryPanel ─────────────────────────────────────────────
49
+
50
+ export function BrowserHistoryPanel({ entries, currentTable, onSelect, onClear }: {
51
+ entries: BrowserHistoryEntry[];
52
+ currentTable: string;
53
+ onSelect: (entry: BrowserHistoryEntry) => void;
54
+ onClear: () => void;
55
+ }) {
56
+ if (entries.length === 0) {
57
+ return (
58
+ <div class="bg-panel rounded-lg border border-border p-5 mb-4 text-center text-sm text-text-muted">
59
+ No history yet — filter or sort a table to save an entry
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const formatFilters = (filters: Record<string, string>) => {
65
+ const parts = Object.entries(filters).filter(([, v]) => v.trim());
66
+ if (parts.length === 0) return null;
67
+ return parts.map(([col, val]) => `${col}: ${val}`).join(", ");
68
+ };
69
+
70
+ return (
71
+ <div class="bg-panel rounded-lg border border-border mb-4 overflow-hidden">
72
+ <div class="flex items-center justify-between px-4 py-2.5 border-b border-border-subtle bg-panel-hover/50">
73
+ <span class="text-xs font-semibold text-text-muted uppercase tracking-wider">Browser History</span>
74
+ <button onClick={onClear} class="text-xs text-text-muted hover:text-red-500 transition-colors">Clear all</button>
75
+ </div>
76
+ <div class="max-h-64 overflow-y-auto divide-y divide-border-row">
77
+ {entries.map((entry, i) => {
78
+ const filterStr = formatFilters(entry.filters);
79
+ const isSameTable = entry.table === currentTable;
80
+ return (
81
+ <button
82
+ key={i}
83
+ onClick={() => onSelect(entry)}
84
+ class="w-full text-left px-4 py-2.5 hover:bg-panel-hover transition-colors group"
85
+ >
86
+ <div class="flex items-center gap-2 mb-1">
87
+ <span class={`font-mono text-xs font-semibold ${isSameTable ? "text-ink" : "text-accent-olive"}`}>{entry.table}</span>
88
+ <span class="text-[10px] text-text-dim tabular-nums">{formatTime(entry.ts)}</span>
89
+ </div>
90
+ <div class="flex flex-wrap gap-x-3 gap-y-0.5">
91
+ {filterStr && (
92
+ <span class="text-xs text-text-secondary">
93
+ <span class="text-text-muted">filter:</span>{" "}
94
+ <span class="font-mono">{filterStr}</span>
95
+ </span>
96
+ )}
97
+ {entry.sortCol && (
98
+ <span class="text-xs text-text-secondary">
99
+ <span class="text-text-muted">order:</span>{" "}
100
+ <span class="font-mono">{entry.sortCol} {entry.sortDir}</span>
101
+ </span>
102
+ )}
103
+ </div>
104
+ </button>
105
+ );
106
+ })}
107
+ </div>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,137 @@
1
+ import { useState, useCallback } from "preact/hooks";
2
+ import type { HistoryEntry, BrowserHistoryEntry, SortDir } from "./types";
3
+
4
+ // ─── Query history (localStorage, scoped per database) ───────────────
5
+
6
+ const HISTORY_PREFIX = "bunflare-sql-history";
7
+ const HISTORY_MAX = 100;
8
+
9
+ function historyKey(scope?: string): string {
10
+ return scope ? `${HISTORY_PREFIX}:${scope}` : HISTORY_PREFIX;
11
+ }
12
+
13
+ function loadHistory(scope?: string): HistoryEntry[] {
14
+ try {
15
+ const raw = localStorage.getItem(historyKey(scope));
16
+ return raw ? JSON.parse(raw) : [];
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ function saveToHistory(sql: string, scope?: string): HistoryEntry[] {
23
+ const trimmed = sql.trim();
24
+ if (!trimmed) return loadHistory(scope);
25
+ const entries = loadHistory(scope);
26
+ // Deduplicate: remove existing entry with same SQL
27
+ const filtered = entries.filter(e => e.sql !== trimmed);
28
+ const next = [{ sql: trimmed, ts: Date.now() }, ...filtered].slice(0, HISTORY_MAX);
29
+ localStorage.setItem(historyKey(scope), JSON.stringify(next));
30
+ return next;
31
+ }
32
+
33
+ function clearHistory(scope?: string): HistoryEntry[] {
34
+ localStorage.removeItem(historyKey(scope));
35
+ return [];
36
+ }
37
+
38
+ export function useHistory(scope?: string) {
39
+ const [entries, setEntries] = useState<HistoryEntry[]>(() => loadHistory(scope));
40
+ const add = useCallback((sql: string) => {
41
+ setEntries(saveToHistory(sql, scope));
42
+ }, [scope]);
43
+ const clear = useCallback(() => {
44
+ setEntries(clearHistory(scope));
45
+ }, [scope]);
46
+ return { entries, add, clear };
47
+ }
48
+
49
+ // ─── Browser history (structured, localStorage, scoped per database) ─
50
+
51
+ const BROWSER_HISTORY_PREFIX = "bunflare-browser-history";
52
+ const BROWSER_HISTORY_MAX = 50;
53
+
54
+ function browserHistoryKey(scope?: string): string {
55
+ return scope ? `${BROWSER_HISTORY_PREFIX}:${scope}` : BROWSER_HISTORY_PREFIX;
56
+ }
57
+
58
+ function loadBrowserHistory(scope?: string): BrowserHistoryEntry[] {
59
+ try {
60
+ const raw = localStorage.getItem(browserHistoryKey(scope));
61
+ return raw ? JSON.parse(raw) : [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ function saveToBrowserHistory(entry: Omit<BrowserHistoryEntry, "ts">, scope?: string): BrowserHistoryEntry[] {
68
+ const entries = loadBrowserHistory(scope);
69
+ // Deduplicate by same table + filters + sort
70
+ const key = JSON.stringify({ t: entry.table, f: entry.filters, s: entry.sortCol, d: entry.sortDir });
71
+ const filtered = entries.filter(e =>
72
+ JSON.stringify({ t: e.table, f: e.filters, s: e.sortCol, d: e.sortDir }) !== key
73
+ );
74
+ const next = [{ ...entry, ts: Date.now() }, ...filtered].slice(0, BROWSER_HISTORY_MAX);
75
+ localStorage.setItem(browserHistoryKey(scope), JSON.stringify(next));
76
+ return next;
77
+ }
78
+
79
+ function clearBrowserHistory(scope?: string): BrowserHistoryEntry[] {
80
+ localStorage.removeItem(browserHistoryKey(scope));
81
+ return [];
82
+ }
83
+
84
+ export function useBrowserHistory(scope?: string) {
85
+ const [entries, setEntries] = useState<BrowserHistoryEntry[]>(() => loadBrowserHistory(scope));
86
+ const add = useCallback((entry: Omit<BrowserHistoryEntry, "ts">) => {
87
+ setEntries(saveToBrowserHistory(entry, scope));
88
+ }, [scope]);
89
+ const clear = useCallback(() => {
90
+ setEntries(clearBrowserHistory(scope));
91
+ }, [scope]);
92
+ return { entries, add, clear };
93
+ }
94
+
95
+ // ─── Per-table view state (filters + sort, localStorage) ────────────
96
+
97
+ const TABLE_STATE_PREFIX = "bunflare-table-state";
98
+
99
+ interface TableViewState {
100
+ filters: Record<string, string>;
101
+ sortCol: string | null;
102
+ sortDir: SortDir;
103
+ }
104
+
105
+ function tableStateKey(scope?: string): string {
106
+ return scope ? `${TABLE_STATE_PREFIX}:${scope}` : TABLE_STATE_PREFIX;
107
+ }
108
+
109
+ export function loadTableState(table: string, scope?: string): TableViewState | null {
110
+ try {
111
+ const raw = localStorage.getItem(tableStateKey(scope));
112
+ if (!raw) return null;
113
+ const all = JSON.parse(raw);
114
+ return all[table] ?? null;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ export function saveTableState(table: string, state: TableViewState, scope?: string): void {
121
+ try {
122
+ const raw = localStorage.getItem(tableStateKey(scope));
123
+ const all = raw ? JSON.parse(raw) : {};
124
+ all[table] = state;
125
+ localStorage.setItem(tableStateKey(scope), JSON.stringify(all));
126
+ } catch {}
127
+ }
128
+
129
+ export function clearTableState(table: string, scope?: string): void {
130
+ try {
131
+ const raw = localStorage.getItem(tableStateKey(scope));
132
+ if (!raw) return;
133
+ const all = JSON.parse(raw);
134
+ delete all[table];
135
+ localStorage.setItem(tableStateKey(scope), JSON.stringify(all));
136
+ } catch {}
137
+ }
@@ -0,0 +1,4 @@
1
+ export { SqlBrowser } from "./sql-browser";
2
+ export type { SqlBrowserProps, Tab } from "./types";
3
+ export { parseCreateTable } from "./utils";
4
+ export { sqlLiteral } from "./utils";
@@ -0,0 +1,85 @@
1
+ import { useState } from "preact/hooks";
2
+ import type { TableSchema } from "./types";
3
+
4
+ export function InsertRowForm({ schema, displayCols, onSave, onCancel, hasCheckboxCol }: {
5
+ schema: TableSchema;
6
+ displayCols: string[];
7
+ onSave: (values: Record<string, unknown>) => void;
8
+ onCancel: () => void;
9
+ hasCheckboxCol?: boolean;
10
+ }) {
11
+ const [values, setValues] = useState<Record<string, string>>({});
12
+ const [nulls, setNulls] = useState<Record<string, boolean>>(() => {
13
+ const n: Record<string, boolean> = {};
14
+ for (const col of schema.columns) {
15
+ // Default autoincrement PKs to NULL
16
+ if (col.autoIncrement) n[col.name] = true;
17
+ }
18
+ return n;
19
+ });
20
+
21
+ const handleSave = () => {
22
+ const result: Record<string, unknown> = {};
23
+ for (const col of displayCols) {
24
+ const colInfo = schema.columns.find(c => c.name === col);
25
+ if (colInfo?.autoIncrement && nulls[col]) continue; // omit autoincrement columns set to NULL
26
+ if (nulls[col]) {
27
+ result[col] = null;
28
+ } else {
29
+ result[col] = values[col] ?? "";
30
+ }
31
+ }
32
+ onSave(result);
33
+ };
34
+
35
+ return (
36
+ <tr class="border-b border-emerald-100 bg-emerald-50/30">
37
+ {hasCheckboxCol && <td class="w-10 px-3 py-2"></td>}
38
+ {displayCols.map(col => {
39
+ const colInfo = schema.columns.find(c => c.name === col);
40
+ const isAutoInc = colInfo?.autoIncrement ?? false;
41
+ const isNullVal = nulls[col] ?? false;
42
+ return (
43
+ <td key={col} class="px-4 py-2">
44
+ <div class="flex items-center gap-1">
45
+ <input
46
+ type="text"
47
+ value={isNullVal ? "" : (values[col] ?? "")}
48
+ disabled={isNullVal}
49
+ placeholder={isAutoInc ? "auto" : colInfo?.type ?? ""}
50
+ onInput={e => setValues(v => ({ ...v, [col]: (e.target as HTMLInputElement).value }))}
51
+ onKeyDown={e => { if (e.key === "Enter") handleSave(); else if (e.key === "Escape") onCancel(); }}
52
+ class={`w-full bg-panel border border-border rounded px-2 py-1 font-mono text-xs outline-none focus:border-ink focus:ring-1 focus:ring-border ${isNullVal ? "opacity-40" : ""}`}
53
+ />
54
+ <button
55
+ onClick={() => setNulls(n => ({ ...n, [col]: !n[col] }))}
56
+ title={isNullVal ? "Set to value" : "Set to NULL"}
57
+ class={`flex-shrink-0 rounded px-1.5 py-1 text-xs font-bold transition-colors ${
58
+ isNullVal ? "bg-amber-100 text-amber-700" : "bg-panel-hover text-text-muted hover:bg-panel-active"
59
+ }`}
60
+ >
61
+ N
62
+ </button>
63
+ </div>
64
+ </td>
65
+ );
66
+ })}
67
+ <td class="px-4 py-2">
68
+ <div class="flex items-center gap-1">
69
+ <button
70
+ onClick={handleSave}
71
+ class="rounded px-2 py-1 text-xs font-medium bg-emerald-500 text-white hover:bg-emerald-600 transition-colors"
72
+ >
73
+ Save
74
+ </button>
75
+ <button
76
+ onClick={onCancel}
77
+ class="rounded px-2 py-1 text-xs font-medium text-text-muted hover:text-text-data transition-colors"
78
+ >
79
+ Cancel
80
+ </button>
81
+ </div>
82
+ </td>
83
+ </tr>
84
+ );
85
+ }
@@ -0,0 +1,116 @@
1
+ import { useState } from "preact/hooks";
2
+ import type { ForeignKeyInfo } from "./types";
3
+ import { Modal } from "../components/modal";
4
+
5
+ // ─── RowDetailModal ──────────────────────────────────────────────────
6
+
7
+ export function RowDetailModal({ columns, row, fkMap, onClose, onNavigateFK }: {
8
+ columns: string[];
9
+ row: Record<string, unknown>;
10
+ fkMap: Map<string, ForeignKeyInfo>;
11
+ onClose: () => void;
12
+ onNavigateFK?: (targetTable: string, targetColumn: string, value: unknown) => void;
13
+ }) {
14
+ const [copied, setCopied] = useState<string | null>(null);
15
+
16
+ const copyValue = (col: string, value: unknown) => {
17
+ navigator.clipboard.writeText(value === null ? "NULL" : String(value));
18
+ setCopied(col);
19
+ setTimeout(() => setCopied(null), 1500);
20
+ };
21
+
22
+ return (
23
+ <Modal title="Row Detail" onClose={onClose}>
24
+ <div class="overflow-y-auto flex-1 divide-y divide-border-row">
25
+ {columns.map(col => {
26
+ const value = row[col];
27
+ const fk = fkMap.get(col);
28
+ const strVal = value === null ? "" : String(value);
29
+ let formatted = strVal;
30
+ let isJson = false;
31
+ if (value !== null && strVal.length > 0) {
32
+ try { formatted = JSON.stringify(JSON.parse(strVal), null, 2); isJson = true; } catch {}
33
+ }
34
+ return (
35
+ <div key={col} class="px-5 py-3 flex gap-4 group">
36
+ <div class="w-1/3 flex-shrink-0 flex items-start gap-1.5 pt-0.5">
37
+ <span class="font-mono text-xs font-medium text-text-secondary">{col}</span>
38
+ {fk && <span class="text-[10px] font-semibold bg-link/10 text-link px-1 py-0.5 rounded">FK</span>}
39
+ {isJson && <span class="text-[10px] font-semibold bg-link/10 text-link px-1 py-0.5 rounded">JSON</span>}
40
+ </div>
41
+ <div class="flex-1 min-w-0 flex items-start gap-2">
42
+ {value === null ? (
43
+ <span class="text-text-dim italic text-xs">NULL</span>
44
+ ) : fk && onNavigateFK ? (
45
+ <button
46
+ onClick={() => { onNavigateFK(fk.targetTable, fk.targetColumn, value); onClose(); }}
47
+ class="font-mono text-xs text-link hover:opacity-80 hover:underline text-left"
48
+ >
49
+ {strVal} &rarr; {fk.targetTable}
50
+ </button>
51
+ ) : (
52
+ <pre class="font-mono text-xs whitespace-pre-wrap break-all flex-1 min-w-0">{formatted}</pre>
53
+ )}
54
+ <button
55
+ onClick={() => copyValue(col, value)}
56
+ class="opacity-0 group-hover:opacity-100 text-[10px] text-text-muted hover:text-text-data flex-shrink-0 px-1 py-0.5 rounded bg-panel-secondary hover:bg-panel-hover transition-all"
57
+ >
58
+ {copied === col ? "ok" : "copy"}
59
+ </button>
60
+ </div>
61
+ </div>
62
+ );
63
+ })}
64
+ </div>
65
+ </Modal>
66
+ );
67
+ }
68
+
69
+ // ─── CellInspectorModal ─────────────────────────────────────────────
70
+
71
+ export function CellInspectorModal({ column, value, onClose }: {
72
+ column: string;
73
+ value: unknown;
74
+ onClose: () => void;
75
+ }) {
76
+ const [copied, setCopied] = useState(false);
77
+ const strValue = value === null ? "NULL" : String(value);
78
+
79
+ let formatted = strValue;
80
+ let isJson = false;
81
+ if (value !== null) {
82
+ try {
83
+ formatted = JSON.stringify(JSON.parse(strValue), null, 2);
84
+ isJson = true;
85
+ } catch {}
86
+ }
87
+
88
+ const handleCopy = () => {
89
+ navigator.clipboard.writeText(strValue);
90
+ setCopied(true);
91
+ setTimeout(() => setCopied(false), 1500);
92
+ };
93
+
94
+ const titleContent = (
95
+ <span>
96
+ <span class="font-mono">{column}</span>
97
+ {isJson && <span class="ml-2 text-[10px] font-semibold bg-link/10 text-link px-1.5 py-0.5 rounded">JSON</span>}
98
+ </span>
99
+ );
100
+
101
+ return (
102
+ <Modal title={titleContent} onClose={onClose} maxWidth="max-w-xl">
103
+ <div class="overflow-auto flex-1 p-5">
104
+ <div class="flex justify-end mb-3">
105
+ <button
106
+ onClick={handleCopy}
107
+ class="text-xs font-medium px-2 py-1 rounded bg-panel-hover text-text-secondary hover:bg-panel-active transition-colors"
108
+ >
109
+ {copied ? "Copied!" : "Copy"}
110
+ </button>
111
+ </div>
112
+ <pre class={`text-xs font-mono whitespace-pre-wrap break-all ${value === null ? "text-text-dim italic" : ""}`}>{formatted}</pre>
113
+ </div>
114
+ </Modal>
115
+ );
116
+ }
@@ -0,0 +1,67 @@
1
+ import type { D1Table } from "../rpc/types";
2
+ import { parseCreateTable } from "./utils";
3
+
4
+ export function SchemaBrowserTab({ tables }: { tables?: D1Table[] | null }) {
5
+ if (!tables?.length) {
6
+ return <div class="text-center py-16 text-text-muted text-sm font-medium">No tables found</div>;
7
+ }
8
+
9
+ return (
10
+ <div class="space-y-4">
11
+ {tables.map(t => {
12
+ const schema = parseCreateTable(t.sql);
13
+ return (
14
+ <div key={t.name} class="bg-panel rounded-lg border border-border">
15
+ <div class="flex items-center justify-between px-4 py-3 border-b border-border-subtle">
16
+ <div class="flex items-center gap-3">
17
+ <span class="font-mono text-sm font-bold text-ink">{t.name}</span>
18
+ <span class="text-xs text-text-muted tabular-nums">{t.rows} row(s)</span>
19
+ </div>
20
+ {schema.primaryKeys.length > 0 && (
21
+ <span class="text-xs text-text-muted">
22
+ PK: <span class="font-mono text-text-secondary">{schema.primaryKeys.join(", ")}</span>
23
+ </span>
24
+ )}
25
+ </div>
26
+ <table class="w-full text-sm">
27
+ <thead>
28
+ <tr class="border-b border-border-row text-xs text-text-muted uppercase tracking-wider">
29
+ <th class="text-left px-4 py-2 font-medium">Column</th>
30
+ <th class="text-left px-4 py-2 font-medium">Type</th>
31
+ <th class="text-left px-4 py-2 font-medium">Nullable</th>
32
+ <th class="text-left px-4 py-2 font-medium">Default</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ {schema.columns.map(col => (
37
+ <tr key={col.name} class="border-b border-border-row last:border-0">
38
+ <td class="px-4 py-2 font-mono text-xs font-medium text-ink">
39
+ {col.name}
40
+ {schema.primaryKeys.includes(col.name) && (
41
+ <span class="ml-1.5 text-[10px] font-semibold bg-panel-hover text-text-secondary px-1 py-0.5 rounded">PK</span>
42
+ )}
43
+ {col.autoIncrement && (
44
+ <span class="ml-1.5 text-[10px] font-semibold bg-amber-50 text-amber-600 px-1 py-0.5 rounded">AI</span>
45
+ )}
46
+ {col.foreignKey && (
47
+ <span class="ml-1.5 text-[10px] font-semibold bg-blue-50 text-blue-600 px-1 py-0.5 rounded" title={`${col.foreignKey.targetTable}(${col.foreignKey.targetColumn})`}>
48
+ FK &rarr; {col.foreignKey.targetTable}
49
+ </span>
50
+ )}
51
+ </td>
52
+ <td class="px-4 py-2 font-mono text-xs text-text-secondary">{col.type || "—"}</td>
53
+ <td class="px-4 py-2 text-xs text-text-muted">{col.notNull ? "NOT NULL" : "NULL"}</td>
54
+ <td class="px-4 py-2 font-mono text-xs text-text-muted">{col.defaultValue ?? "—"}</td>
55
+ </tr>
56
+ ))}
57
+ </tbody>
58
+ </table>
59
+ <div class="px-4 py-2.5 border-t border-border-subtle bg-panel-hover/50">
60
+ <pre class="text-xs text-text-muted font-mono whitespace-pre-wrap">{t.sql}</pre>
61
+ </div>
62
+ </div>
63
+ );
64
+ })}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,52 @@
1
+ import { useState } from "preact/hooks";
2
+ import { navigate } from "../lib";
3
+ import type { SqlBrowserProps, Tab } from "./types";
4
+ import { TABS } from "./types";
5
+ import { useHistory, useBrowserHistory } from "./hooks";
6
+ import { DataBrowserTab } from "./data-browser-tab";
7
+ import { SchemaBrowserTab } from "./schema-browser-tab";
8
+ import { SqlConsoleTab } from "./sql-console-tab";
9
+
10
+ export function SqlBrowser({ tables, execQuery, historyScope, basePath, routeTab, routeTable, routeQuery }: SqlBrowserProps) {
11
+ const [localTab, setLocalTab] = useState<Tab>("data");
12
+ const tab = basePath ? (routeTab ?? "data") : localTab;
13
+ const [consoleSql, setConsoleSql] = useState("");
14
+ const history = useHistory(historyScope);
15
+ const browserHistory = useBrowserHistory(historyScope);
16
+
17
+ const switchTab = (t: Tab) => {
18
+ if (basePath) navigate(basePath + "/" + t);
19
+ else setLocalTab(t);
20
+ };
21
+
22
+ const openInConsole = (sql: string) => {
23
+ setConsoleSql(sql);
24
+ if (basePath) navigate(basePath + "/sql");
25
+ else setLocalTab("sql");
26
+ };
27
+
28
+ return (
29
+ <div>
30
+ {/* Tab bar */}
31
+ <div class="flex gap-1 mb-5 border-b border-border">
32
+ {TABS.map(t => (
33
+ <button
34
+ key={t.key}
35
+ onClick={() => switchTab(t.key)}
36
+ class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px ${
37
+ tab === t.key
38
+ ? "border-ink text-ink"
39
+ : "border-transparent text-text-muted hover:text-text-data"
40
+ }`}
41
+ >
42
+ {t.label}
43
+ </button>
44
+ ))}
45
+ </div>
46
+
47
+ {tab === "data" && <DataBrowserTab tables={tables} execQuery={execQuery} onOpenInConsole={openInConsole} history={history} browserHistory={browserHistory} historyScope={historyScope} basePath={basePath} routeTable={routeTable} routeQuery={routeQuery} />}
48
+ {tab === "schema" && <SchemaBrowserTab tables={tables} />}
49
+ {tab === "sql" && <SqlConsoleTab execQuery={execQuery} initialSql={consoleSql} history={history} />}
50
+ </div>
51
+ );
52
+ }