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,168 @@
|
|
|
1
|
+
import { formatTime, parseHashRoute } from "../lib";
|
|
2
|
+
import { useQuery, useMutation } from "../rpc/hooks";
|
|
3
|
+
import { rpc } from "../rpc/client";
|
|
4
|
+
import { EmptyState, Breadcrumb, Table, PageHeader, DeleteButton, TableLink, ServiceInfo, SqlBrowser, RefreshButton } from "../components";
|
|
5
|
+
import type { Tab } from "../sql-browser/index";
|
|
6
|
+
|
|
7
|
+
export function DoView({ route }: { route: string }) {
|
|
8
|
+
const { segments, query } = parseHashRoute(route);
|
|
9
|
+
if (segments.length <= 1) return <DoNamespaceList />;
|
|
10
|
+
if (segments.length === 2) return <DoInstanceList ns={decodeURIComponent(segments[1]!)} />;
|
|
11
|
+
if (segments.length >= 3) {
|
|
12
|
+
const ns = decodeURIComponent(segments[1]!);
|
|
13
|
+
const id = decodeURIComponent(segments[2]!);
|
|
14
|
+
const rawTab = segments[3] as Tab | undefined;
|
|
15
|
+
const tab: Tab = rawTab === "schema" || rawTab === "sql" ? rawTab : "data";
|
|
16
|
+
const tableName = segments[4] ? decodeURIComponent(segments[4]) : null;
|
|
17
|
+
const basePath = `/do/${encodeURIComponent(ns)}/${encodeURIComponent(id)}`;
|
|
18
|
+
return <DoInstanceDetail ns={ns} id={id} basePath={basePath} routeTab={tab} routeTable={tableName} routeQuery={query} />;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function DoNamespaceList() {
|
|
24
|
+
const { data: namespaces, refetch } = useQuery("do.listNamespaces");
|
|
25
|
+
const { data: configGroups } = useQuery("config.forService", { type: "do" });
|
|
26
|
+
|
|
27
|
+
const totalInstances = namespaces?.reduce((s, ns) => s + ns.count, 0) ?? 0;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div class="p-8 max-w-6xl">
|
|
31
|
+
<PageHeader title="Durable Objects" subtitle={`${namespaces?.length ?? 0} namespace(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
32
|
+
<div class="flex gap-6 items-start">
|
|
33
|
+
<div class="flex-1 min-w-0">
|
|
34
|
+
{!namespaces?.length ? (
|
|
35
|
+
<EmptyState message="No Durable Object namespaces found" />
|
|
36
|
+
) : (
|
|
37
|
+
<Table
|
|
38
|
+
headers={["Namespace", "Instances"]}
|
|
39
|
+
rows={namespaces.map(ns => [
|
|
40
|
+
<TableLink href={`#/do/${encodeURIComponent(ns.namespace)}`}>{ns.namespace}</TableLink>,
|
|
41
|
+
<span class="tabular-nums">{ns.count}</span>,
|
|
42
|
+
])}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<ServiceInfo
|
|
47
|
+
description="Durable Objects provide strongly consistent coordination."
|
|
48
|
+
stats={[
|
|
49
|
+
{ label: "Namespaces", value: namespaces?.length ?? 0 },
|
|
50
|
+
{ label: "Instances", value: totalInstances.toLocaleString() },
|
|
51
|
+
]}
|
|
52
|
+
configGroups={configGroups}
|
|
53
|
+
links={[
|
|
54
|
+
{ label: "Documentation", href: "https://developers.cloudflare.com/durable-objects/" },
|
|
55
|
+
{ label: "API Reference", href: "https://developers.cloudflare.com/api/resources/durable_objects/" },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function DoInstanceList({ ns }: { ns: string }) {
|
|
64
|
+
const { data: instances, refetch } = useQuery("do.listInstances", { ns });
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div class="p-8">
|
|
68
|
+
<Breadcrumb items={[{ label: "Durable Objects", href: "#/do" }, { label: ns }]} />
|
|
69
|
+
<div class="mb-6 flex justify-end">
|
|
70
|
+
<RefreshButton onClick={refetch} />
|
|
71
|
+
</div>
|
|
72
|
+
{!instances?.length ? (
|
|
73
|
+
<EmptyState message="No instances found" />
|
|
74
|
+
) : (
|
|
75
|
+
<Table
|
|
76
|
+
headers={["Instance ID", "Name", "Storage Keys", "Alarm"]}
|
|
77
|
+
rows={instances.map(inst => [
|
|
78
|
+
<TableLink href={`#/do/${encodeURIComponent(ns)}/${encodeURIComponent(inst.id)}`} mono>{inst.id}</TableLink>,
|
|
79
|
+
inst.name ? <span class="text-sm">{inst.name}</span> : <span class="text-text-muted">—</span>,
|
|
80
|
+
<span class="tabular-nums">{inst.key_count}</span>,
|
|
81
|
+
inst.alarm ? formatTime(inst.alarm) : "—",
|
|
82
|
+
])}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function DoInstanceDetail({ ns, id, basePath, routeTab, routeTable, routeQuery }: {
|
|
90
|
+
ns: string;
|
|
91
|
+
id: string;
|
|
92
|
+
basePath: string;
|
|
93
|
+
routeTab: Tab;
|
|
94
|
+
routeTable: string | null;
|
|
95
|
+
routeQuery: URLSearchParams;
|
|
96
|
+
}) {
|
|
97
|
+
const { data, refetch } = useQuery("do.getInstance", { ns, id });
|
|
98
|
+
const deleteEntry = useMutation("do.deleteEntry");
|
|
99
|
+
const triggerAlarm = useMutation("do.triggerAlarm");
|
|
100
|
+
const { data: sqlTables } = useQuery("do.listSqlTables", { ns, id });
|
|
101
|
+
|
|
102
|
+
const handleDelete = async (key: string) => {
|
|
103
|
+
if (!confirm(`Delete storage key "${key}"?`)) return;
|
|
104
|
+
await deleteEntry.mutate({ ns, id, key });
|
|
105
|
+
refetch();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleTriggerAlarm = async () => {
|
|
109
|
+
await triggerAlarm.mutate({ ns, id });
|
|
110
|
+
refetch();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (!data) return <div class="p-8 text-text-muted font-medium">Loading...</div>;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div class="p-8">
|
|
117
|
+
<Breadcrumb items={[
|
|
118
|
+
{ label: "Durable Objects", href: "#/do" },
|
|
119
|
+
{ label: ns, href: `#/do/${encodeURIComponent(ns)}` },
|
|
120
|
+
{ label: id.slice(0, 16) + "..." },
|
|
121
|
+
]} />
|
|
122
|
+
<div class="mb-6 flex justify-end">
|
|
123
|
+
<RefreshButton onClick={refetch} />
|
|
124
|
+
</div>
|
|
125
|
+
{(data.alarm || data.hasAlarmHandler) && (
|
|
126
|
+
<div class="mb-6 px-4 py-3 bg-panel-secondary border border-border rounded-lg text-sm font-medium text-ink flex items-center justify-between">
|
|
127
|
+
<span>{data.alarm ? `Alarm set for: ${formatTime(data.alarm)}` : "No alarm scheduled"}</span>
|
|
128
|
+
{data.hasAlarmHandler && (
|
|
129
|
+
<button
|
|
130
|
+
onClick={handleTriggerAlarm}
|
|
131
|
+
disabled={triggerAlarm.isLoading}
|
|
132
|
+
class="rounded-md px-3 py-1.5 text-xs font-medium bg-ink text-surface hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed transition-all"
|
|
133
|
+
>
|
|
134
|
+
{triggerAlarm.isLoading ? "Triggering..." : "Trigger now"}
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
{data.entries.length === 0 ? (
|
|
140
|
+
<EmptyState message="No storage entries" />
|
|
141
|
+
) : (
|
|
142
|
+
<Table
|
|
143
|
+
headers={["Key", "Value", ""]}
|
|
144
|
+
rows={data.entries.map(e => [
|
|
145
|
+
<span class="font-mono text-xs font-medium">{e.key}</span>,
|
|
146
|
+
<pre class="text-xs max-w-lg truncate font-mono">{e.value}</pre>,
|
|
147
|
+
<DeleteButton onClick={() => handleDelete(e.key)} />,
|
|
148
|
+
])}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{/* SQL Storage */}
|
|
153
|
+
{sqlTables && sqlTables.length > 0 && (
|
|
154
|
+
<div class="mt-8">
|
|
155
|
+
<SqlBrowser
|
|
156
|
+
tables={sqlTables}
|
|
157
|
+
execQuery={(sql) => rpc("do.sqlQuery", { ns, id, sql })}
|
|
158
|
+
historyScope={`do:${ns}:${id}`}
|
|
159
|
+
basePath={basePath}
|
|
160
|
+
routeTab={routeTab}
|
|
161
|
+
routeTable={routeTable}
|
|
162
|
+
routeQuery={routeQuery}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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 EMAIL_STATUS_COLORS: Record<string, string> = {
|
|
7
|
+
sent: "bg-blue-100 text-blue-700",
|
|
8
|
+
received: "bg-emerald-100 text-emerald-700",
|
|
9
|
+
forwarded: "bg-purple-100 text-purple-700",
|
|
10
|
+
rejected: "bg-red-100 text-red-700",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function formatBytes(bytes: number): string {
|
|
14
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function EmailView({ route }: { route: string }) {
|
|
20
|
+
const parts = route.split("/").filter(Boolean);
|
|
21
|
+
if (parts.length >= 2) return <EmailDetail id={parts[1]!} />;
|
|
22
|
+
return <EmailList />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function SendEmailForm({ open, onToggle, onSent }: { open: boolean; onToggle: () => void; onSent: () => void }) {
|
|
26
|
+
const [from, setFrom] = useState("sender@example.com");
|
|
27
|
+
const [to, setTo] = useState("recipient@example.com");
|
|
28
|
+
const [subject, setSubject] = useState("");
|
|
29
|
+
const [body, setBody] = useState("");
|
|
30
|
+
const [error, setError] = useState("");
|
|
31
|
+
const trigger = useMutation("email.trigger");
|
|
32
|
+
|
|
33
|
+
const handleSubmit = async () => {
|
|
34
|
+
setError("");
|
|
35
|
+
const result = await trigger.mutate({ from, to, subject, body });
|
|
36
|
+
if (result) {
|
|
37
|
+
setSubject("");
|
|
38
|
+
setBody("");
|
|
39
|
+
onToggle();
|
|
40
|
+
onSent();
|
|
41
|
+
} else if (trigger.error) {
|
|
42
|
+
setError(trigger.error.message);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!open) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div class="bg-panel border border-border rounded-lg p-4 mb-4">
|
|
50
|
+
<div class="flex items-center justify-between mb-3">
|
|
51
|
+
<div class="text-sm font-semibold text-ink">Send test email</div>
|
|
52
|
+
<button onClick={() => { onToggle(); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
|
|
53
|
+
Cancel
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="grid grid-cols-2 gap-3 mb-3">
|
|
57
|
+
<div>
|
|
58
|
+
<label class="block text-xs text-text-muted mb-1">From</label>
|
|
59
|
+
<input
|
|
60
|
+
value={from}
|
|
61
|
+
onInput={e => setFrom((e.target as HTMLInputElement).value)}
|
|
62
|
+
class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<label class="block text-xs text-text-muted mb-1">To</label>
|
|
67
|
+
<input
|
|
68
|
+
value={to}
|
|
69
|
+
onInput={e => setTo((e.target as HTMLInputElement).value)}
|
|
70
|
+
class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="mb-3">
|
|
75
|
+
<label class="block text-xs text-text-muted mb-1">Subject</label>
|
|
76
|
+
<input
|
|
77
|
+
value={subject}
|
|
78
|
+
onInput={e => setSubject((e.target as HTMLInputElement).value)}
|
|
79
|
+
placeholder="Test email"
|
|
80
|
+
class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-border focus:ring-1 focus:ring-border transition-all"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="mb-3">
|
|
84
|
+
<label class="block text-xs text-text-muted mb-1">Body</label>
|
|
85
|
+
<textarea
|
|
86
|
+
value={body}
|
|
87
|
+
onInput={e => setBody((e.target as HTMLTextAreaElement).value)}
|
|
88
|
+
placeholder="Email body..."
|
|
89
|
+
class="w-full bg-panel-secondary border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-border focus:ring-1 focus:ring-border transition-all resize-y min-h-[80px]"
|
|
90
|
+
rows={3}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
{error && <div class="text-red-500 text-xs mt-1">{error}</div>}
|
|
94
|
+
<div class="flex justify-end mt-3">
|
|
95
|
+
<button
|
|
96
|
+
onClick={handleSubmit}
|
|
97
|
+
disabled={trigger.isLoading || !from.trim() || !to.trim()}
|
|
98
|
+
class="rounded-md px-4 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
99
|
+
>
|
|
100
|
+
{trigger.isLoading ? "Sending..." : "Send"}
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function EmailList() {
|
|
108
|
+
const [filter, setFilter] = useState("");
|
|
109
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
110
|
+
const { data: emails, refetch } = useQuery("email.list", { status: filter || undefined });
|
|
111
|
+
const { data: stats } = useQuery("email.stats");
|
|
112
|
+
const { data: configGroups } = useQuery("config.forService", { type: "email" });
|
|
113
|
+
const deleteEmail = useMutation("email.delete");
|
|
114
|
+
|
|
115
|
+
const handleDelete = async (id: string) => {
|
|
116
|
+
if (!confirm("Delete this email?")) return;
|
|
117
|
+
await deleteEmail.mutate({ id });
|
|
118
|
+
refetch();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div class="p-8 max-w-6xl">
|
|
123
|
+
<PageHeader title="Email" subtitle={`${stats?.total ?? 0} email(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
124
|
+
<div class="flex gap-6 items-start">
|
|
125
|
+
<div class="flex-1 min-w-0">
|
|
126
|
+
<SendEmailForm open={formOpen} onToggle={() => setFormOpen(!formOpen)} onSent={refetch} />
|
|
127
|
+
<div class="mb-6 flex gap-2 items-center justify-between">
|
|
128
|
+
<div class="flex gap-2">
|
|
129
|
+
{["", "received", "sent", "forwarded", "rejected"].map(s => (
|
|
130
|
+
<PillButton key={s} onClick={() => setFilter(s)} active={filter === s}>
|
|
131
|
+
{s || "All"}
|
|
132
|
+
</PillButton>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
{!formOpen && (
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => setFormOpen(true)}
|
|
138
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
|
|
139
|
+
>
|
|
140
|
+
Send test email
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
{!emails?.length ? (
|
|
145
|
+
<EmptyState message="No emails found" />
|
|
146
|
+
) : (
|
|
147
|
+
<Table
|
|
148
|
+
headers={["From", "To", "Status", "Size", "Binding", "Time", ""]}
|
|
149
|
+
rows={emails.map(e => [
|
|
150
|
+
<a href={`#/email/${e.id}`} class="font-mono text-xs text-blue-600 hover:underline">{e.from_addr}</a>,
|
|
151
|
+
<span class="font-mono text-xs">{e.to_addr}</span>,
|
|
152
|
+
<StatusBadge status={e.status} colorMap={EMAIL_STATUS_COLORS} />,
|
|
153
|
+
<span class="text-xs text-text-muted tabular-nums">{formatBytes(e.raw_size)}</span>,
|
|
154
|
+
<span class="text-xs text-text-muted font-mono">{e.binding === "_incoming" ? "incoming" : e.binding === "_forward" ? "forward" : e.binding === "_reply" ? "reply" : e.binding}</span>,
|
|
155
|
+
<span class="text-xs text-text-muted">{formatTime(e.created_at)}</span>,
|
|
156
|
+
<DeleteButton onClick={() => handleDelete(e.id)} />,
|
|
157
|
+
])}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
<ServiceInfo
|
|
162
|
+
description="Email handler for processing incoming emails and send_email bindings for sending."
|
|
163
|
+
stats={[
|
|
164
|
+
{ label: "Total", value: stats?.total ?? 0 },
|
|
165
|
+
...(stats?.byStatus ? Object.entries(stats.byStatus).map(([k, v]) => ({ label: k, value: v })) : []),
|
|
166
|
+
]}
|
|
167
|
+
configGroups={configGroups}
|
|
168
|
+
links={[
|
|
169
|
+
{ label: "Email Workers", href: "https://developers.cloudflare.com/email-routing/email-workers/" },
|
|
170
|
+
{ label: "Send Email", href: "https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/" },
|
|
171
|
+
]}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function EmailDetail({ id }: { id: string }) {
|
|
179
|
+
const { data } = useQuery("email.get", { id });
|
|
180
|
+
|
|
181
|
+
if (!data) {
|
|
182
|
+
return (
|
|
183
|
+
<div class="p-8">
|
|
184
|
+
<a href="#/email" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to emails</a>
|
|
185
|
+
<EmptyState message="Email not found" />
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { record, raw } = data;
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div class="p-8 max-w-4xl">
|
|
194
|
+
<a href="#/email" class="text-sm text-blue-600 hover:underline mb-4 inline-block">Back to emails</a>
|
|
195
|
+
<div class="bg-panel border border-border rounded-lg p-6">
|
|
196
|
+
<div class="flex items-center justify-between mb-4">
|
|
197
|
+
<h2 class="text-lg font-semibold text-ink">Email Detail</h2>
|
|
198
|
+
<StatusBadge status={record.status} colorMap={EMAIL_STATUS_COLORS} />
|
|
199
|
+
</div>
|
|
200
|
+
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
|
|
201
|
+
<div>
|
|
202
|
+
<span class="text-text-muted">From:</span>{" "}
|
|
203
|
+
<span class="font-mono">{record.from_addr}</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div>
|
|
206
|
+
<span class="text-text-muted">To:</span>{" "}
|
|
207
|
+
<span class="font-mono">{record.to_addr}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div>
|
|
210
|
+
<span class="text-text-muted">Binding:</span>{" "}
|
|
211
|
+
<span class="font-mono">{record.binding}</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<span class="text-text-muted">Size:</span>{" "}
|
|
215
|
+
<span class="tabular-nums">{formatBytes(record.raw_size)}</span>
|
|
216
|
+
</div>
|
|
217
|
+
<div>
|
|
218
|
+
<span class="text-text-muted">Time:</span>{" "}
|
|
219
|
+
{formatTime(record.created_at)}
|
|
220
|
+
</div>
|
|
221
|
+
{record.reject_reason && (
|
|
222
|
+
<div>
|
|
223
|
+
<span class="text-text-muted">Reject reason:</span>{" "}
|
|
224
|
+
<span class="text-red-600">{record.reject_reason}</span>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
<div>
|
|
229
|
+
<div class="text-xs text-text-muted mb-2">Raw content</div>
|
|
230
|
+
<pre class="bg-panel-secondary border border-border rounded-lg p-4 text-xs font-mono overflow-x-auto whitespace-pre-wrap max-h-96 overflow-y-auto">{raw}</pre>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|