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,38 @@
|
|
|
1
|
+
import type { D1Table } from "../rpc/types";
|
|
2
|
+
|
|
3
|
+
export function TableSidebar({ tables, selected, onSelect }: {
|
|
4
|
+
tables?: D1Table[] | null;
|
|
5
|
+
selected: string | null;
|
|
6
|
+
onSelect: (name: string) => void;
|
|
7
|
+
}) {
|
|
8
|
+
if (!tables?.length) {
|
|
9
|
+
return (
|
|
10
|
+
<div class="w-52 flex-shrink-0">
|
|
11
|
+
<div class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2 px-2">Tables</div>
|
|
12
|
+
<div class="text-xs text-text-muted px-2">No tables</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class="w-52 flex-shrink-0">
|
|
19
|
+
<div class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2 px-2">Tables</div>
|
|
20
|
+
<div class="space-y-0.5">
|
|
21
|
+
{tables.map(t => (
|
|
22
|
+
<button
|
|
23
|
+
key={t.name}
|
|
24
|
+
onClick={() => onSelect(t.name)}
|
|
25
|
+
class={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors flex items-center justify-between ${
|
|
26
|
+
selected === t.name
|
|
27
|
+
? "bg-ink text-surface"
|
|
28
|
+
: "text-text-data hover:bg-panel-hover"
|
|
29
|
+
}`}
|
|
30
|
+
>
|
|
31
|
+
<span class="font-mono text-xs truncate">{t.name}</span>
|
|
32
|
+
<span class={`text-xs tabular-nums ${selected === t.name ? "text-text-dim" : "text-text-muted"}`}>{t.rows}</span>
|
|
33
|
+
</button>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { D1Table, QueryResult } from "../rpc/types";
|
|
2
|
+
|
|
3
|
+
// ─── Schema types ────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface ForeignKeyInfo {
|
|
6
|
+
targetTable: string;
|
|
7
|
+
targetColumn: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ColumnInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
notNull: boolean;
|
|
14
|
+
defaultValue: string | null;
|
|
15
|
+
autoIncrement: boolean;
|
|
16
|
+
foreignKey: ForeignKeyInfo | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TableSchema {
|
|
20
|
+
columns: ColumnInfo[];
|
|
21
|
+
primaryKeys: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Browser types ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface SqlBrowserProps {
|
|
27
|
+
tables?: D1Table[] | null;
|
|
28
|
+
execQuery: (sql: string) => Promise<QueryResult>;
|
|
29
|
+
historyScope?: string;
|
|
30
|
+
basePath?: string;
|
|
31
|
+
routeTab?: Tab;
|
|
32
|
+
routeTable?: string | null;
|
|
33
|
+
routeQuery?: URLSearchParams;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Tab = "data" | "schema" | "sql";
|
|
37
|
+
|
|
38
|
+
export type SortDir = "ASC" | "DESC";
|
|
39
|
+
|
|
40
|
+
export const PAGE_SIZE = 50;
|
|
41
|
+
|
|
42
|
+
export const TABS: { key: Tab; label: string }[] = [
|
|
43
|
+
{ key: "data", label: "Data Browser" },
|
|
44
|
+
{ key: "schema", label: "Schema" },
|
|
45
|
+
{ key: "sql", label: "SQL Console" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// ─── History types ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export interface HistoryEntry {
|
|
51
|
+
sql: string;
|
|
52
|
+
ts: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BrowserHistoryEntry {
|
|
56
|
+
table: string;
|
|
57
|
+
filters: Record<string, string>;
|
|
58
|
+
sortCol: string | null;
|
|
59
|
+
sortDir: SortDir;
|
|
60
|
+
ts: number;
|
|
61
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { TableSchema, ColumnInfo, ForeignKeyInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
// ─── Schema parsing ──────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export function parseCreateTable(sql: string): TableSchema {
|
|
6
|
+
const columns: ColumnInfo[] = [];
|
|
7
|
+
const primaryKeys: string[] = [];
|
|
8
|
+
|
|
9
|
+
// Extract the part between the outer parentheses
|
|
10
|
+
const bodyMatch = sql.match(/\((.+)\)\s*$/s);
|
|
11
|
+
if (!bodyMatch) return { columns, primaryKeys };
|
|
12
|
+
|
|
13
|
+
// Split on commas that are not inside nested parens
|
|
14
|
+
const parts: string[] = [];
|
|
15
|
+
let depth = 0;
|
|
16
|
+
let current = "";
|
|
17
|
+
for (const ch of bodyMatch[1]!) {
|
|
18
|
+
if (ch === "(") depth++;
|
|
19
|
+
else if (ch === ")") depth--;
|
|
20
|
+
else if (ch === "," && depth === 0) {
|
|
21
|
+
parts.push(current.trim());
|
|
22
|
+
current = "";
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
current += ch;
|
|
26
|
+
}
|
|
27
|
+
if (current.trim()) parts.push(current.trim());
|
|
28
|
+
|
|
29
|
+
const foreignKeys: Record<string, ForeignKeyInfo> = {};
|
|
30
|
+
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
// Table-level PRIMARY KEY(col1, col2)
|
|
33
|
+
const pkMatch = part.match(/^PRIMARY\s+KEY\s*\((.+)\)/i);
|
|
34
|
+
if (pkMatch) {
|
|
35
|
+
for (const col of pkMatch[1]!.split(",")) {
|
|
36
|
+
const name = col.trim().replace(/^["'`]|["'`]$/g, "");
|
|
37
|
+
if (name && !primaryKeys.includes(name)) primaryKeys.push(name);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Table-level FOREIGN KEY(col) REFERENCES table(col)
|
|
43
|
+
const fkMatch = part.match(/^(?:CONSTRAINT\s+["'`]?\w+["'`]?\s+)?FOREIGN\s+KEY\s*\(["'`]?(\w+)["'`]?\)\s*REFERENCES\s+["'`]?(\w+)["'`]?\s*\(["'`]?(\w+)["'`]?\)/i);
|
|
44
|
+
if (fkMatch) {
|
|
45
|
+
foreignKeys[fkMatch[1]!] = { targetTable: fkMatch[2]!, targetColumn: fkMatch[3]! };
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Skip other constraints (UNIQUE, CHECK)
|
|
50
|
+
if (/^(UNIQUE|CHECK|CONSTRAINT)\s/i.test(part)) continue;
|
|
51
|
+
|
|
52
|
+
// Column definition
|
|
53
|
+
const colMatch = part.match(/^["'`]?(\w+)["'`]?\s+(.*)/s);
|
|
54
|
+
if (!colMatch) continue;
|
|
55
|
+
|
|
56
|
+
const name = colMatch[1]!;
|
|
57
|
+
const rest = colMatch[2]!;
|
|
58
|
+
const typePart = rest.match(/^(\w[\w\s()]*?)(?:\s+(?:NOT|NULL|DEFAULT|PRIMARY|UNIQUE|CHECK|REFERENCES|AUTOINCREMENT|AUTO_INCREMENT)|$)/i);
|
|
59
|
+
const type = typePart ? typePart[1]!.trim() : rest.split(/\s/)[0] ?? "";
|
|
60
|
+
const notNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
61
|
+
const autoIncrement = /\b(?:AUTOINCREMENT|AUTO_INCREMENT)\b/i.test(rest);
|
|
62
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(\S+)/i);
|
|
63
|
+
const defaultValue = defaultMatch ? defaultMatch[1]! : null;
|
|
64
|
+
|
|
65
|
+
// Inline REFERENCES
|
|
66
|
+
const refMatch = rest.match(/\bREFERENCES\s+["'`]?(\w+)["'`]?\s*\(["'`]?(\w+)["'`]?\)/i);
|
|
67
|
+
const foreignKey: ForeignKeyInfo | null = refMatch
|
|
68
|
+
? { targetTable: refMatch[1]!, targetColumn: refMatch[2]! }
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
columns.push({ name, type, notNull, defaultValue, autoIncrement, foreignKey });
|
|
72
|
+
|
|
73
|
+
if (/\bPRIMARY\s+KEY\b/i.test(rest)) {
|
|
74
|
+
if (!primaryKeys.includes(name)) primaryKeys.push(name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Apply table-level FK constraints to columns
|
|
79
|
+
for (const col of columns) {
|
|
80
|
+
if (!col.foreignKey && foreignKeys[col.name]) {
|
|
81
|
+
col.foreignKey = foreignKeys[col.name]!;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { columns, primaryKeys };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── SQL helpers ─────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export function sqlLiteral(value: unknown): string {
|
|
91
|
+
if (value === null || value === undefined) return "NULL";
|
|
92
|
+
if (typeof value === "number") return String(value);
|
|
93
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
94
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function quoteId(name: string): string {
|
|
98
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseFilterExpr(col: string, expr: string): string | null {
|
|
102
|
+
const trimmed = expr.trim();
|
|
103
|
+
if (!trimmed) return null;
|
|
104
|
+
|
|
105
|
+
const lower = trimmed.toLowerCase();
|
|
106
|
+
if (lower === "null") return `${quoteId(col)} IS NULL`;
|
|
107
|
+
if (lower === "!null" || lower === "not null") return `${quoteId(col)} IS NOT NULL`;
|
|
108
|
+
|
|
109
|
+
// Operators: >=, <=, !=, >, <, =
|
|
110
|
+
const opMatch = trimmed.match(/^(>=|<=|!=|>|<|=)\s*(.+)$/);
|
|
111
|
+
if (opMatch) {
|
|
112
|
+
const [, op, val] = opMatch;
|
|
113
|
+
return `${quoteId(col)} ${op} ${sqlLiteral(val!)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// LIKE pattern (contains %)
|
|
117
|
+
if (trimmed.includes("%")) {
|
|
118
|
+
return `${quoteId(col)} LIKE ${sqlLiteral(trimmed)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Negation: !value
|
|
122
|
+
if (trimmed.startsWith("!")) {
|
|
123
|
+
return `${quoteId(col)} != ${sqlLiteral(trimmed.slice(1))}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Default: contains
|
|
127
|
+
return `${quoteId(col)} LIKE ${sqlLiteral("%" + trimmed + "%")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function buildWhereClause(filters: Record<string, string>): string {
|
|
131
|
+
const conditions: string[] = [];
|
|
132
|
+
for (const [col, expr] of Object.entries(filters)) {
|
|
133
|
+
const cond = parseFilterExpr(col, expr);
|
|
134
|
+
if (cond) conditions.push(cond);
|
|
135
|
+
}
|
|
136
|
+
return conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Export helpers ──────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function downloadBlob(content: string, filename: string, mimeType: string) {
|
|
142
|
+
const blob = new Blob([content], { type: mimeType });
|
|
143
|
+
const url = URL.createObjectURL(blob);
|
|
144
|
+
const a = document.createElement("a");
|
|
145
|
+
a.href = url;
|
|
146
|
+
a.download = filename;
|
|
147
|
+
a.click();
|
|
148
|
+
URL.revokeObjectURL(url);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function exportCSV(columns: string[], rows: Record<string, unknown>[], tableName: string) {
|
|
152
|
+
const escape = (v: unknown) => {
|
|
153
|
+
if (v === null || v === undefined) return "";
|
|
154
|
+
const s = String(v);
|
|
155
|
+
if (s.includes(",") || s.includes('"') || s.includes("\n")) {
|
|
156
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
157
|
+
}
|
|
158
|
+
return s;
|
|
159
|
+
};
|
|
160
|
+
const header = columns.map(escape).join(",");
|
|
161
|
+
const body = rows.map(row => columns.map(col => escape(row[col])).join(",")).join("\n");
|
|
162
|
+
downloadBlob(header + "\n" + body, `${tableName}.csv`, "text/csv");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function exportJSON(rows: Record<string, unknown>[], tableName: string) {
|
|
166
|
+
downloadBlob(JSON.stringify(rows, null, 2), `${tableName}.json`, "application/json");
|
|
167
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
@import "tailwindcss" source(".");
|
|
2
|
+
|
|
3
|
+
html {
|
|
4
|
+
font-size: 110%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
@theme {
|
|
8
|
+
--color-surface: #f9fafb;
|
|
9
|
+
--color-surface-raised: #f3f4f6;
|
|
10
|
+
--color-surface-hover: #f3f4f6;
|
|
11
|
+
--color-surface-dim: #e5e7eb;
|
|
12
|
+
--color-ink: #1A1A1A;
|
|
13
|
+
--color-ink-muted: #333333;
|
|
14
|
+
--color-accent-lime: #DFFF40;
|
|
15
|
+
--color-accent-blue: #9BB5FC;
|
|
16
|
+
--color-accent-olive: #7C8B00;
|
|
17
|
+
--color-panel: #ffffff;
|
|
18
|
+
--color-panel-secondary: #f9fafb;
|
|
19
|
+
--color-panel-hover: #f3f4f6;
|
|
20
|
+
--color-panel-active: #e5e7eb;
|
|
21
|
+
--color-border: #e5e7eb;
|
|
22
|
+
--color-border-subtle: #f3f4f6;
|
|
23
|
+
--color-border-row: #f9fafb;
|
|
24
|
+
--color-text-secondary: #6b7280;
|
|
25
|
+
--color-text-muted: #9ca3af;
|
|
26
|
+
--color-text-dim: #d1d5db;
|
|
27
|
+
--color-text-data: #4b5563;
|
|
28
|
+
--color-bar: #f3f4f6;
|
|
29
|
+
--color-code-bg: #f9fafb;
|
|
30
|
+
--color-link: #2563eb;
|
|
31
|
+
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
32
|
+
--shadow-card-hover: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
|
33
|
+
--shadow-lime-glow: none;
|
|
34
|
+
--shadow-focus: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
|
35
|
+
--shadow-focus-soft: 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|
36
|
+
--radius-card: 10px;
|
|
37
|
+
--color-error-highlight: #fef2f2;
|
|
38
|
+
--color-error-ring: #fca5a5;
|
|
39
|
+
--color-error-icon-bg: #fef2f2;
|
|
40
|
+
--color-error-icon-text: #ef4444;
|
|
41
|
+
--color-btn-danger-hover-bg: #fef2f2;
|
|
42
|
+
--color-btn-danger-hover-text: #dc2626;
|
|
43
|
+
--color-btn-danger-hover-border: #fecaca;
|
|
44
|
+
--color-badge-red-bg: #fef2f2;
|
|
45
|
+
--color-badge-red-text: #b91c1c;
|
|
46
|
+
--color-badge-blue-bg: #eff6ff;
|
|
47
|
+
--color-badge-blue-text: #1d4ed8;
|
|
48
|
+
--color-badge-purple-bg: #faf5ff;
|
|
49
|
+
--color-badge-purple-text: #7e22ce;
|
|
50
|
+
--color-badge-orange-bg: #fff7ed;
|
|
51
|
+
--color-badge-orange-text: #c2410c;
|
|
52
|
+
--color-badge-yellow-bg: #fefce8;
|
|
53
|
+
--color-badge-yellow-text: #a16207;
|
|
54
|
+
--color-badge-emerald-bg: #ecfdf5;
|
|
55
|
+
--color-badge-emerald-text: #047857;
|
|
56
|
+
--color-span-ok: #059669;
|
|
57
|
+
--color-span-error: #ef4444;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Explicit dark mode */
|
|
61
|
+
html[data-theme="dark"] {
|
|
62
|
+
--color-surface: #0c0c0c;
|
|
63
|
+
--color-surface-raised: #1a1a1a;
|
|
64
|
+
--color-surface-hover: #1e1e1e;
|
|
65
|
+
--color-surface-dim: #2a2a2a;
|
|
66
|
+
--color-ink: #f5f5f5;
|
|
67
|
+
--color-ink-muted: #d4d4d4;
|
|
68
|
+
--color-panel: #161616;
|
|
69
|
+
--color-panel-secondary: #1a1a1a;
|
|
70
|
+
--color-panel-hover: #1e1e1e;
|
|
71
|
+
--color-panel-active: #2a2a2a;
|
|
72
|
+
--color-border: #2a2a2a;
|
|
73
|
+
--color-border-subtle: #1e1e1e;
|
|
74
|
+
--color-border-row: #141414;
|
|
75
|
+
--color-text-secondary: #a3a3a3;
|
|
76
|
+
--color-text-muted: #737373;
|
|
77
|
+
--color-text-dim: #525252;
|
|
78
|
+
--color-text-data: #d4d4d4;
|
|
79
|
+
--color-bar: #1c1c1c;
|
|
80
|
+
--color-code-bg: #1c1c1c;
|
|
81
|
+
--color-link: #60a5fa;
|
|
82
|
+
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
|
83
|
+
--shadow-card-hover: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
|
84
|
+
--shadow-focus: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
|
85
|
+
--shadow-focus-soft: 0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
86
|
+
--color-error-highlight: rgba(127, 29, 29, 0.4);
|
|
87
|
+
--color-error-ring: #7f1d1d;
|
|
88
|
+
--color-error-icon-bg: rgba(127, 29, 29, 0.4);
|
|
89
|
+
--color-error-icon-text: #f87171;
|
|
90
|
+
--color-btn-danger-hover-bg: rgba(127, 29, 29, 0.3);
|
|
91
|
+
--color-btn-danger-hover-text: #f87171;
|
|
92
|
+
--color-btn-danger-hover-border: #991b1b;
|
|
93
|
+
--color-badge-red-bg: rgba(127, 29, 29, 0.4);
|
|
94
|
+
--color-badge-red-text: #f87171;
|
|
95
|
+
--color-badge-blue-bg: rgba(23, 37, 84, 0.4);
|
|
96
|
+
--color-badge-blue-text: #60a5fa;
|
|
97
|
+
--color-badge-purple-bg: rgba(59, 7, 100, 0.4);
|
|
98
|
+
--color-badge-purple-text: #c084fc;
|
|
99
|
+
--color-badge-orange-bg: rgba(124, 45, 18, 0.4);
|
|
100
|
+
--color-badge-orange-text: #fb923c;
|
|
101
|
+
--color-badge-yellow-bg: rgba(113, 63, 18, 0.4);
|
|
102
|
+
--color-badge-yellow-text: #facc15;
|
|
103
|
+
--color-badge-emerald-bg: rgba(6, 78, 59, 0.4);
|
|
104
|
+
--color-badge-emerald-text: #34d399;
|
|
105
|
+
--color-span-ok: #047857;
|
|
106
|
+
--color-span-error: #ef4444;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Auto dark mode (follows system when no explicit theme) */
|
|
110
|
+
@media (prefers-color-scheme: dark) {
|
|
111
|
+
html:not([data-theme="light"]):not([data-theme="dark"]) {
|
|
112
|
+
--color-surface: #0c0c0c;
|
|
113
|
+
--color-surface-raised: #1a1a1a;
|
|
114
|
+
--color-surface-hover: #1e1e1e;
|
|
115
|
+
--color-surface-dim: #2a2a2a;
|
|
116
|
+
--color-ink: #f5f5f5;
|
|
117
|
+
--color-ink-muted: #d4d4d4;
|
|
118
|
+
--color-panel: #161616;
|
|
119
|
+
--color-panel-secondary: #1a1a1a;
|
|
120
|
+
--color-panel-hover: #1e1e1e;
|
|
121
|
+
--color-panel-active: #2a2a2a;
|
|
122
|
+
--color-border: #2a2a2a;
|
|
123
|
+
--color-border-subtle: #1e1e1e;
|
|
124
|
+
--color-border-row: #141414;
|
|
125
|
+
--color-text-secondary: #a3a3a3;
|
|
126
|
+
--color-text-muted: #737373;
|
|
127
|
+
--color-text-dim: #525252;
|
|
128
|
+
--color-text-data: #d4d4d4;
|
|
129
|
+
--color-bar: #1c1c1c;
|
|
130
|
+
--color-code-bg: #1c1c1c;
|
|
131
|
+
--color-link: #60a5fa;
|
|
132
|
+
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
|
133
|
+
--shadow-card-hover: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
|
134
|
+
--shadow-focus: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
|
135
|
+
--shadow-focus-soft: 0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
136
|
+
--color-error-highlight: rgba(127, 29, 29, 0.4);
|
|
137
|
+
--color-error-ring: #7f1d1d;
|
|
138
|
+
--color-error-icon-bg: rgba(127, 29, 29, 0.4);
|
|
139
|
+
--color-error-icon-text: #f87171;
|
|
140
|
+
--color-btn-danger-hover-bg: rgba(127, 29, 29, 0.3);
|
|
141
|
+
--color-btn-danger-hover-text: #f87171;
|
|
142
|
+
--color-btn-danger-hover-border: #991b1b;
|
|
143
|
+
--color-badge-red-bg: rgba(127, 29, 29, 0.4);
|
|
144
|
+
--color-badge-red-text: #f87171;
|
|
145
|
+
--color-badge-blue-bg: rgba(23, 37, 84, 0.4);
|
|
146
|
+
--color-badge-blue-text: #60a5fa;
|
|
147
|
+
--color-badge-purple-bg: rgba(59, 7, 100, 0.4);
|
|
148
|
+
--color-badge-purple-text: #c084fc;
|
|
149
|
+
--color-badge-orange-bg: rgba(124, 45, 18, 0.4);
|
|
150
|
+
--color-badge-orange-text: #fb923c;
|
|
151
|
+
--color-badge-yellow-bg: rgba(113, 63, 18, 0.4);
|
|
152
|
+
--color-badge-yellow-text: #facc15;
|
|
153
|
+
--color-badge-emerald-bg: rgba(6, 78, 59, 0.4);
|
|
154
|
+
--color-badge-emerald-text: #34d399;
|
|
155
|
+
--color-span-ok: #047857;
|
|
156
|
+
--color-span-error: #ef4444;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Scrollbar — not possible via Tailwind */
|
|
161
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
162
|
+
width: 6px;
|
|
163
|
+
}
|
|
164
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
165
|
+
background: transparent;
|
|
166
|
+
}
|
|
167
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
168
|
+
background: var(--color-text-dim);
|
|
169
|
+
border-radius: 3px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Danger button hover */
|
|
173
|
+
.btn-danger:hover {
|
|
174
|
+
background: var(--color-btn-danger-hover-bg);
|
|
175
|
+
color: var(--color-btn-danger-hover-text);
|
|
176
|
+
border-color: var(--color-btn-danger-hover-border);
|
|
177
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import { formatTime } from "../lib";
|
|
3
|
+
import { useQuery, useMutation } from "../rpc/hooks";
|
|
4
|
+
import { EmptyState, Table, PageHeader, PillButton, DeleteButton, StatusBadge, ServiceInfo, RefreshButton } from "../components";
|
|
5
|
+
|
|
6
|
+
const AI_STATUS_COLORS: Record<string, string> = {
|
|
7
|
+
ok: "bg-emerald-100 text-emerald-700",
|
|
8
|
+
error: "bg-red-100 text-red-700",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AiView({ route }: { route: string }) {
|
|
12
|
+
const parts = route.split("/").filter(Boolean);
|
|
13
|
+
if (parts.length >= 2) return <AiDetail id={parts[1]!} />;
|
|
14
|
+
return <AiList />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function AiList() {
|
|
18
|
+
const [modelFilter, setModelFilter] = useState("");
|
|
19
|
+
const [statusFilter, setStatusFilter] = useState("");
|
|
20
|
+
const { data: requests, refetch } = useQuery("ai.list", {
|
|
21
|
+
model: modelFilter || undefined,
|
|
22
|
+
status: statusFilter || undefined,
|
|
23
|
+
});
|
|
24
|
+
const { data: stats } = useQuery("ai.stats");
|
|
25
|
+
const { data: models } = useQuery("ai.models");
|
|
26
|
+
const { data: configGroups } = useQuery("config.forService", { type: "ai" });
|
|
27
|
+
const deleteReq = useMutation("ai.delete");
|
|
28
|
+
|
|
29
|
+
const handleDelete = async (id: string) => {
|
|
30
|
+
if (!confirm("Delete this AI request log?")) return;
|
|
31
|
+
await deleteReq.mutate({ id });
|
|
32
|
+
refetch();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div class="p-8 max-w-6xl">
|
|
37
|
+
<PageHeader title="Workers AI" subtitle={`${stats?.total ?? 0} request(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
38
|
+
<div class="flex gap-6 items-start">
|
|
39
|
+
<div class="flex-1 min-w-0">
|
|
40
|
+
<div class="mb-6 flex gap-2 items-center flex-wrap">
|
|
41
|
+
<PillButton onClick={() => setStatusFilter("")} active={statusFilter === ""}>
|
|
42
|
+
All
|
|
43
|
+
</PillButton>
|
|
44
|
+
<PillButton onClick={() => setStatusFilter("ok")} active={statusFilter === "ok"}>
|
|
45
|
+
OK
|
|
46
|
+
</PillButton>
|
|
47
|
+
<PillButton onClick={() => setStatusFilter("error")} active={statusFilter === "error"}>
|
|
48
|
+
Error
|
|
49
|
+
</PillButton>
|
|
50
|
+
{(models?.length ?? 0) > 0 && (
|
|
51
|
+
<select
|
|
52
|
+
value={modelFilter}
|
|
53
|
+
onChange={e => setModelFilter((e.target as HTMLSelectElement).value)}
|
|
54
|
+
class="ml-2 text-xs bg-panel-secondary border border-border rounded-md px-2 py-1 outline-none"
|
|
55
|
+
>
|
|
56
|
+
<option value="">All models</option>
|
|
57
|
+
{models!.map(m => (
|
|
58
|
+
<option key={m} value={m}>{m}</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
{!requests?.length ? (
|
|
64
|
+
<EmptyState message="No AI requests found" />
|
|
65
|
+
) : (
|
|
66
|
+
<Table
|
|
67
|
+
headers={["Model", "Status", "Duration", "Stream", "Time", ""]}
|
|
68
|
+
rows={requests.map(r => [
|
|
69
|
+
<a href={`#/ai/${r.id}`} class="font-mono text-xs text-blue-600 hover:underline max-w-[200px] truncate block">{r.model}</a>,
|
|
70
|
+
<StatusBadge status={r.status} colorMap={AI_STATUS_COLORS} />,
|
|
71
|
+
<span class="text-xs text-text-muted tabular-nums">{r.duration_ms}ms</span>,
|
|
72
|
+
r.is_streaming ? <span class="text-xs text-purple-500">yes</span> : <span class="text-xs text-text-dim">no</span>,
|
|
73
|
+
<span class="text-xs text-text-muted">{formatTime(r.created_at)}</span>,
|
|
74
|
+
<DeleteButton onClick={() => handleDelete(r.id)} />,
|
|
75
|
+
])}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
<ServiceInfo
|
|
80
|
+
description="Workers AI binding — proxies requests to Cloudflare AI API."
|
|
81
|
+
stats={[
|
|
82
|
+
{ label: "Total", value: stats?.total ?? 0 },
|
|
83
|
+
{ label: "Avg duration", value: `${stats?.avgDuration ?? 0}ms` },
|
|
84
|
+
...(stats?.byStatus ? Object.entries(stats.byStatus).map(([k, v]) => ({ label: k, value: v })) : []),
|
|
85
|
+
]}
|
|
86
|
+
configGroups={configGroups}
|
|
87
|
+
links={[
|
|
88
|
+
{ label: "Workers AI docs", href: "https://developers.cloudflare.com/workers-ai/" },
|
|
89
|
+
{ label: "Models", href: "https://developers.cloudflare.com/workers-ai/models/" },
|
|
90
|
+
]}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function AiDetail({ id }: { id: string }) {
|
|
98
|
+
const { data } = useQuery("ai.get", { id });
|
|
99
|
+
|
|
100
|
+
if (!data) {
|
|
101
|
+
return (
|
|
102
|
+
<div class="p-8">
|
|
103
|
+
<a href="#/ai" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to AI requests</a>
|
|
104
|
+
<EmptyState message="Request not found" />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div class="p-8 max-w-4xl">
|
|
111
|
+
<a href="#/ai" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to AI requests</a>
|
|
112
|
+
<div class="bg-panel border border-border rounded-lg p-6">
|
|
113
|
+
<div class="flex items-center justify-between mb-4">
|
|
114
|
+
<h2 class="text-lg font-semibold text-ink">AI Request Detail</h2>
|
|
115
|
+
<StatusBadge status={data.status} colorMap={AI_STATUS_COLORS} />
|
|
116
|
+
</div>
|
|
117
|
+
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
|
|
118
|
+
<div>
|
|
119
|
+
<span class="text-text-muted">Model:</span>{" "}
|
|
120
|
+
<span class="font-mono">{data.model}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div>
|
|
123
|
+
<span class="text-text-muted">Duration:</span>{" "}
|
|
124
|
+
<span class="tabular-nums">{data.duration_ms}ms</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div>
|
|
127
|
+
<span class="text-text-muted">Streaming:</span>{" "}
|
|
128
|
+
{data.is_streaming ? "Yes" : "No"}
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<span class="text-text-muted">Time:</span>{" "}
|
|
132
|
+
{formatTime(data.created_at)}
|
|
133
|
+
</div>
|
|
134
|
+
{data.error && (
|
|
135
|
+
<div class="col-span-2">
|
|
136
|
+
<span class="text-text-muted">Error:</span>{" "}
|
|
137
|
+
<span class="text-red-600">{data.error}</span>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
<div class="mb-4">
|
|
142
|
+
<div class="text-xs text-text-muted mb-2">Input</div>
|
|
143
|
+
<pre class="bg-panel-secondary border border-border rounded-lg p-4 text-xs font-mono overflow-x-auto whitespace-pre-wrap max-h-64 overflow-y-auto">{data.input_summary ?? "<empty>"}</pre>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<div class="text-xs text-text-muted mb-2">Output</div>
|
|
147
|
+
<pre class="bg-panel-secondary border border-border rounded-lg p-4 text-xs font-mono overflow-x-auto whitespace-pre-wrap max-h-64 overflow-y-auto">{data.output_summary ?? "<empty>"}</pre>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|