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,55 @@
|
|
|
1
|
+
import { useQuery } from "../rpc/hooks";
|
|
2
|
+
import { EmptyState, PageHeader, Table, TableLink, StatusBadge } from "../components";
|
|
3
|
+
|
|
4
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
5
|
+
kv: "bg-emerald-100 text-emerald-700",
|
|
6
|
+
r2: "bg-blue-100 text-blue-700",
|
|
7
|
+
d1: "bg-violet-100 text-violet-700",
|
|
8
|
+
do: "bg-amber-100 text-amber-700",
|
|
9
|
+
queue: "bg-rose-100 text-rose-700",
|
|
10
|
+
workflow: "bg-cyan-100 text-cyan-700",
|
|
11
|
+
service: "bg-panel-active text-text-data",
|
|
12
|
+
images: "bg-pink-100 text-pink-700",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function WorkersView() {
|
|
16
|
+
const { data: workers } = useQuery("workers.list");
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div class="p-8">
|
|
20
|
+
<PageHeader title="Workers" subtitle={`${workers?.length ?? 0} worker(s)`} />
|
|
21
|
+
{!workers?.length ? (
|
|
22
|
+
<EmptyState message="No workers configured" />
|
|
23
|
+
) : (
|
|
24
|
+
<div class="space-y-8">
|
|
25
|
+
{workers.map(w => (
|
|
26
|
+
<div key={w.name}>
|
|
27
|
+
<div class="flex items-center gap-3 mb-4">
|
|
28
|
+
<span class="w-7 h-7 rounded-md bg-panel-hover flex items-center justify-center text-sm">⊡</span>
|
|
29
|
+
<h2 class="text-lg font-bold text-ink">{w.name}</h2>
|
|
30
|
+
{w.isMain && (
|
|
31
|
+
<span class="px-2 py-0.5 rounded-md text-xs font-medium bg-gray-900 text-white">main</span>
|
|
32
|
+
)}
|
|
33
|
+
<span class="text-xs text-text-muted">{w.bindings.length} binding(s)</span>
|
|
34
|
+
</div>
|
|
35
|
+
{w.bindings.length === 0 ? (
|
|
36
|
+
<EmptyState message="No bindings configured" />
|
|
37
|
+
) : (
|
|
38
|
+
<Table
|
|
39
|
+
headers={["Type", "Binding", "Target"]}
|
|
40
|
+
rows={w.bindings.map(b => [
|
|
41
|
+
<StatusBadge status={b.type} colorMap={TYPE_COLORS} />,
|
|
42
|
+
<span class="font-mono text-xs font-medium">{b.name}</span>,
|
|
43
|
+
b.href
|
|
44
|
+
? <TableLink href={b.href}>{b.target}</TableLink>
|
|
45
|
+
: <span class="text-text-secondary">{b.target || "—"}</span>,
|
|
46
|
+
])}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import { formatTime } from "../lib";
|
|
3
|
+
import { useQuery, useMutation } from "../rpc/hooks";
|
|
4
|
+
import { EmptyState, Breadcrumb, Table, PageHeader, CodeBlock, TableLink, StatusBadge, ServiceInfo, RefreshButton } from "../components";
|
|
5
|
+
|
|
6
|
+
const WORKFLOW_STATUS_COLORS: Record<string, string> = {
|
|
7
|
+
running: "bg-accent-blue text-ink",
|
|
8
|
+
complete: "bg-emerald-100 text-emerald-700",
|
|
9
|
+
errored: "bg-red-100 text-red-700",
|
|
10
|
+
terminated: "bg-panel-active text-text-data",
|
|
11
|
+
waiting: "bg-blue-100 text-blue-700",
|
|
12
|
+
paused: "bg-amber-100 text-amber-700",
|
|
13
|
+
queued: "bg-purple-100 text-purple-700",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function WorkflowsView({ route }: { route: string }) {
|
|
17
|
+
const parts = route.split("/").filter(Boolean);
|
|
18
|
+
if (parts.length === 1) return <WorkflowList />;
|
|
19
|
+
if (parts.length === 2) return <WorkflowInstanceList name={decodeURIComponent(parts[1]!)} />;
|
|
20
|
+
if (parts.length >= 3) return <WorkflowInstanceDetail name={decodeURIComponent(parts[1]!)} id={decodeURIComponent(parts[2]!)} />;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function WorkflowList() {
|
|
25
|
+
const { data: workflows, refetch } = useQuery("workflows.list");
|
|
26
|
+
const { data: configGroups } = useQuery("config.forService", { type: "workflows" });
|
|
27
|
+
|
|
28
|
+
const totalInstances = workflows?.reduce((s, w) => s + w.total, 0) ?? 0;
|
|
29
|
+
const totalRunning = workflows?.reduce((s, w) => s + (w.byStatus.running ?? 0), 0) ?? 0;
|
|
30
|
+
const totalErrored = workflows?.reduce((s, w) => s + (w.byStatus.errored ?? 0), 0) ?? 0;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div class="p-8 max-w-6xl">
|
|
34
|
+
<PageHeader title="Workflows" subtitle={`${workflows?.length ?? 0} workflow(s)`} actions={<RefreshButton onClick={refetch} />} />
|
|
35
|
+
<div class="flex gap-6 items-start">
|
|
36
|
+
<div class="flex-1 min-w-0">
|
|
37
|
+
{!workflows?.length ? (
|
|
38
|
+
<EmptyState message="No workflow instances found" />
|
|
39
|
+
) : (
|
|
40
|
+
<Table
|
|
41
|
+
headers={["Workflow", "Total", "Running", "Complete", "Errored"]}
|
|
42
|
+
rows={workflows.map(w => [
|
|
43
|
+
<TableLink href={`#/workflows/${encodeURIComponent(w.name)}`}>{w.name}</TableLink>,
|
|
44
|
+
<span class="tabular-nums">{w.total}</span>,
|
|
45
|
+
w.byStatus.running ?? 0,
|
|
46
|
+
w.byStatus.complete ?? 0,
|
|
47
|
+
w.byStatus.errored ?? 0,
|
|
48
|
+
])}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
<ServiceInfo
|
|
53
|
+
description="Durable execution engine for multi-step tasks."
|
|
54
|
+
stats={[
|
|
55
|
+
{ label: "Instances", value: totalInstances.toLocaleString() },
|
|
56
|
+
{ label: "Running", value: totalRunning.toLocaleString() },
|
|
57
|
+
{ label: "Errored", value: totalErrored.toLocaleString() },
|
|
58
|
+
]}
|
|
59
|
+
configGroups={configGroups}
|
|
60
|
+
links={[
|
|
61
|
+
{ label: "Documentation", href: "https://developers.cloudflare.com/workflows/" },
|
|
62
|
+
{ label: "API Reference", href: "https://developers.cloudflare.com/api/resources/workflows/" },
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function CreateWorkflowForm({ name, onCreated }: { name: string; onCreated: (id: string) => void }) {
|
|
71
|
+
const [open, setOpen] = useState(false);
|
|
72
|
+
const [params, setParams] = useState("{}");
|
|
73
|
+
const [error, setError] = useState("");
|
|
74
|
+
const create = useMutation("workflows.create");
|
|
75
|
+
|
|
76
|
+
const handleSubmit = async () => {
|
|
77
|
+
setError("");
|
|
78
|
+
const result = await create.mutate({ name, params });
|
|
79
|
+
if (result) {
|
|
80
|
+
setParams("{}");
|
|
81
|
+
setOpen(false);
|
|
82
|
+
onCreated(result.id);
|
|
83
|
+
} else if (create.error) {
|
|
84
|
+
setError(create.error.message);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!open) {
|
|
89
|
+
return (
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => setOpen(true)}
|
|
92
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-ink text-surface hover:opacity-80 transition-all"
|
|
93
|
+
>
|
|
94
|
+
Create instance
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div class="bg-panel border border-border rounded-lg p-4 mb-6">
|
|
101
|
+
<div class="flex items-center justify-between mb-3">
|
|
102
|
+
<div class="text-sm font-semibold text-ink">Create workflow instance</div>
|
|
103
|
+
<button onClick={() => { setOpen(false); setError(""); }} class="text-text-muted hover:text-text-data text-xs font-medium">
|
|
104
|
+
Cancel
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<textarea
|
|
108
|
+
value={params}
|
|
109
|
+
onInput={e => setParams((e.target as HTMLTextAreaElement).value)}
|
|
110
|
+
placeholder='{"key": "value"}'
|
|
111
|
+
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]"
|
|
112
|
+
rows={3}
|
|
113
|
+
/>
|
|
114
|
+
{error && <div class="text-red-500 text-xs mt-1">{error}</div>}
|
|
115
|
+
<div class="flex justify-end mt-3">
|
|
116
|
+
<button
|
|
117
|
+
onClick={handleSubmit}
|
|
118
|
+
disabled={create.isLoading || !params.trim()}
|
|
119
|
+
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"
|
|
120
|
+
>
|
|
121
|
+
{create.isLoading ? "Creating..." : "Create"}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ActionButton({ onClick, label, color = "blue", disabled }: { onClick: () => void; label: string; color?: "blue" | "red" | "amber" | "emerald"; disabled?: boolean }) {
|
|
129
|
+
const colors = {
|
|
130
|
+
blue: "text-blue-500 hover:text-blue-700 hover:bg-blue-50",
|
|
131
|
+
red: "text-red-400 hover:text-red-600 hover:bg-red-50",
|
|
132
|
+
amber: "text-amber-500 hover:text-amber-700 hover:bg-amber-50",
|
|
133
|
+
emerald: "text-emerald-500 hover:text-emerald-700 hover:bg-emerald-50",
|
|
134
|
+
};
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
onClick={onClick}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
class={`text-xs font-medium rounded-md px-2 py-1 transition-all disabled:opacity-50 disabled:cursor-not-allowed ${colors[color]}`}
|
|
140
|
+
>
|
|
141
|
+
{label}
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function InstanceActions({ name, id, status, refetch, onDuplicated }: { name: string; id: string; status: string; refetch: () => void; onDuplicated?: (id: string) => void }) {
|
|
147
|
+
const pause = useMutation("workflows.pause");
|
|
148
|
+
const resume = useMutation("workflows.resume");
|
|
149
|
+
const terminate = useMutation("workflows.terminate");
|
|
150
|
+
const restart = useMutation("workflows.restart");
|
|
151
|
+
const duplicate = useMutation("workflows.duplicate");
|
|
152
|
+
|
|
153
|
+
const handlePause = async () => { await pause.mutate({ name, id }); refetch(); };
|
|
154
|
+
const handleResume = async () => { await resume.mutate({ name, id }); refetch(); };
|
|
155
|
+
const handleTerminate = async () => {
|
|
156
|
+
if (!confirm("Terminate this workflow instance?")) return;
|
|
157
|
+
await terminate.mutate({ name, id }); refetch();
|
|
158
|
+
};
|
|
159
|
+
const handleRestart = async () => {
|
|
160
|
+
if (!confirm("Restart this workflow instance? All steps will re-execute.")) return;
|
|
161
|
+
await restart.mutate({ name, id }); refetch();
|
|
162
|
+
};
|
|
163
|
+
const handleDuplicate = async () => {
|
|
164
|
+
const result = await duplicate.mutate({ name, id });
|
|
165
|
+
if (result && onDuplicated) onDuplicated(result.id);
|
|
166
|
+
else refetch();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const isTerminal = ["complete", "errored", "terminated"].includes(status);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div class="flex gap-1">
|
|
173
|
+
{(status === "running" || status === "waiting") && (
|
|
174
|
+
<>
|
|
175
|
+
<ActionButton onClick={handlePause} label="Pause" color="amber" />
|
|
176
|
+
<ActionButton onClick={handleTerminate} label="Terminate" color="red" />
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
{status === "paused" && (
|
|
180
|
+
<>
|
|
181
|
+
<ActionButton onClick={handleResume} label="Resume" color="emerald" />
|
|
182
|
+
<ActionButton onClick={handleTerminate} label="Terminate" color="red" />
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
{status === "queued" && (
|
|
186
|
+
<ActionButton onClick={handleTerminate} label="Terminate" color="red" />
|
|
187
|
+
)}
|
|
188
|
+
{isTerminal && (
|
|
189
|
+
<>
|
|
190
|
+
<ActionButton onClick={handleRestart} label="Restart" color="blue" />
|
|
191
|
+
<ActionButton onClick={handleDuplicate} label="Duplicate" color="blue" />
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function WorkflowInstanceList({ name }: { name: string }) {
|
|
199
|
+
const [statusFilter, setStatusFilter] = useState("");
|
|
200
|
+
const { data: instances, refetch } = useQuery("workflows.listInstances", { name, status: statusFilter || undefined });
|
|
201
|
+
|
|
202
|
+
const handleCreated = (_id: string) => { refetch(); };
|
|
203
|
+
const handleDuplicated = (id: string) => { location.hash = `#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(id)}`; };
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div class="p-8">
|
|
207
|
+
<Breadcrumb items={[{ label: "Workflows", href: "#/workflows" }, { label: name }]} />
|
|
208
|
+
<div class="mb-6 flex gap-2 items-center justify-between">
|
|
209
|
+
<select
|
|
210
|
+
value={statusFilter}
|
|
211
|
+
onChange={e => setStatusFilter((e.target as HTMLSelectElement).value)}
|
|
212
|
+
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 appearance-none pr-10"
|
|
213
|
+
>
|
|
214
|
+
<option value="">All statuses</option>
|
|
215
|
+
<option value="running">Running</option>
|
|
216
|
+
<option value="waiting">Waiting</option>
|
|
217
|
+
<option value="paused">Paused</option>
|
|
218
|
+
<option value="queued">Queued</option>
|
|
219
|
+
<option value="complete">Complete</option>
|
|
220
|
+
<option value="errored">Errored</option>
|
|
221
|
+
<option value="terminated">Terminated</option>
|
|
222
|
+
</select>
|
|
223
|
+
<div class="flex gap-2 items-center">
|
|
224
|
+
<RefreshButton onClick={refetch} />
|
|
225
|
+
<CreateWorkflowForm name={name} onCreated={handleCreated} />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
{!instances?.length ? (
|
|
229
|
+
<EmptyState message="No instances found" />
|
|
230
|
+
) : (
|
|
231
|
+
<Table
|
|
232
|
+
headers={["Instance ID", "Status", "Created", "Updated", ""]}
|
|
233
|
+
rows={instances.map(inst => [
|
|
234
|
+
<TableLink href={`#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(inst.id)}`} mono>{inst.id.slice(0, 16)}...</TableLink>,
|
|
235
|
+
<StatusBadge status={inst.status} colorMap={WORKFLOW_STATUS_COLORS} />,
|
|
236
|
+
formatTime(inst.created_at),
|
|
237
|
+
formatTime(inst.updated_at),
|
|
238
|
+
<InstanceActions name={name} id={inst.id} status={inst.status} refetch={refetch} onDuplicated={handleDuplicated} />,
|
|
239
|
+
])}
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function SkipSleepBanner({ name, id, activeSleep, refetch }: { name: string; id: string; activeSleep: { stepName: string; until: number }; refetch: () => void }) {
|
|
247
|
+
const skipSleep = useMutation("workflows.skipSleep");
|
|
248
|
+
const remaining = Math.max(0, activeSleep.until - Date.now());
|
|
249
|
+
const label = activeSleep.stepName.replace(/^(sleep|sleepUntil):/, "");
|
|
250
|
+
|
|
251
|
+
const formatRemaining = (ms: number) => {
|
|
252
|
+
if (ms < 1000) return "< 1s";
|
|
253
|
+
const s = Math.floor(ms / 1000);
|
|
254
|
+
if (s < 60) return `${s}s`;
|
|
255
|
+
const m = Math.floor(s / 60);
|
|
256
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
257
|
+
const h = Math.floor(m / 60);
|
|
258
|
+
return `${h}h ${m % 60}m`;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const handleSkip = async () => {
|
|
262
|
+
await skipSleep.mutate({ name, id });
|
|
263
|
+
refetch();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div class="mb-6 bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-center justify-between">
|
|
268
|
+
<div>
|
|
269
|
+
<span class="text-sm font-semibold text-amber-800">Sleeping</span>
|
|
270
|
+
<span class="text-sm text-amber-700 ml-2">
|
|
271
|
+
step "{label}" — {formatRemaining(remaining)} remaining
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
<button
|
|
275
|
+
onClick={handleSkip}
|
|
276
|
+
disabled={skipSleep.isLoading}
|
|
277
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium bg-amber-600 text-white hover:bg-amber-700 transition-all disabled:opacity-50"
|
|
278
|
+
>
|
|
279
|
+
{skipSleep.isLoading ? "Skipping..." : "Skip sleep"}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function SendEventForm({ name, id, waitingForEvents, refetch }: { name: string; id: string; waitingForEvents: string[]; refetch: () => void }) {
|
|
286
|
+
const [eventType, setEventType] = useState(waitingForEvents[0] ?? "");
|
|
287
|
+
const [payload, setPayload] = useState("{}");
|
|
288
|
+
const [error, setError] = useState("");
|
|
289
|
+
const sendEvent = useMutation("workflows.sendEvent");
|
|
290
|
+
|
|
291
|
+
const handleSend = async () => {
|
|
292
|
+
setError("");
|
|
293
|
+
try {
|
|
294
|
+
JSON.parse(payload); // validate JSON
|
|
295
|
+
} catch {
|
|
296
|
+
setError("Invalid JSON payload");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const result = await sendEvent.mutate({ name, id, type: eventType, payload });
|
|
300
|
+
if (result) {
|
|
301
|
+
setPayload("{}");
|
|
302
|
+
refetch();
|
|
303
|
+
} else if (sendEvent.error) {
|
|
304
|
+
setError(sendEvent.error.message);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
310
|
+
<div class="text-sm font-semibold text-blue-800 mb-3">
|
|
311
|
+
Waiting for event{waitingForEvents.length > 0 && (
|
|
312
|
+
<span class="font-normal text-blue-600">
|
|
313
|
+
{" "}— type: {waitingForEvents.map(t => `"${t}"`).join(", ")}
|
|
314
|
+
</span>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
<div class="flex gap-3 items-start">
|
|
318
|
+
<div class="flex-1">
|
|
319
|
+
<input
|
|
320
|
+
type="text"
|
|
321
|
+
value={eventType}
|
|
322
|
+
onInput={e => setEventType((e.target as HTMLInputElement).value)}
|
|
323
|
+
placeholder="Event type"
|
|
324
|
+
class="w-full bg-white border border-blue-200 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-300 transition-all mb-2"
|
|
325
|
+
/>
|
|
326
|
+
<textarea
|
|
327
|
+
value={payload}
|
|
328
|
+
onInput={e => setPayload((e.target as HTMLTextAreaElement).value)}
|
|
329
|
+
placeholder='{"key": "value"}'
|
|
330
|
+
class="w-full bg-white border border-blue-200 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-300 transition-all resize-y min-h-[60px]"
|
|
331
|
+
rows={2}
|
|
332
|
+
/>
|
|
333
|
+
{error && <div class="text-red-500 text-xs mt-1">{error}</div>}
|
|
334
|
+
</div>
|
|
335
|
+
<button
|
|
336
|
+
onClick={handleSend}
|
|
337
|
+
disabled={sendEvent.isLoading || !eventType.trim()}
|
|
338
|
+
class="rounded-md px-4 py-2 text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
|
339
|
+
>
|
|
340
|
+
{sendEvent.isLoading ? "Sending..." : "Send event"}
|
|
341
|
+
</button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function WorkflowInstanceDetail({ name, id }: { name: string; id: string }) {
|
|
348
|
+
const { data, refetch } = useQuery("workflows.getInstance", { name, id });
|
|
349
|
+
const restartFromStep = useMutation("workflows.restart");
|
|
350
|
+
|
|
351
|
+
const handleDuplicated = (newId: string) => { location.hash = `#/workflows/${encodeURIComponent(name)}/${encodeURIComponent(newId)}`; };
|
|
352
|
+
|
|
353
|
+
const handleRestartFromStep = async (stepName: string) => {
|
|
354
|
+
if (!confirm(`Restart from step "${stepName}"? This step and all subsequent steps will re-execute.`)) return;
|
|
355
|
+
await restartFromStep.mutate({ name, id, fromStep: stepName });
|
|
356
|
+
refetch();
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (!data) return <div class="p-8 text-text-muted font-medium">Loading...</div>;
|
|
360
|
+
|
|
361
|
+
const isTerminal = ["complete", "errored", "terminated"].includes(data.status);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div class="p-8">
|
|
365
|
+
<Breadcrumb items={[
|
|
366
|
+
{ label: "Workflows", href: "#/workflows" },
|
|
367
|
+
{ label: name, href: `#/workflows/${encodeURIComponent(name)}` },
|
|
368
|
+
{ label: id.slice(0, 16) + "..." },
|
|
369
|
+
]} />
|
|
370
|
+
|
|
371
|
+
<div class="flex items-center gap-4 mb-8">
|
|
372
|
+
<StatusBadge status={data.status} colorMap={WORKFLOW_STATUS_COLORS} />
|
|
373
|
+
<span class="text-sm text-text-muted font-medium">Created: {formatTime(data.created_at)}</span>
|
|
374
|
+
<InstanceActions name={name} id={id} status={data.status} refetch={refetch} onDuplicated={handleDuplicated} />
|
|
375
|
+
<RefreshButton onClick={refetch} />
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{data.activeSleep && (
|
|
379
|
+
<SkipSleepBanner name={name} id={id} activeSleep={data.activeSleep} refetch={refetch} />
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{data.status === "waiting" && (
|
|
383
|
+
<SendEventForm name={name} id={id} waitingForEvents={data.waitingForEvents} refetch={refetch} />
|
|
384
|
+
)}
|
|
385
|
+
|
|
386
|
+
{data.params && (
|
|
387
|
+
<div class="mb-6 bg-panel rounded-lg border border-border p-5">
|
|
388
|
+
<h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Parameters</h3>
|
|
389
|
+
<CodeBlock>{data.params}</CodeBlock>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{data.output && (
|
|
394
|
+
<div class="mb-6 bg-panel rounded-lg border border-border p-5">
|
|
395
|
+
<h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Output</h3>
|
|
396
|
+
<CodeBlock>{data.output}</CodeBlock>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{data.error && (
|
|
401
|
+
<div class="mb-6 bg-panel rounded-lg border border-border p-5">
|
|
402
|
+
<h3 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-3">Error</h3>
|
|
403
|
+
<pre class="bg-red-50 rounded-lg p-4 text-xs text-red-600 overflow-x-auto font-mono">{data.error}</pre>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
<div class="mb-6">
|
|
408
|
+
<h3 class="text-sm font-semibold text-ink mb-4">Steps ({data.steps.length})</h3>
|
|
409
|
+
{data.steps.length === 0 && data.stepAttempts.length === 0 ? (
|
|
410
|
+
<div class="text-text-muted text-sm font-medium">No steps completed yet</div>
|
|
411
|
+
) : (
|
|
412
|
+
<Table
|
|
413
|
+
headers={["Step", "Output", "Completed", ...(isTerminal ? [""] : [])]}
|
|
414
|
+
rows={[
|
|
415
|
+
...data.steps.map(s => {
|
|
416
|
+
const row = [
|
|
417
|
+
<span class="font-mono text-xs font-medium">{s.step_name}</span>,
|
|
418
|
+
s.output ? <pre class="text-xs max-w-md truncate font-mono">{s.output}</pre> : "\u2014",
|
|
419
|
+
formatTime(s.completed_at),
|
|
420
|
+
];
|
|
421
|
+
if (isTerminal) {
|
|
422
|
+
row.push(
|
|
423
|
+
<ActionButton onClick={() => handleRestartFromStep(s.step_name)} label="Restart from here" color="blue" />
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return row;
|
|
427
|
+
}),
|
|
428
|
+
...data.stepAttempts.map(a => {
|
|
429
|
+
const errorContent = a.last_error ? (
|
|
430
|
+
a.last_error_id ? (
|
|
431
|
+
<a href={`#/errors/${encodeURIComponent(a.last_error_id)}`} class="text-xs max-w-md truncate font-mono text-red-600 dark:text-red-400 hover:underline block" title={a.last_error}>
|
|
432
|
+
{a.last_error_name ? `${a.last_error_name}: ` : ""}{a.last_error}
|
|
433
|
+
</a>
|
|
434
|
+
) : (
|
|
435
|
+
<pre class="text-xs max-w-md truncate font-mono text-red-600 dark:text-red-400" title={a.last_error}>
|
|
436
|
+
{a.last_error_name ? `${a.last_error_name}: ` : ""}{a.last_error}
|
|
437
|
+
</pre>
|
|
438
|
+
)
|
|
439
|
+
) : "\u2014";
|
|
440
|
+
const row = [
|
|
441
|
+
<span class="font-mono text-xs font-medium">
|
|
442
|
+
{a.step_name}
|
|
443
|
+
<span class="ml-2 text-amber-600 dark:text-amber-400 text-[10px] font-semibold uppercase">retrying ({a.failed_attempts}x failed)</span>
|
|
444
|
+
</span>,
|
|
445
|
+
errorContent,
|
|
446
|
+
a.updated_at ? formatTime(a.updated_at) : "\u2014",
|
|
447
|
+
];
|
|
448
|
+
if (isTerminal) {
|
|
449
|
+
row.push("");
|
|
450
|
+
}
|
|
451
|
+
return row;
|
|
452
|
+
}),
|
|
453
|
+
]}
|
|
454
|
+
/>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{data.events.length > 0 && (
|
|
459
|
+
<div>
|
|
460
|
+
<h3 class="text-sm font-semibold text-ink mb-4">Events ({data.events.length})</h3>
|
|
461
|
+
<Table
|
|
462
|
+
headers={["Type", "Payload", "Time"]}
|
|
463
|
+
rows={data.events.map(e => [
|
|
464
|
+
<span class="font-mono text-xs font-medium">{e.event_type}</span>,
|
|
465
|
+
e.payload ? <pre class="text-xs max-w-md truncate font-mono">{e.payload}</pre> : "\u2014",
|
|
466
|
+
formatTime(e.created_at),
|
|
467
|
+
])}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|