@donkeylabs/cli 0.5.0 → 0.6.3
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/package.json +2 -2
- package/src/client/base.ts +6 -4
- package/templates/sveltekit-app/bun.lock +170 -34
- package/templates/sveltekit-app/package.json +3 -3
- package/templates/sveltekit-app/src/lib/api.ts +109 -40
- package/templates/sveltekit-app/src/routes/+page.svelte +10 -3
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +23 -0
- package/templates/sveltekit-app/src/routes/workflows/+page.svelte +522 -0
- package/templates/sveltekit-app/src/server/index.ts +3 -1
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +197 -0
- package/templates/sveltekit-app/src/server/routes/demo.ts +82 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from "$app/environment";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import { Button } from "$lib/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "$lib/components/ui/card";
|
|
12
|
+
import { Badge } from "$lib/components/ui/badge";
|
|
13
|
+
import { createApi } from "$lib/api";
|
|
14
|
+
|
|
15
|
+
// Workflow steps for visual display
|
|
16
|
+
const WORKFLOW_STEPS = [
|
|
17
|
+
{ id: "validate", label: "Validate Order", icon: "1" },
|
|
18
|
+
{ id: "payment", label: "Process Payment", icon: "2" },
|
|
19
|
+
{ id: "fulfill", label: "Fulfill Order", icon: "3", parallel: true },
|
|
20
|
+
{ id: "complete", label: "Complete", icon: "4" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Parallel sub-steps
|
|
24
|
+
const PARALLEL_STEPS = {
|
|
25
|
+
fulfill: [
|
|
26
|
+
{ id: "send-email", label: "Send Email" },
|
|
27
|
+
{ id: "prepare-shipment", label: "Prepare Shipment" },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let { data } = $props();
|
|
32
|
+
|
|
33
|
+
const client = createApi();
|
|
34
|
+
|
|
35
|
+
// Active workflow state
|
|
36
|
+
let activeWorkflow = $state<any>(null);
|
|
37
|
+
let workflowProgress = $state(0);
|
|
38
|
+
let currentStep = $state<string | null>(null);
|
|
39
|
+
let stepStatuses = $state<Record<string, string>>({});
|
|
40
|
+
let stepResults = $state<Record<string, any>>({});
|
|
41
|
+
let isStarting = $state(false);
|
|
42
|
+
|
|
43
|
+
// History state
|
|
44
|
+
let instances = $state(data.instances || []);
|
|
45
|
+
|
|
46
|
+
// Event log for debugging
|
|
47
|
+
let eventLog = $state<Array<{ time: string; event: string; data: any }>>([]);
|
|
48
|
+
|
|
49
|
+
function getStepStatus(stepId: string): "pending" | "running" | "completed" | "failed" {
|
|
50
|
+
return (stepStatuses[stepId] as any) || "pending";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getStatusColor(status: string): "default" | "secondary" | "destructive" | "outline" | "success" {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case "completed":
|
|
56
|
+
return "success";
|
|
57
|
+
case "running":
|
|
58
|
+
return "default";
|
|
59
|
+
case "failed":
|
|
60
|
+
return "destructive";
|
|
61
|
+
case "cancelled":
|
|
62
|
+
return "secondary";
|
|
63
|
+
default:
|
|
64
|
+
return "outline";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getStepBgClass(status: string): string {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case "completed":
|
|
71
|
+
return "bg-green-500 text-white";
|
|
72
|
+
case "running":
|
|
73
|
+
return "bg-blue-500 text-white animate-pulse";
|
|
74
|
+
case "failed":
|
|
75
|
+
return "bg-red-500 text-white";
|
|
76
|
+
default:
|
|
77
|
+
return "bg-muted text-muted-foreground";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getStepBorderClass(status: string): string {
|
|
82
|
+
switch (status) {
|
|
83
|
+
case "completed":
|
|
84
|
+
return "border-green-500";
|
|
85
|
+
case "running":
|
|
86
|
+
return "border-blue-500";
|
|
87
|
+
case "failed":
|
|
88
|
+
return "border-red-500";
|
|
89
|
+
default:
|
|
90
|
+
return "border-muted";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function startWorkflow() {
|
|
95
|
+
isStarting = true;
|
|
96
|
+
stepStatuses = {};
|
|
97
|
+
stepResults = {};
|
|
98
|
+
workflowProgress = 0;
|
|
99
|
+
currentStep = null;
|
|
100
|
+
eventLog = [];
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await client.api.workflow.start({});
|
|
104
|
+
activeWorkflow = { id: result.instanceId, status: "pending" };
|
|
105
|
+
|
|
106
|
+
// Subscribe to this specific workflow's SSE channel
|
|
107
|
+
const unsubscribe = client.sse.subscribe(
|
|
108
|
+
[`workflow:${result.instanceId}`],
|
|
109
|
+
handleWorkflowEvent
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Store unsubscribe for cleanup
|
|
113
|
+
(activeWorkflow as any)._unsubscribe = unsubscribe;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error("Failed to start workflow:", e);
|
|
116
|
+
} finally {
|
|
117
|
+
isStarting = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleWorkflowEvent(eventType: string, eventData: any) {
|
|
122
|
+
// Log the event
|
|
123
|
+
eventLog = [
|
|
124
|
+
{ time: new Date().toLocaleTimeString(), event: eventType, data: eventData },
|
|
125
|
+
...eventLog,
|
|
126
|
+
].slice(0, 20);
|
|
127
|
+
|
|
128
|
+
switch (eventType) {
|
|
129
|
+
case "workflow.started":
|
|
130
|
+
activeWorkflow = { ...activeWorkflow, status: "running" };
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case "workflow.progress":
|
|
134
|
+
workflowProgress = eventData.progress || 0;
|
|
135
|
+
currentStep = eventData.currentStep;
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case "workflow.step.started":
|
|
139
|
+
stepStatuses = { ...stepStatuses, [eventData.stepName]: "running" };
|
|
140
|
+
currentStep = eventData.stepName;
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case "workflow.step.completed":
|
|
144
|
+
stepStatuses = { ...stepStatuses, [eventData.stepName]: "completed" };
|
|
145
|
+
if (eventData.output) {
|
|
146
|
+
stepResults = { ...stepResults, [eventData.stepName]: eventData.output };
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "workflow.step.failed":
|
|
151
|
+
stepStatuses = { ...stepStatuses, [eventData.stepName]: "failed" };
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case "workflow.completed":
|
|
155
|
+
activeWorkflow = { ...activeWorkflow, status: "completed", output: eventData.output };
|
|
156
|
+
workflowProgress = 100;
|
|
157
|
+
refreshInstances();
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "workflow.failed":
|
|
161
|
+
activeWorkflow = { ...activeWorkflow, status: "failed", error: eventData.error };
|
|
162
|
+
refreshInstances();
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case "workflow.cancelled":
|
|
166
|
+
activeWorkflow = { ...activeWorkflow, status: "cancelled" };
|
|
167
|
+
refreshInstances();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function cancelWorkflow() {
|
|
173
|
+
if (!activeWorkflow) return;
|
|
174
|
+
try {
|
|
175
|
+
await client.api.workflow.cancel({ instanceId: activeWorkflow.id });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error("Failed to cancel workflow:", e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function refreshInstances() {
|
|
182
|
+
try {
|
|
183
|
+
const result = await client.api.workflow.list({});
|
|
184
|
+
instances = result.instances || [];
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error("Failed to refresh instances:", e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function viewWorkflowDetails(instanceId: string) {
|
|
191
|
+
try {
|
|
192
|
+
const status = await client.api.workflow.status({ instanceId });
|
|
193
|
+
if (status) {
|
|
194
|
+
activeWorkflow = status;
|
|
195
|
+
stepStatuses = {};
|
|
196
|
+
stepResults = {};
|
|
197
|
+
workflowProgress = status.status === "completed" ? 100 : 0;
|
|
198
|
+
|
|
199
|
+
// Rebuild step statuses from stepResults
|
|
200
|
+
if (status.stepResults) {
|
|
201
|
+
for (const [stepName, result] of Object.entries(status.stepResults as Record<string, any>)) {
|
|
202
|
+
stepStatuses[stepName] = result.status || "completed";
|
|
203
|
+
if (result.output) {
|
|
204
|
+
stepResults[stepName] = result.output;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error("Failed to get workflow status:", e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function clearWorkflow() {
|
|
215
|
+
if (activeWorkflow?._unsubscribe) {
|
|
216
|
+
activeWorkflow._unsubscribe();
|
|
217
|
+
}
|
|
218
|
+
activeWorkflow = null;
|
|
219
|
+
stepStatuses = {};
|
|
220
|
+
stepResults = {};
|
|
221
|
+
workflowProgress = 0;
|
|
222
|
+
currentStep = null;
|
|
223
|
+
eventLog = [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
onMount(() => {
|
|
227
|
+
if (!browser) return;
|
|
228
|
+
|
|
229
|
+
// Subscribe to workflow-updates channel for list updates
|
|
230
|
+
const unsubscribe = client.sse.subscribe(
|
|
231
|
+
["workflow-updates"],
|
|
232
|
+
(eventType, eventData) => {
|
|
233
|
+
if (["workflow.completed", "workflow.failed", "workflow.cancelled"].includes(eventType)) {
|
|
234
|
+
refreshInstances();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return () => {
|
|
240
|
+
unsubscribe();
|
|
241
|
+
if (activeWorkflow?._unsubscribe) {
|
|
242
|
+
activeWorkflow._unsubscribe();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
</script>
|
|
247
|
+
|
|
248
|
+
<div class="min-h-screen bg-background">
|
|
249
|
+
<div class="container mx-auto max-w-6xl py-8 px-4">
|
|
250
|
+
<!-- Header -->
|
|
251
|
+
<div class="text-center mb-8">
|
|
252
|
+
<h1 class="text-3xl font-bold tracking-tight">Workflow Demo</h1>
|
|
253
|
+
<p class="text-muted-foreground mt-2">
|
|
254
|
+
Step Function Orchestration with Real-time Progress
|
|
255
|
+
</p>
|
|
256
|
+
<div class="flex gap-2 justify-center mt-4">
|
|
257
|
+
<a href="/">
|
|
258
|
+
<Button variant="outline" size="sm">Back to Main Demo</Button>
|
|
259
|
+
</a>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
264
|
+
<!-- Workflow Runner -->
|
|
265
|
+
<div class="lg:col-span-2 space-y-6">
|
|
266
|
+
<!-- Control Panel -->
|
|
267
|
+
<Card>
|
|
268
|
+
<CardHeader>
|
|
269
|
+
<CardTitle>Order Processing Workflow</CardTitle>
|
|
270
|
+
<CardDescription>
|
|
271
|
+
Multi-step workflow with validation, payment, and parallel fulfillment
|
|
272
|
+
</CardDescription>
|
|
273
|
+
</CardHeader>
|
|
274
|
+
<CardContent>
|
|
275
|
+
<div class="flex gap-3 items-center">
|
|
276
|
+
<Button
|
|
277
|
+
onclick={startWorkflow}
|
|
278
|
+
disabled={isStarting || (activeWorkflow && activeWorkflow.status === "running")}
|
|
279
|
+
>
|
|
280
|
+
{isStarting ? "Starting..." : "Start New Workflow"}
|
|
281
|
+
</Button>
|
|
282
|
+
{#if activeWorkflow && activeWorkflow.status === "running"}
|
|
283
|
+
<Button variant="destructive" onclick={cancelWorkflow}>
|
|
284
|
+
Cancel
|
|
285
|
+
</Button>
|
|
286
|
+
{/if}
|
|
287
|
+
{#if activeWorkflow && activeWorkflow.status !== "running"}
|
|
288
|
+
<Button variant="outline" onclick={clearWorkflow}>
|
|
289
|
+
Clear
|
|
290
|
+
</Button>
|
|
291
|
+
{/if}
|
|
292
|
+
{#if activeWorkflow}
|
|
293
|
+
<Badge variant={getStatusColor(activeWorkflow.status)}>
|
|
294
|
+
{activeWorkflow.status.toUpperCase()}
|
|
295
|
+
</Badge>
|
|
296
|
+
{/if}
|
|
297
|
+
</div>
|
|
298
|
+
</CardContent>
|
|
299
|
+
</Card>
|
|
300
|
+
|
|
301
|
+
<!-- Progress Visualization -->
|
|
302
|
+
{#if activeWorkflow}
|
|
303
|
+
<Card>
|
|
304
|
+
<CardHeader>
|
|
305
|
+
<CardTitle class="text-lg">Progress</CardTitle>
|
|
306
|
+
<CardDescription>
|
|
307
|
+
Instance: <code class="bg-muted px-1 rounded text-xs">{activeWorkflow.id}</code>
|
|
308
|
+
</CardDescription>
|
|
309
|
+
</CardHeader>
|
|
310
|
+
<CardContent>
|
|
311
|
+
<!-- Progress Bar -->
|
|
312
|
+
<div class="mb-6">
|
|
313
|
+
<div class="flex justify-between text-sm mb-1">
|
|
314
|
+
<span>Progress</span>
|
|
315
|
+
<span>{Math.round(workflowProgress)}%</span>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="h-2 bg-muted rounded-full overflow-hidden">
|
|
318
|
+
<div
|
|
319
|
+
class="h-full bg-primary transition-all duration-500 ease-out"
|
|
320
|
+
style="width: {workflowProgress}%"
|
|
321
|
+
></div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<!-- Step Visualization -->
|
|
326
|
+
<div class="space-y-4">
|
|
327
|
+
{#each WORKFLOW_STEPS as step, i}
|
|
328
|
+
{@const status = getStepStatus(step.id)}
|
|
329
|
+
<div class="flex items-start gap-4">
|
|
330
|
+
<!-- Step Number/Icon -->
|
|
331
|
+
<div
|
|
332
|
+
class="w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-300 {getStepBgClass(status)}"
|
|
333
|
+
>
|
|
334
|
+
{#if status === "completed"}
|
|
335
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
336
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
337
|
+
</svg>
|
|
338
|
+
{:else if status === "running"}
|
|
339
|
+
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
340
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
341
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
342
|
+
</svg>
|
|
343
|
+
{:else if status === "failed"}
|
|
344
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
345
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
346
|
+
</svg>
|
|
347
|
+
{:else}
|
|
348
|
+
{step.icon}
|
|
349
|
+
{/if}
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<!-- Step Content -->
|
|
353
|
+
<div class="flex-1">
|
|
354
|
+
<div class="flex items-center gap-2">
|
|
355
|
+
<span class="font-medium">{step.label}</span>
|
|
356
|
+
{#if step.parallel}
|
|
357
|
+
<Badge variant="outline" class="text-xs">Parallel</Badge>
|
|
358
|
+
{/if}
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Parallel Sub-steps -->
|
|
362
|
+
{#if step.parallel && PARALLEL_STEPS[step.id as keyof typeof PARALLEL_STEPS]}
|
|
363
|
+
<div class="mt-2 ml-4 space-y-2">
|
|
364
|
+
{#each PARALLEL_STEPS[step.id as keyof typeof PARALLEL_STEPS] as subStep}
|
|
365
|
+
{@const subStatus = getStepStatus(subStep.id)}
|
|
366
|
+
<div class="flex items-center gap-2 text-sm">
|
|
367
|
+
<div
|
|
368
|
+
class="w-6 h-6 rounded-full flex items-center justify-center text-xs transition-all duration-300 {getStepBgClass(subStatus)}"
|
|
369
|
+
>
|
|
370
|
+
{#if subStatus === "completed"}
|
|
371
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
372
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
373
|
+
</svg>
|
|
374
|
+
{:else if subStatus === "running"}
|
|
375
|
+
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
376
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
377
|
+
</svg>
|
|
378
|
+
{:else}
|
|
379
|
+
-
|
|
380
|
+
{/if}
|
|
381
|
+
</div>
|
|
382
|
+
<span class={subStatus === "completed" ? "text-green-600" : subStatus === "running" ? "text-blue-600" : "text-muted-foreground"}>
|
|
383
|
+
{subStep.label}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
{/each}
|
|
387
|
+
</div>
|
|
388
|
+
{/if}
|
|
389
|
+
|
|
390
|
+
<!-- Step Result -->
|
|
391
|
+
{#if stepResults[step.id]}
|
|
392
|
+
<pre class="mt-2 text-xs bg-muted p-2 rounded overflow-auto max-h-20">{JSON.stringify(stepResults[step.id], null, 2)}</pre>
|
|
393
|
+
{/if}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<!-- Connector Line -->
|
|
398
|
+
{#if i < WORKFLOW_STEPS.length - 1}
|
|
399
|
+
<div class="ml-5 w-0.5 h-4 bg-muted"></div>
|
|
400
|
+
{/if}
|
|
401
|
+
{/each}
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<!-- Final Output -->
|
|
405
|
+
{#if activeWorkflow.status === "completed" && activeWorkflow.output}
|
|
406
|
+
<div class="mt-6 p-4 bg-green-50 dark:bg-green-950 rounded-lg border border-green-200 dark:border-green-800">
|
|
407
|
+
<h4 class="font-medium text-green-800 dark:text-green-200 mb-2">Workflow Complete</h4>
|
|
408
|
+
<pre class="text-xs overflow-auto">{JSON.stringify(activeWorkflow.output, null, 2)}</pre>
|
|
409
|
+
</div>
|
|
410
|
+
{/if}
|
|
411
|
+
|
|
412
|
+
{#if activeWorkflow.status === "failed" && activeWorkflow.error}
|
|
413
|
+
<div class="mt-6 p-4 bg-red-50 dark:bg-red-950 rounded-lg border border-red-200 dark:border-red-800">
|
|
414
|
+
<h4 class="font-medium text-red-800 dark:text-red-200 mb-2">Workflow Failed</h4>
|
|
415
|
+
<p class="text-sm text-red-600 dark:text-red-400">{activeWorkflow.error}</p>
|
|
416
|
+
</div>
|
|
417
|
+
{/if}
|
|
418
|
+
</CardContent>
|
|
419
|
+
</Card>
|
|
420
|
+
{/if}
|
|
421
|
+
|
|
422
|
+
<!-- Event Log -->
|
|
423
|
+
{#if eventLog.length > 0}
|
|
424
|
+
<Card>
|
|
425
|
+
<CardHeader>
|
|
426
|
+
<CardTitle class="text-lg">SSE Event Log</CardTitle>
|
|
427
|
+
<CardDescription>Real-time events from the server</CardDescription>
|
|
428
|
+
</CardHeader>
|
|
429
|
+
<CardContent>
|
|
430
|
+
<ul class="space-y-1 max-h-48 overflow-y-auto font-mono text-xs">
|
|
431
|
+
{#each eventLog as log}
|
|
432
|
+
<li class="flex gap-2 p-1 hover:bg-muted rounded">
|
|
433
|
+
<span class="text-muted-foreground">{log.time}</span>
|
|
434
|
+
<Badge variant="outline" class="text-xs">{log.event.replace("workflow.", "")}</Badge>
|
|
435
|
+
{#if log.data.stepName}
|
|
436
|
+
<span class="text-primary">{log.data.stepName}</span>
|
|
437
|
+
{/if}
|
|
438
|
+
{#if log.data.progress !== undefined}
|
|
439
|
+
<span class="text-green-600">{log.data.progress}%</span>
|
|
440
|
+
{/if}
|
|
441
|
+
</li>
|
|
442
|
+
{/each}
|
|
443
|
+
</ul>
|
|
444
|
+
</CardContent>
|
|
445
|
+
</Card>
|
|
446
|
+
{/if}
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<!-- Sidebar - History -->
|
|
450
|
+
<div class="space-y-6">
|
|
451
|
+
<Card>
|
|
452
|
+
<CardHeader>
|
|
453
|
+
<CardTitle class="text-lg">Workflow History</CardTitle>
|
|
454
|
+
<CardDescription>Recent workflow instances</CardDescription>
|
|
455
|
+
</CardHeader>
|
|
456
|
+
<CardContent>
|
|
457
|
+
<div class="flex gap-2 mb-4">
|
|
458
|
+
<Button size="sm" variant="outline" onclick={refreshInstances}>
|
|
459
|
+
Refresh
|
|
460
|
+
</Button>
|
|
461
|
+
</div>
|
|
462
|
+
{#if instances.length === 0}
|
|
463
|
+
<p class="text-sm text-muted-foreground italic">No workflows yet</p>
|
|
464
|
+
{:else}
|
|
465
|
+
<ul class="space-y-2 max-h-96 overflow-y-auto">
|
|
466
|
+
{#each instances as instance}
|
|
467
|
+
<li>
|
|
468
|
+
<button
|
|
469
|
+
class="w-full text-left p-3 rounded-lg border bg-card hover:bg-muted/50 transition-colors"
|
|
470
|
+
onclick={() => viewWorkflowDetails(instance.id)}
|
|
471
|
+
>
|
|
472
|
+
<div class="flex items-center justify-between">
|
|
473
|
+
<code class="text-xs truncate max-w-[120px]">{instance.id}</code>
|
|
474
|
+
<Badge variant={getStatusColor(instance.status)} class="text-xs">
|
|
475
|
+
{instance.status}
|
|
476
|
+
</Badge>
|
|
477
|
+
</div>
|
|
478
|
+
{#if instance.currentStep}
|
|
479
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
480
|
+
Step: {instance.currentStep}
|
|
481
|
+
</p>
|
|
482
|
+
{/if}
|
|
483
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
484
|
+
{new Date(instance.createdAt).toLocaleString()}
|
|
485
|
+
</p>
|
|
486
|
+
</button>
|
|
487
|
+
</li>
|
|
488
|
+
{/each}
|
|
489
|
+
</ul>
|
|
490
|
+
{/if}
|
|
491
|
+
</CardContent>
|
|
492
|
+
</Card>
|
|
493
|
+
|
|
494
|
+
<!-- Workflow Info -->
|
|
495
|
+
<Card>
|
|
496
|
+
<CardHeader>
|
|
497
|
+
<CardTitle class="text-lg">About This Demo</CardTitle>
|
|
498
|
+
</CardHeader>
|
|
499
|
+
<CardContent class="text-sm space-y-2">
|
|
500
|
+
<p>
|
|
501
|
+
This demo shows a <strong>step function workflow</strong> for order processing:
|
|
502
|
+
</p>
|
|
503
|
+
<ol class="list-decimal list-inside space-y-1 text-muted-foreground">
|
|
504
|
+
<li>Validate order (1s)</li>
|
|
505
|
+
<li>Process payment (2s)</li>
|
|
506
|
+
<li>Parallel fulfillment:
|
|
507
|
+
<ul class="list-disc list-inside ml-4">
|
|
508
|
+
<li>Send confirmation email</li>
|
|
509
|
+
<li>Prepare shipment</li>
|
|
510
|
+
</ul>
|
|
511
|
+
</li>
|
|
512
|
+
<li>Complete with summary</li>
|
|
513
|
+
</ol>
|
|
514
|
+
<p class="text-muted-foreground">
|
|
515
|
+
Progress is streamed via <strong>Server-Sent Events (SSE)</strong> for real-time updates.
|
|
516
|
+
</p>
|
|
517
|
+
</CardContent>
|
|
518
|
+
</Card>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
@@ -4,6 +4,7 @@ import { Kysely } from "kysely";
|
|
|
4
4
|
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { demoPlugin } from "./plugins/demo";
|
|
7
|
+
import { workflowDemoPlugin } from "./plugins/workflow-demo";
|
|
7
8
|
import demoRoutes from "./routes/demo";
|
|
8
9
|
|
|
9
10
|
// Simple in-memory database
|
|
@@ -20,8 +21,9 @@ export const server = new AppServer({
|
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
// Register
|
|
24
|
+
// Register plugins
|
|
24
25
|
server.registerPlugin(demoPlugin);
|
|
26
|
+
server.registerPlugin(workflowDemoPlugin);
|
|
25
27
|
|
|
26
28
|
// Register routes
|
|
27
29
|
server.use(demoRoutes);
|