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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- 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,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} → {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 → {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
|
+
}
|