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,193 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import { formatTime } from "../lib";
|
|
3
|
+
import { useQuery, useMutation } from "../rpc/hooks";
|
|
4
|
+
import { EmptyState, Breadcrumb, Table, PageHeader, PillButton, DeleteButton, TableLink, StatusBadge, ServiceInfo, RefreshButton } from "../components";
|
|
5
|
+
|
|
6
|
+
const QUEUE_STATUS_COLORS: Record<string, string> = {
|
|
7
|
+
pending: "bg-amber-100 text-amber-700",
|
|
8
|
+
acked: "bg-emerald-100 text-emerald-700",
|
|
9
|
+
failed: "bg-red-100 text-red-700",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function QueueView({ route }: { route: string }) {
|
|
13
|
+
const parts = route.split("/").filter(Boolean);
|
|
14
|
+
if (parts.length === 1) return <QueueList />;
|
|
15
|
+
if (parts.length >= 2) return <QueueMessages name={decodeURIComponent(parts[1]!)} />;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function QueueList() {
|
|
20
|
+
const { data: queues, refetch } = useQuery("queue.listQueues");
|
|
21
|
+
const { data: configGroups } = useQuery("config.forService", { type: "queue" });
|
|
22
|
+
|
|
23
|
+
const totalPending = queues?.reduce((s, q) => s + q.pending, 0) ?? 0;
|
|
24
|
+
const totalAcked = queues?.reduce((s, q) => s + q.acked, 0) ?? 0;
|
|
25
|
+
const totalFailed = queues?.reduce((s, q) => s + q.failed, 0) ?? 0;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div class="p-8 max-w-6xl">
|
|
29
|
+
<PageHeader title="Queues" subtitle={`${queues?.length ?? 0} queue(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
30
|
+
<div class="flex gap-6 items-start">
|
|
31
|
+
<div class="flex-1 min-w-0">
|
|
32
|
+
{!queues?.length ? (
|
|
33
|
+
<EmptyState message="No queues found" />
|
|
34
|
+
) : (
|
|
35
|
+
<Table
|
|
36
|
+
headers={["Queue", "Pending", "Acked", "Failed"]}
|
|
37
|
+
rows={queues.map(q => [
|
|
38
|
+
<TableLink href={`#/queue/${encodeURIComponent(q.queue)}`}>{q.queue}</TableLink>,
|
|
39
|
+
<span class="tabular-nums">{q.pending}</span>,
|
|
40
|
+
<span class="tabular-nums">{q.acked}</span>,
|
|
41
|
+
<span class="tabular-nums">{q.failed}</span>,
|
|
42
|
+
])}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<ServiceInfo
|
|
47
|
+
description="Message queues for asynchronous task processing."
|
|
48
|
+
stats={[
|
|
49
|
+
{ label: "Pending", value: totalPending.toLocaleString() },
|
|
50
|
+
{ label: "Processed", value: totalAcked.toLocaleString() },
|
|
51
|
+
{ label: "Failed", value: totalFailed.toLocaleString() },
|
|
52
|
+
{ label: "Queues", value: queues?.length ?? 0 },
|
|
53
|
+
]}
|
|
54
|
+
configGroups={configGroups}
|
|
55
|
+
links={[
|
|
56
|
+
{ label: "Documentation", href: "https://developers.cloudflare.com/queues/" },
|
|
57
|
+
{ label: "API Reference", href: "https://developers.cloudflare.com/api/resources/queues/" },
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function PublishForm({ queue, onPublished }: { queue: string; onPublished: () => void }) {
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
const [body, setBody] = useState("");
|
|
68
|
+
const [contentType, setContentType] = useState("json");
|
|
69
|
+
const [error, setError] = useState("");
|
|
70
|
+
const publish = useMutation("queue.publishMessage");
|
|
71
|
+
|
|
72
|
+
const handleSubmit = async () => {
|
|
73
|
+
setError("");
|
|
74
|
+
const result = await publish.mutate({ queue, body, contentType });
|
|
75
|
+
if (result) {
|
|
76
|
+
setBody("");
|
|
77
|
+
setOpen(false);
|
|
78
|
+
onPublished();
|
|
79
|
+
} else if (publish.error) {
|
|
80
|
+
setError(publish.error.message);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!open) {
|
|
85
|
+
return (
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => setOpen(true)}
|
|
88
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
|
|
89
|
+
>
|
|
90
|
+
Publish message
|
|
91
|
+
</button>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div class="bg-panel border border-border rounded-lg p-4 mb-6">
|
|
97
|
+
<div class="flex items-center justify-between mb-3">
|
|
98
|
+
<div class="text-sm font-semibold text-ink">Publish message</div>
|
|
99
|
+
<button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
|
|
100
|
+
Cancel
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="flex gap-2 mb-3">
|
|
104
|
+
{["json", "text"].map(ct => (
|
|
105
|
+
<PillButton key={ct} onClick={() => setContentType(ct)} active={contentType === ct}>
|
|
106
|
+
{ct.toUpperCase()}
|
|
107
|
+
</PillButton>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
<textarea
|
|
111
|
+
value={body}
|
|
112
|
+
onInput={e => setBody((e.target as HTMLTextAreaElement).value)}
|
|
113
|
+
placeholder={contentType === "json" ? '{"key": "value"}' : "Message body..."}
|
|
114
|
+
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 resize-y min-h-[80px]"
|
|
115
|
+
rows={3}
|
|
116
|
+
/>
|
|
117
|
+
{error && <div class="text-red-500 text-xs mt-1">{error}</div>}
|
|
118
|
+
<div class="flex justify-end mt-3">
|
|
119
|
+
<button
|
|
120
|
+
onClick={handleSubmit}
|
|
121
|
+
disabled={publish.isLoading || !body.trim()}
|
|
122
|
+
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"
|
|
123
|
+
>
|
|
124
|
+
{publish.isLoading ? "Publishing..." : "Publish"}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function RequeueButton({ onClick }: { onClick: () => void }) {
|
|
132
|
+
return (
|
|
133
|
+
<button onClick={onClick} class="text-amber-500 hover:text-amber-700 text-xs font-medium rounded-md px-2 py-1 hover:bg-amber-50 transition-all">
|
|
134
|
+
Requeue
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function QueueMessages({ name }: { name: string }) {
|
|
140
|
+
const [filter, setFilter] = useState("");
|
|
141
|
+
const { data: messages, refetch } = useQuery("queue.listMessages", { queue: name, status: filter || undefined });
|
|
142
|
+
const deleteMsg = useMutation("queue.deleteMessage");
|
|
143
|
+
const requeueMsg = useMutation("queue.requeueMessage");
|
|
144
|
+
|
|
145
|
+
const handleDelete = async (id: string) => {
|
|
146
|
+
if (!confirm("Delete this message?")) return;
|
|
147
|
+
await deleteMsg.mutate({ queue: name, id });
|
|
148
|
+
refetch();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleRequeue = async (id: string) => {
|
|
152
|
+
await requeueMsg.mutate({ queue: name, id });
|
|
153
|
+
refetch();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div class="p-8">
|
|
158
|
+
<Breadcrumb items={[{ label: "Queues", href: "#/queue" }, { label: name }]} />
|
|
159
|
+
<div class="mb-6 flex gap-2 items-center justify-between">
|
|
160
|
+
<div class="flex gap-2">
|
|
161
|
+
{["", "pending", "acked", "failed"].map(s => (
|
|
162
|
+
<PillButton key={s} onClick={() => setFilter(s)} active={filter === s}>
|
|
163
|
+
{s || "All"}
|
|
164
|
+
</PillButton>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
<div class="flex gap-2 items-center">
|
|
168
|
+
<RefreshButton onClick={refetch} />
|
|
169
|
+
<PublishForm queue={name} onPublished={refetch} />
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
{!messages?.length ? (
|
|
173
|
+
<EmptyState message="No messages found" />
|
|
174
|
+
) : (
|
|
175
|
+
<Table
|
|
176
|
+
headers={["ID", "Body", "Status", "Attempts", "Created", "Completed", ""]}
|
|
177
|
+
rows={messages.map(m => [
|
|
178
|
+
<span class="font-mono text-xs">{m.id.slice(0, 12)}...</span>,
|
|
179
|
+
<pre class="text-xs max-w-md truncate font-mono">{m.body}</pre>,
|
|
180
|
+
<StatusBadge status={m.status} colorMap={QUEUE_STATUS_COLORS} />,
|
|
181
|
+
m.attempts,
|
|
182
|
+
formatTime(m.created_at),
|
|
183
|
+
m.completed_at ? formatTime(m.completed_at) : "—",
|
|
184
|
+
<div class="flex gap-1">
|
|
185
|
+
{m.status !== "pending" && <RequeueButton onClick={() => handleRequeue(m.id)} />}
|
|
186
|
+
<DeleteButton onClick={() => handleDelete(m.id)} />
|
|
187
|
+
</div>,
|
|
188
|
+
])}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState, useRef } from "preact/hooks";
|
|
2
|
+
import { formatBytes } from "../lib";
|
|
3
|
+
import { useQuery, usePaginatedQuery, useMutation } from "../rpc/hooks";
|
|
4
|
+
import { EmptyState, Breadcrumb, Table, PageHeader, FilterInput, LoadMoreButton, DeleteButton, TableLink, ServiceInfo, RefreshButton } from "../components";
|
|
5
|
+
|
|
6
|
+
export function R2View({ route }: { route: string }) {
|
|
7
|
+
const parts = route.split("/").filter(Boolean);
|
|
8
|
+
if (parts.length === 1) return <R2BucketList />;
|
|
9
|
+
if (parts.length >= 2) return <R2ObjectList bucket={decodeURIComponent(parts[1]!)} />;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function R2BucketList() {
|
|
14
|
+
const { data: buckets, refetch } = useQuery("r2.listBuckets");
|
|
15
|
+
const { data: configGroups } = useQuery("config.forService", { type: "r2" });
|
|
16
|
+
|
|
17
|
+
const totalObjects = buckets?.reduce((s, b) => s + b.count, 0) ?? 0;
|
|
18
|
+
const totalSize = buckets?.reduce((s, b) => s + b.total_size, 0) ?? 0;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div class="p-8 max-w-6xl">
|
|
22
|
+
<PageHeader title="R2 Buckets" subtitle={`${buckets?.length ?? 0} bucket(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
23
|
+
<div class="flex gap-6 items-start">
|
|
24
|
+
<div class="flex-1 min-w-0">
|
|
25
|
+
{!buckets?.length ? (
|
|
26
|
+
<EmptyState message="No R2 buckets found" />
|
|
27
|
+
) : (
|
|
28
|
+
<Table
|
|
29
|
+
headers={["Bucket", "Objects", "Total Size"]}
|
|
30
|
+
rows={buckets.map(b => [
|
|
31
|
+
<TableLink href={`#/r2/${encodeURIComponent(b.bucket)}`}>{b.bucket}</TableLink>,
|
|
32
|
+
<span class="tabular-nums">{b.count}</span>,
|
|
33
|
+
formatBytes(b.total_size),
|
|
34
|
+
])}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
<ServiceInfo
|
|
39
|
+
description="Object storage with S3-compatible API. Zero egress fees."
|
|
40
|
+
stats={[
|
|
41
|
+
{ label: "Buckets", value: buckets?.length ?? 0 },
|
|
42
|
+
{ label: "Objects", value: totalObjects.toLocaleString() },
|
|
43
|
+
{ label: "Storage", value: formatBytes(totalSize) },
|
|
44
|
+
]}
|
|
45
|
+
configGroups={configGroups}
|
|
46
|
+
links={[
|
|
47
|
+
{ label: "Documentation", href: "https://developers.cloudflare.com/r2/" },
|
|
48
|
+
{ label: "API Reference", href: "https://developers.cloudflare.com/api/resources/r2/" },
|
|
49
|
+
]}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function UploadForm({ bucket, onUploaded }: { bucket: string; onUploaded: () => void }) {
|
|
57
|
+
const [open, setOpen] = useState(false);
|
|
58
|
+
const [key, setKey] = useState("");
|
|
59
|
+
const [file, setFile] = useState<File | null>(null);
|
|
60
|
+
const [error, setError] = useState("");
|
|
61
|
+
const [uploading, setUploading] = useState(false);
|
|
62
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
63
|
+
|
|
64
|
+
const handleFileChange = (e: Event) => {
|
|
65
|
+
const input = e.target as HTMLInputElement;
|
|
66
|
+
const selected = input.files?.[0] ?? null;
|
|
67
|
+
setFile(selected);
|
|
68
|
+
if (selected && !key) setKey(selected.name);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleSubmit = async () => {
|
|
72
|
+
if (!file || !key.trim()) return;
|
|
73
|
+
setError("");
|
|
74
|
+
setUploading(true);
|
|
75
|
+
try {
|
|
76
|
+
const formData = new FormData();
|
|
77
|
+
formData.append("bucket", bucket);
|
|
78
|
+
formData.append("key", key.trim());
|
|
79
|
+
formData.append("file", file);
|
|
80
|
+
const res = await fetch("/__dashboard/api/r2/upload", { method: "POST", body: formData });
|
|
81
|
+
const data = await res.json() as { error?: string };
|
|
82
|
+
if (!res.ok) throw new Error(data.error ?? "Upload failed");
|
|
83
|
+
setKey("");
|
|
84
|
+
setFile(null);
|
|
85
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
86
|
+
setOpen(false);
|
|
87
|
+
onUploaded();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
90
|
+
} finally {
|
|
91
|
+
setUploading(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!open) {
|
|
96
|
+
return (
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => setOpen(true)}
|
|
99
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
|
|
100
|
+
>
|
|
101
|
+
Upload object
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div class="bg-panel border border-border rounded-lg p-4 mb-6">
|
|
108
|
+
<div class="flex items-center justify-between mb-3">
|
|
109
|
+
<div class="text-sm font-semibold text-ink">Upload object</div>
|
|
110
|
+
<button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
|
|
111
|
+
Cancel
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="space-y-3">
|
|
115
|
+
<input
|
|
116
|
+
ref={fileInputRef}
|
|
117
|
+
type="file"
|
|
118
|
+
onChange={handleFileChange}
|
|
119
|
+
class="w-full text-sm text-text-secondary file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-panel-hover file:text-ink hover:file:bg-panel-active file:cursor-pointer file:transition-all"
|
|
120
|
+
/>
|
|
121
|
+
<input
|
|
122
|
+
type="text"
|
|
123
|
+
value={key}
|
|
124
|
+
onInput={e => setKey((e.target as HTMLInputElement).value)}
|
|
125
|
+
placeholder="Object key (defaults to filename)"
|
|
126
|
+
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"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
{error && <div class="text-red-500 text-xs mt-2">{error}</div>}
|
|
130
|
+
<div class="flex justify-end mt-3">
|
|
131
|
+
<button
|
|
132
|
+
onClick={handleSubmit}
|
|
133
|
+
disabled={uploading || !file || !key.trim()}
|
|
134
|
+
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"
|
|
135
|
+
>
|
|
136
|
+
{uploading ? "Uploading..." : "Upload"}
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function R2ObjectList({ bucket }: { bucket: string }) {
|
|
144
|
+
const [prefix, setPrefix] = useState("");
|
|
145
|
+
const { items: objects, hasMore, loadMore, refetch } = usePaginatedQuery("r2.listObjects", { bucket, prefix });
|
|
146
|
+
const deleteObject = useMutation("r2.deleteObject");
|
|
147
|
+
const renameObject = useMutation("r2.renameObject");
|
|
148
|
+
|
|
149
|
+
const handleDelete = async (key: string) => {
|
|
150
|
+
if (!confirm(`Delete object "${key}"?`)) return;
|
|
151
|
+
await deleteObject.mutate({ bucket, key });
|
|
152
|
+
refetch();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleRename = async (oldKey: string) => {
|
|
156
|
+
const newKey = prompt("New key:", oldKey);
|
|
157
|
+
if (!newKey || newKey === oldKey) return;
|
|
158
|
+
await renameObject.mutate({ bucket, oldKey, newKey });
|
|
159
|
+
refetch();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div class="p-8">
|
|
164
|
+
<Breadcrumb items={[{ label: "R2", href: "#/r2" }, { label: bucket }]} />
|
|
165
|
+
<div class="mb-6 flex gap-2 items-center justify-between">
|
|
166
|
+
<FilterInput value={prefix} onInput={setPrefix} placeholder="Filter by prefix..." />
|
|
167
|
+
<div class="flex gap-2 items-center">
|
|
168
|
+
<RefreshButton onClick={refetch} />
|
|
169
|
+
<UploadForm bucket={bucket} onUploaded={refetch} />
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
{objects.length === 0 ? (
|
|
173
|
+
<EmptyState message="No objects found" />
|
|
174
|
+
) : (
|
|
175
|
+
<>
|
|
176
|
+
<Table
|
|
177
|
+
headers={["Key", "Size", "ETag", "Uploaded", ""]}
|
|
178
|
+
rows={objects.map(o => [
|
|
179
|
+
<span class="font-mono text-xs font-medium">{o.key}</span>,
|
|
180
|
+
formatBytes(o.size),
|
|
181
|
+
<span class="font-mono text-xs text-text-muted">{o.etag.slice(0, 12)}</span>,
|
|
182
|
+
o.uploaded,
|
|
183
|
+
<div class="flex gap-1">
|
|
184
|
+
<a
|
|
185
|
+
href={`/__dashboard/api/r2/download?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(o.key)}`}
|
|
186
|
+
class="text-blue-500 hover:text-blue-700 text-xs font-medium rounded-md px-2 py-1 hover:bg-blue-50 transition-all"
|
|
187
|
+
>
|
|
188
|
+
Download
|
|
189
|
+
</a>
|
|
190
|
+
<button onClick={() => handleRename(o.key)} class="text-text-secondary hover:text-ink text-xs font-medium rounded-md px-2 py-1 hover:bg-panel-hover transition-all">
|
|
191
|
+
Rename
|
|
192
|
+
</button>
|
|
193
|
+
<DeleteButton onClick={() => handleDelete(o.key)} />
|
|
194
|
+
</div>,
|
|
195
|
+
])}
|
|
196
|
+
/>
|
|
197
|
+
{hasMore && <LoadMoreButton onClick={loadMore} />}
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import { useQuery, useMutation } from "../rpc/hooks";
|
|
3
|
+
import { PageHeader, Table, EmptyState, ServiceInfo, RefreshButton } from "../components";
|
|
4
|
+
|
|
5
|
+
export function ScheduledView({ route }: { route: string }) {
|
|
6
|
+
return <ScheduledList />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ScheduledList() {
|
|
10
|
+
const { data: triggers, refetch } = useQuery("scheduled.listTriggers");
|
|
11
|
+
const { data: configGroups } = useQuery("config.forService", { type: "scheduled" });
|
|
12
|
+
const runNow = useMutation("scheduled.trigger");
|
|
13
|
+
const [runningCron, setRunningCron] = useState<string | null>(null);
|
|
14
|
+
const [lastResult, setLastResult] = useState<{ cron: string; ok: boolean; error?: string } | null>(null);
|
|
15
|
+
|
|
16
|
+
const handleRun = async (cron: string, workerName: string | null) => {
|
|
17
|
+
setRunningCron(cron);
|
|
18
|
+
setLastResult(null);
|
|
19
|
+
const result = await runNow.mutate({ cron, workerName });
|
|
20
|
+
setRunningCron(null);
|
|
21
|
+
if (result) {
|
|
22
|
+
setLastResult({ cron, ok: true });
|
|
23
|
+
} else if (runNow.error) {
|
|
24
|
+
setLastResult({ cron, ok: false, error: runNow.error.message });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div class="p-8 max-w-6xl">
|
|
30
|
+
<PageHeader title="Scheduled" subtitle={`${triggers?.length ?? 0} cron trigger(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
31
|
+
|
|
32
|
+
{lastResult && (
|
|
33
|
+
<div class={`mb-6 px-4 py-3 rounded-lg text-sm font-medium ${
|
|
34
|
+
lastResult.ok
|
|
35
|
+
? "bg-emerald-50 text-emerald-700"
|
|
36
|
+
: "bg-red-50 text-red-600"
|
|
37
|
+
}`}>
|
|
38
|
+
{lastResult.ok
|
|
39
|
+
? `Triggered "${lastResult.cron}" successfully`
|
|
40
|
+
: `Failed to trigger "${lastResult.cron}": ${lastResult.error}`}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<div class="flex gap-6 items-start">
|
|
45
|
+
<div class="flex-1 min-w-0">
|
|
46
|
+
{!triggers?.length ? (
|
|
47
|
+
<EmptyState message="No scheduled triggers configured" />
|
|
48
|
+
) : (
|
|
49
|
+
<Table
|
|
50
|
+
headers={["Cron Expression", "Schedule", ...(triggers.some(t => t.workerName) ? ["Worker"] : []), ""]}
|
|
51
|
+
rows={triggers.map(t => {
|
|
52
|
+
const row = [
|
|
53
|
+
<span class="font-mono text-xs font-medium">{t.expression}</span>,
|
|
54
|
+
<span class="text-sm text-text-secondary">{t.description}</span>,
|
|
55
|
+
];
|
|
56
|
+
if (triggers.some(t => t.workerName)) {
|
|
57
|
+
row.push(
|
|
58
|
+
<span class="text-xs text-text-muted font-mono">{t.workerName ?? "main"}</span>,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
row.push(
|
|
62
|
+
<button
|
|
63
|
+
onClick={() => handleRun(t.expression, t.workerName)}
|
|
64
|
+
disabled={runningCron === t.expression}
|
|
65
|
+
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"
|
|
66
|
+
>
|
|
67
|
+
{runningCron === t.expression ? "Running..." : "Trigger"}
|
|
68
|
+
</button>,
|
|
69
|
+
);
|
|
70
|
+
return row;
|
|
71
|
+
})}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
<ServiceInfo
|
|
76
|
+
description="Cron-based scheduled triggers. Each expression defines when the worker's scheduled handler is invoked."
|
|
77
|
+
stats={[
|
|
78
|
+
{ label: "Triggers", value: triggers?.length ?? 0 },
|
|
79
|
+
]}
|
|
80
|
+
configGroups={configGroups}
|
|
81
|
+
links={[
|
|
82
|
+
{ label: "Cron Triggers", href: "https://developers.cloudflare.com/workers/configuration/cron-triggers/" },
|
|
83
|
+
{ label: "Crontab Guru", href: "https://crontab.guru/" },
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|