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,174 @@
|
|
|
1
|
+
import { join, dirname } from "node:path";
|
|
2
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import type { WranglerConfig } from "../config";
|
|
4
|
+
import type { GenerationManager } from "../generation-manager";
|
|
5
|
+
import type { WorkerRegistry } from "../worker-registry";
|
|
6
|
+
import type { HandlerContext } from "./rpc/types";
|
|
7
|
+
import { dispatch } from "./rpc/server";
|
|
8
|
+
import { getDatabase, getDataDir } from "../db";
|
|
9
|
+
|
|
10
|
+
const ctx: HandlerContext = { config: null, manager: null, registry: null };
|
|
11
|
+
|
|
12
|
+
export function setDashboardConfig(config: WranglerConfig): void {
|
|
13
|
+
ctx.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function setGenerationManager(manager: GenerationManager): void {
|
|
17
|
+
ctx.manager = manager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setWorkerRegistry(registry: WorkerRegistry): void {
|
|
21
|
+
ctx.registry = registry;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Pre-built dashboard assets ──────────────────────────────────────────
|
|
25
|
+
// We use Bun.build() with bun-plugin-tailwind to pre-build the dashboard
|
|
26
|
+
// so Tailwind CSS works regardless of the CWD where bunflare is launched.
|
|
27
|
+
|
|
28
|
+
let dashboardAssets: Map<string, { content: Uint8Array; contentType: string }> | null = null;
|
|
29
|
+
let dashboardHtmlContent: string | null = null;
|
|
30
|
+
|
|
31
|
+
async function buildDashboard(): Promise<void> {
|
|
32
|
+
const tailwindPlugin = (await import("bun-plugin-tailwind")).default;
|
|
33
|
+
const htmlEntry = join(import.meta.dir, "index.html");
|
|
34
|
+
|
|
35
|
+
const result = await Bun.build({
|
|
36
|
+
entrypoints: [htmlEntry],
|
|
37
|
+
plugins: [tailwindPlugin],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
console.error("[bunflare] Dashboard build failed:", result.logs);
|
|
42
|
+
throw new Error("Dashboard build failed");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const assets = new Map<string, { content: Uint8Array; contentType: string }>();
|
|
46
|
+
let html = "";
|
|
47
|
+
|
|
48
|
+
for (const output of result.outputs) {
|
|
49
|
+
const name = output.path.split("/").pop()!;
|
|
50
|
+
const content = new Uint8Array(await output.arrayBuffer());
|
|
51
|
+
|
|
52
|
+
if (output.kind === "entry-point" && name.endsWith(".html")) {
|
|
53
|
+
html = new TextDecoder().decode(content);
|
|
54
|
+
} else {
|
|
55
|
+
const contentType = name.endsWith(".css")
|
|
56
|
+
? "text/css"
|
|
57
|
+
: name.endsWith(".js")
|
|
58
|
+
? "application/javascript"
|
|
59
|
+
: "application/octet-stream";
|
|
60
|
+
assets.set(name, { content, contentType });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Rewrite asset paths in HTML from "./chunk-xxx" to "/__dashboard/assets/chunk-xxx"
|
|
65
|
+
for (const name of assets.keys()) {
|
|
66
|
+
html = html.replaceAll(`./${name}`, `/__dashboard/assets/${name}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dashboardHtmlContent = html;
|
|
70
|
+
dashboardAssets = assets;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build on import
|
|
74
|
+
await buildDashboard();
|
|
75
|
+
|
|
76
|
+
export function handleDashboardRequest(request: Request): Response | Promise<Response> {
|
|
77
|
+
const url = new URL(request.url);
|
|
78
|
+
|
|
79
|
+
// Serve dashboard HTML
|
|
80
|
+
if (url.pathname === "/__dashboard") {
|
|
81
|
+
return new Response(dashboardHtmlContent, {
|
|
82
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Serve dashboard assets (JS, CSS)
|
|
87
|
+
const assetMatch = url.pathname.match(/^\/__dashboard\/assets\/(.+)$/);
|
|
88
|
+
if (assetMatch && dashboardAssets) {
|
|
89
|
+
const asset = dashboardAssets.get(assetMatch[1]!);
|
|
90
|
+
if (asset) {
|
|
91
|
+
return new Response(asset.content as unknown as BodyInit, {
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": asset.contentType,
|
|
94
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// RPC endpoint
|
|
101
|
+
if (url.pathname === "/__dashboard/api/rpc" && request.method === "POST") {
|
|
102
|
+
return dispatch(request, ctx);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// R2 upload (multipart/form-data)
|
|
106
|
+
if (url.pathname === "/__dashboard/api/r2/upload" && request.method === "POST") {
|
|
107
|
+
return handleR2Upload(request);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// R2 download
|
|
111
|
+
if (url.pathname === "/__dashboard/api/r2/download" && request.method === "GET") {
|
|
112
|
+
return handleR2Download(url);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return new Response("Not found", { status: 404 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function handleR2Upload(request: Request): Promise<Response> {
|
|
119
|
+
try {
|
|
120
|
+
const formData = await request.formData();
|
|
121
|
+
const bucket = formData.get("bucket") as string;
|
|
122
|
+
const key = formData.get("key") as string;
|
|
123
|
+
const file = formData.get("file") as File;
|
|
124
|
+
|
|
125
|
+
if (!bucket || !key || !file) {
|
|
126
|
+
return Response.json({ error: "Missing bucket, key, or file" }, { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await file.arrayBuffer();
|
|
130
|
+
const fp = join(getDataDir(), "r2", bucket, key);
|
|
131
|
+
mkdirSync(dirname(fp), { recursive: true });
|
|
132
|
+
await Bun.write(fp, data);
|
|
133
|
+
|
|
134
|
+
const hasher = new Bun.CryptoHasher("md5");
|
|
135
|
+
hasher.update(new Uint8Array(data));
|
|
136
|
+
const etag = hasher.digest("hex");
|
|
137
|
+
|
|
138
|
+
const db = getDatabase();
|
|
139
|
+
db.run(
|
|
140
|
+
`INSERT OR REPLACE INTO r2_objects (bucket, key, size, etag, version, uploaded, http_metadata, custom_metadata)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)`,
|
|
142
|
+
[bucket, key, data.byteLength, etag, crypto.randomUUID(), new Date().toISOString()],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return Response.json({ ok: true });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error("[bunflare dashboard] R2 upload error:", err);
|
|
148
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleR2Download(url: URL): Response {
|
|
153
|
+
const bucket = url.searchParams.get("bucket");
|
|
154
|
+
const key = url.searchParams.get("key");
|
|
155
|
+
|
|
156
|
+
if (!bucket || !key) {
|
|
157
|
+
return Response.json({ error: "Missing bucket or key" }, { status: 400 });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fp = join(getDataDir(), "r2", bucket, key);
|
|
161
|
+
const file = Bun.file(fp);
|
|
162
|
+
|
|
163
|
+
if (!existsSync(fp)) {
|
|
164
|
+
return new Response("Not found", { status: 404 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const filename = key.split("/").pop() ?? key;
|
|
168
|
+
return new Response(file as unknown as BodyInit, {
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
171
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { render } from "preact";
|
|
2
|
+
import { useState, useEffect } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
import { useRoute } from "./lib";
|
|
5
|
+
import { useQuery } from "./rpc/hooks";
|
|
6
|
+
import { HomeView } from "./views/home";
|
|
7
|
+
import { KvView } from "./views/kv";
|
|
8
|
+
import { R2View } from "./views/r2";
|
|
9
|
+
import { QueueView } from "./views/queue";
|
|
10
|
+
import { DoView } from "./views/do";
|
|
11
|
+
import { WorkflowsView } from "./views/workflows";
|
|
12
|
+
import { ContainersView } from "./views/containers";
|
|
13
|
+
import { D1View } from "./views/d1";
|
|
14
|
+
import { CacheView } from "./views/cache";
|
|
15
|
+
import { WorkersView } from "./views/workers";
|
|
16
|
+
import { TracesView } from "./views/traces";
|
|
17
|
+
import { ErrorsView } from "./views/errors";
|
|
18
|
+
import { ScheduledView } from "./views/scheduled";
|
|
19
|
+
import { EmailView } from "./views/email";
|
|
20
|
+
import { AiView } from "./views/ai";
|
|
21
|
+
import { AnalyticsEngineView } from "./views/analytics-engine";
|
|
22
|
+
|
|
23
|
+
interface NavItem {
|
|
24
|
+
path: string;
|
|
25
|
+
label: string;
|
|
26
|
+
icon: string;
|
|
27
|
+
badge?: (counts: Record<string, number>) => number | null;
|
|
28
|
+
badgeColor?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface NavGroup {
|
|
32
|
+
label: string;
|
|
33
|
+
items: NavItem[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const NAV_TOP: NavItem[] = [
|
|
37
|
+
{ path: "/", label: "Overview", icon: "◉" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const NAV_GROUPS: NavGroup[] = [
|
|
41
|
+
{
|
|
42
|
+
label: "Observability",
|
|
43
|
+
items: [
|
|
44
|
+
{
|
|
45
|
+
path: "/errors", label: "Errors", icon: "⚠\uFE0E",
|
|
46
|
+
badge: c => c.errors || null, badgeColor: "bg-red-500",
|
|
47
|
+
},
|
|
48
|
+
{ path: "/traces", label: "Traces", icon: "⟡" },
|
|
49
|
+
{ path: "/analytics", label: "Analytics Engine", icon: "⊘" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Compute",
|
|
54
|
+
items: [
|
|
55
|
+
{ path: "/workers", label: "Workers", icon: "⊡" },
|
|
56
|
+
{ path: "/do", label: "Durable Objects", icon: "⬢" },
|
|
57
|
+
{ path: "/containers", label: "Containers", icon: "▣" },
|
|
58
|
+
{ path: "/workflows", label: "Workflows", icon: "⇶" },
|
|
59
|
+
{ path: "/scheduled", label: "Scheduled", icon: "⏱\uFE0E" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "Storage",
|
|
64
|
+
items: [
|
|
65
|
+
{ path: "/kv", label: "KV", icon: "⬡" },
|
|
66
|
+
{ path: "/r2", label: "R2", icon: "◧" },
|
|
67
|
+
{ path: "/d1", label: "D1", icon: "⊞" },
|
|
68
|
+
{ path: "/cache", label: "Cache", icon: "◎" },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "Messaging",
|
|
73
|
+
items: [
|
|
74
|
+
{ path: "/queue", label: "Queues", icon: "☰" },
|
|
75
|
+
{ path: "/email", label: "Email", icon: "✉\uFE0E" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: "AI",
|
|
80
|
+
items: [
|
|
81
|
+
{ path: "/ai", label: "AI", icon: "⚡" },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function NavLink({ item, active, counts }: { item: NavItem; active: boolean; counts: Record<string, number> }) {
|
|
87
|
+
const badgeValue = item.badge?.(counts) ?? null;
|
|
88
|
+
return (
|
|
89
|
+
<a
|
|
90
|
+
href={`#${item.path}`}
|
|
91
|
+
class={`flex items-center gap-2.5 px-3 py-1.5 text-sm no-underline rounded-md transition-colors ${
|
|
92
|
+
active
|
|
93
|
+
? "bg-panel-hover text-ink font-medium"
|
|
94
|
+
: "text-text-secondary hover:bg-panel-hover hover:text-ink"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
<span class="w-4 text-center text-sm opacity-60">{item.icon}</span>
|
|
98
|
+
<span class="flex-1">{item.label}</span>
|
|
99
|
+
{badgeValue !== null && (
|
|
100
|
+
<span class={`min-w-[20px] h-5 flex items-center justify-center rounded-full text-[10px] font-bold text-white px-1.5 ${item.badgeColor ?? "bg-text-muted"}`}>
|
|
101
|
+
{badgeValue > 99 ? "99+" : badgeValue}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</a>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SidebarGroup({ group, activeSection, counts }: { group: NavGroup; activeSection: string; counts: Record<string, number> }) {
|
|
109
|
+
return (
|
|
110
|
+
<div class="mb-1">
|
|
111
|
+
<div class="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-text-muted">
|
|
112
|
+
{group.label}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="mt-0.5 ml-1">
|
|
115
|
+
{group.items.map(item => (
|
|
116
|
+
<NavLink key={item.path} item={item} active={activeSection === item.path} counts={counts} />
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type Theme = "auto" | "light" | "dark";
|
|
124
|
+
|
|
125
|
+
const THEME_ICONS: Record<Theme, string> = { light: "☀︎", dark: "☾", auto: "◐" };
|
|
126
|
+
const THEME_LABELS: Record<Theme, string> = { light: "Light", dark: "Dark", auto: "Auto" };
|
|
127
|
+
const THEME_CYCLE: Record<Theme, Theme> = { auto: "light", light: "dark", dark: "auto" };
|
|
128
|
+
|
|
129
|
+
function ThemeSwitcher() {
|
|
130
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
131
|
+
const saved = localStorage.getItem("bunflare-theme");
|
|
132
|
+
return saved === "light" || saved === "dark" ? saved : "auto";
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (theme === "auto") {
|
|
137
|
+
document.documentElement.removeAttribute("data-theme");
|
|
138
|
+
localStorage.removeItem("bunflare-theme");
|
|
139
|
+
} else {
|
|
140
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
141
|
+
localStorage.setItem("bunflare-theme", theme);
|
|
142
|
+
}
|
|
143
|
+
}, [theme]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => setTheme(THEME_CYCLE[theme])}
|
|
148
|
+
class="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-text-muted hover:text-ink hover:bg-panel-hover rounded-md transition-colors"
|
|
149
|
+
title={`Theme: ${THEME_LABELS[theme]}`}
|
|
150
|
+
>
|
|
151
|
+
<span class="w-4 text-center text-sm">{THEME_ICONS[theme]}</span>
|
|
152
|
+
<span>{THEME_LABELS[theme]}</span>
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function App() {
|
|
158
|
+
const route = useRoute();
|
|
159
|
+
const activeSection = "/" + (route.split("/")[1] || "");
|
|
160
|
+
const { data: overview } = useQuery("overview.get");
|
|
161
|
+
|
|
162
|
+
const counts: Record<string, number> = overview
|
|
163
|
+
? { errors: overview.errors, kv: overview.kv, r2: overview.r2, queue: overview.queue, do: overview.do, workflows: overview.workflows, containers: overview.containers, d1: overview.d1, cache: overview.cache, scheduled: overview.scheduled, email: overview.email, ai: overview.ai }
|
|
164
|
+
: {};
|
|
165
|
+
|
|
166
|
+
function renderView() {
|
|
167
|
+
if (route === "/" || route === "") return <HomeView />;
|
|
168
|
+
if (route.startsWith("/errors")) return <ErrorsView route={route} />;
|
|
169
|
+
if (route.startsWith("/traces")) return <TracesView />;
|
|
170
|
+
if (route.startsWith("/workers")) return <WorkersView />;
|
|
171
|
+
if (route.startsWith("/kv")) return <KvView route={route} />;
|
|
172
|
+
if (route.startsWith("/r2")) return <R2View route={route} />;
|
|
173
|
+
if (route.startsWith("/queue")) return <QueueView route={route} />;
|
|
174
|
+
if (route.startsWith("/do")) return <DoView route={route} />;
|
|
175
|
+
if (route.startsWith("/workflows")) return <WorkflowsView route={route} />;
|
|
176
|
+
if (route.startsWith("/containers")) return <ContainersView route={route} />;
|
|
177
|
+
if (route.startsWith("/d1")) return <D1View route={route} />;
|
|
178
|
+
if (route.startsWith("/cache")) return <CacheView route={route} />;
|
|
179
|
+
if (route.startsWith("/scheduled")) return <ScheduledView route={route} />;
|
|
180
|
+
if (route.startsWith("/email")) return <EmailView route={route} />;
|
|
181
|
+
if (route.startsWith("/ai")) return <AiView route={route} />;
|
|
182
|
+
if (route.startsWith("/analytics")) return <AnalyticsEngineView route={route} />;
|
|
183
|
+
return <div class="p-8 text-text-muted">Page not found</div>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div class="flex h-full">
|
|
188
|
+
<nav class="w-52 flex-shrink-0 border-r border-border bg-panel flex flex-col">
|
|
189
|
+
<div class="p-4 pb-3">
|
|
190
|
+
<a href="#/" class="flex items-center gap-2.5 no-underline">
|
|
191
|
+
<span class="w-7 h-7 rounded-lg bg-ink flex items-center justify-center text-xs font-bold text-surface">B</span>
|
|
192
|
+
<div>
|
|
193
|
+
<div class="text-sm font-semibold text-ink leading-tight">Bunflare</div>
|
|
194
|
+
<div class="text-[10px] text-text-muted">Dev Dashboard</div>
|
|
195
|
+
</div>
|
|
196
|
+
</a>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="flex-1 overflow-y-auto scrollbar-thin px-2 py-1">
|
|
199
|
+
{NAV_TOP.map(item => (
|
|
200
|
+
<div key={item.path} class="mb-2">
|
|
201
|
+
<NavLink item={item} active={activeSection === item.path} counts={counts} />
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
{NAV_GROUPS.map(group => (
|
|
205
|
+
<SidebarGroup key={group.label} group={group} activeSection={activeSection} counts={counts} />
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
<div class="border-t border-border px-2 py-2">
|
|
209
|
+
<ThemeSwitcher />
|
|
210
|
+
</div>
|
|
211
|
+
</nav>
|
|
212
|
+
|
|
213
|
+
<main class="flex-1 overflow-y-auto scrollbar-thin bg-surface">
|
|
214
|
+
{renderView()}
|
|
215
|
+
</main>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
render(<App />, document.getElementById("app")!);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function Breadcrumb({ items }: { items: { label: string; href?: string }[] }) {
|
|
2
|
+
return (
|
|
3
|
+
<div class="flex items-center gap-2 text-sm text-text-muted mb-8">
|
|
4
|
+
{items.map((item, i) => (
|
|
5
|
+
<span key={i} class="flex items-center gap-2">
|
|
6
|
+
{i > 0 && <span class="text-text-dim">/</span>}
|
|
7
|
+
{item.href ? (
|
|
8
|
+
<a href={item.href} class="text-text-secondary hover:text-ink no-underline font-medium transition-colors">{item.label}</a>
|
|
9
|
+
) : (
|
|
10
|
+
<span class="text-ink font-semibold">{item.label}</span>
|
|
11
|
+
)}
|
|
12
|
+
</span>
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function LoadMoreButton({ onClick }: { onClick: () => void }) {
|
|
2
|
+
return (
|
|
3
|
+
<button onClick={onClick} class="mt-4 rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary hover:bg-panel-hover transition-all">
|
|
4
|
+
Load more
|
|
5
|
+
</button>
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DeleteButton({ onClick }: { onClick: () => void }) {
|
|
10
|
+
return (
|
|
11
|
+
<button onClick={onClick} class="text-red-400 hover:text-red-600 text-xs font-medium rounded-md px-2 py-1 hover:bg-red-50 transition-all">Delete</button>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function DetailField({ label, value, children }: { label: string; value?: string; children?: any }) {
|
|
2
|
+
return (
|
|
3
|
+
<div class="bg-panel rounded-lg border border-border p-5">
|
|
4
|
+
<div class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">{label}</div>
|
|
5
|
+
{value ? <div class="font-mono text-sm font-medium">{value}</div> : children}
|
|
6
|
+
</div>
|
|
7
|
+
);
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function FilterInput({ value, onInput, placeholder, class: className }: { value: string; onInput: (v: string) => void; placeholder?: string; class?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<input
|
|
4
|
+
type="text"
|
|
5
|
+
placeholder={placeholder}
|
|
6
|
+
value={value}
|
|
7
|
+
onInput={e => onInput((e.target as HTMLInputElement).value)}
|
|
8
|
+
class={`bg-panel border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-border focus:ring-1 focus:ring-border transition-all ${className ?? "w-72"}`}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { Modal } from "./modal";
|
|
2
|
+
export { EmptyState } from "./empty-state";
|
|
3
|
+
export { PageHeader } from "./page-header";
|
|
4
|
+
export { Breadcrumb } from "./breadcrumb";
|
|
5
|
+
export { Table } from "./table";
|
|
6
|
+
export { DetailField } from "./detail-field";
|
|
7
|
+
export { CodeBlock } from "./code-block";
|
|
8
|
+
export { FilterInput } from "./filter-input";
|
|
9
|
+
export { PillButton } from "./pill-button";
|
|
10
|
+
export { RefreshButton } from "./refresh-button";
|
|
11
|
+
export { LoadMoreButton, DeleteButton } from "./buttons";
|
|
12
|
+
export { TableLink } from "./table-link";
|
|
13
|
+
export { StatusBadge } from "./status-badge";
|
|
14
|
+
export { ServiceInfo } from "./service-info";
|
|
15
|
+
export { SqlBrowser } from "../sql-browser/index";
|
|
16
|
+
export { KeyValueTable } from "./key-value-table";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function KeyValueTable({ data }: { data: Record<string, string> }) {
|
|
2
|
+
const entries = Object.entries(data);
|
|
3
|
+
if (entries.length === 0) {
|
|
4
|
+
return <div class="px-4 py-3 text-sm text-text-muted">No entries</div>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<table class="w-full text-sm">
|
|
9
|
+
<tbody>
|
|
10
|
+
{entries.map(([key, value]) => (
|
|
11
|
+
<tr key={key} class="border-b border-border-subtle last:border-0 hover:bg-panel-hover/50 transition-colors">
|
|
12
|
+
<td class="px-4 py-2 font-medium text-text-secondary whitespace-nowrap align-top font-mono" style="width: 1%;">
|
|
13
|
+
{key}
|
|
14
|
+
</td>
|
|
15
|
+
<td class="px-4 py-2 text-ink break-all font-mono">
|
|
16
|
+
{value}
|
|
17
|
+
</td>
|
|
18
|
+
</tr>
|
|
19
|
+
))}
|
|
20
|
+
</tbody>
|
|
21
|
+
</table>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
|
|
3
|
+
export function Modal({ title, onClose, maxWidth, children }: {
|
|
4
|
+
title: string | ComponentChildren;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
maxWidth?: string;
|
|
7
|
+
children: ComponentChildren;
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
|
12
|
+
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
|
13
|
+
>
|
|
14
|
+
<div class={`bg-panel rounded-xl shadow-xl border border-border w-full ${maxWidth ?? "max-w-lg"} mx-4 max-h-[80vh] flex flex-col`}>
|
|
15
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-border-subtle">
|
|
16
|
+
<h3 class="text-sm font-bold text-ink">{title}</h3>
|
|
17
|
+
<button onClick={onClose} class="text-text-muted hover:text-text-data text-lg leading-none transition-colors">×</button>
|
|
18
|
+
</div>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: string; actions?: preact.ComponentChildren }) {
|
|
2
|
+
return (
|
|
3
|
+
<div class="mb-8 flex items-start justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-3xl font-bold text-ink">{title}</h1>
|
|
6
|
+
{subtitle && <div class="text-sm text-text-muted mt-1 font-medium">{subtitle}</div>}
|
|
7
|
+
</div>
|
|
8
|
+
{actions && <div class="flex gap-2 items-center">{actions}</div>}
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function PillButton({ onClick, active, children }: { onClick: () => void; active?: boolean; children: any }) {
|
|
2
|
+
return (
|
|
3
|
+
<button
|
|
4
|
+
onClick={onClick}
|
|
5
|
+
class={`rounded-md px-3 py-1.5 text-sm font-medium transition-all ${
|
|
6
|
+
active
|
|
7
|
+
? "bg-ink text-surface"
|
|
8
|
+
: "bg-panel border border-border text-text-secondary hover:bg-panel-hover"
|
|
9
|
+
}`}
|
|
10
|
+
>
|
|
11
|
+
{children}
|
|
12
|
+
</button>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function RefreshButton({ onClick }: { onClick: () => void }) {
|
|
2
|
+
return (
|
|
3
|
+
<button onClick={onClick} class="rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary hover:bg-panel-hover transition-all" title="Refresh">
|
|
4
|
+
Refresh
|
|
5
|
+
</button>
|
|
6
|
+
);
|
|
7
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function ServiceInfo({ description, links, stats, configGroups }: {
|
|
2
|
+
description: string;
|
|
3
|
+
links: { label: string; href: string }[];
|
|
4
|
+
stats?: { label: string; value: string | number }[];
|
|
5
|
+
configGroups?: { title: string; items: { name: string; value: string }[] }[] | null;
|
|
6
|
+
}) {
|
|
7
|
+
return (
|
|
8
|
+
<div class="w-80 flex-shrink-0 space-y-5">
|
|
9
|
+
{stats && stats.length > 0 && (
|
|
10
|
+
<div class="grid grid-cols-2 gap-2">
|
|
11
|
+
{stats.map(stat => (
|
|
12
|
+
<div key={stat.label} class="bg-panel border border-border rounded-lg px-3.5 py-3">
|
|
13
|
+
<div class="text-xs text-text-muted font-medium">{stat.label}</div>
|
|
14
|
+
<div class="text-xl font-semibold text-ink mt-0.5 tabular-nums">{stat.value}</div>
|
|
15
|
+
</div>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
{configGroups && configGroups.length > 0 && configGroups.map(group => (
|
|
20
|
+
<div key={group.title}>
|
|
21
|
+
<div class="text-xs font-medium text-text-muted uppercase tracking-wider mb-2">{group.title}</div>
|
|
22
|
+
<div class="bg-panel border border-border rounded-lg divide-y divide-border-subtle">
|
|
23
|
+
{group.items.map(item => (
|
|
24
|
+
<div key={item.name} class="px-3.5 py-2.5">
|
|
25
|
+
<div class="text-xs font-medium text-ink">{item.name}</div>
|
|
26
|
+
<div class="text-xs text-text-muted font-mono mt-0.5 truncate" title={item.value}>{item.value}</div>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
<div>
|
|
33
|
+
<p class="text-xs text-text-muted leading-relaxed mb-2.5">{description}</p>
|
|
34
|
+
<div class="space-y-1.5">
|
|
35
|
+
{links.map(link => (
|
|
36
|
+
<a key={link.href} href={link.href} target="_blank" rel="noopener"
|
|
37
|
+
class="flex items-center gap-1.5 text-xs text-text-muted hover:text-ink no-underline transition-colors">
|
|
38
|
+
<span>→</span> {link.label}
|
|
39
|
+
</a>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function StatusBadge({ status, colorMap }: { status: string; colorMap: Record<string, string> }) {
|
|
2
|
+
return (
|
|
3
|
+
<span class={`inline-flex px-2 py-0.5 rounded-md text-xs font-semibold ${colorMap[status] ?? "bg-panel-hover text-text-data"}`}>
|
|
4
|
+
{status}
|
|
5
|
+
</span>
|
|
6
|
+
);
|
|
7
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function TableLink({ href, children, mono }: { href: string; children: any; mono?: boolean }) {
|
|
2
|
+
return (
|
|
3
|
+
<a href={href} class={`text-ink font-medium hover:text-accent-olive transition-colors no-underline ${mono ? "font-mono text-xs" : ""}`}>{children}</a>
|
|
4
|
+
);
|
|
5
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function Table({ headers, rows }: { headers: string[]; rows: unknown[][] }) {
|
|
2
|
+
return (
|
|
3
|
+
<div class="bg-panel rounded-lg border border-border overflow-x-auto">
|
|
4
|
+
<table class="w-full text-sm">
|
|
5
|
+
<thead>
|
|
6
|
+
<tr class="border-b border-border-subtle">
|
|
7
|
+
{headers.map(h => (
|
|
8
|
+
<th key={h} class="text-left px-4 py-3 font-medium text-xs text-text-muted uppercase tracking-wider">{h}</th>
|
|
9
|
+
))}
|
|
10
|
+
</tr>
|
|
11
|
+
</thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
{rows.map((row, i) => (
|
|
14
|
+
<tr key={i} class="group border-b border-border-row last:border-0 hover:bg-panel-hover/50 transition-colors">
|
|
15
|
+
{row.map((cell, j) => (
|
|
16
|
+
<td key={j} class="px-4 py-3">
|
|
17
|
+
{cell as any}
|
|
18
|
+
</td>
|
|
19
|
+
))}
|
|
20
|
+
</tr>
|
|
21
|
+
))}
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|